Coverage for src / sparkle / CLI / initialise.py: 76%

108 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-21 15:31 +0000

1#!/usr/bin/env python3 

2"""Command to initialise a Sparkle platform.""" 

3 

4import subprocess 

5import argparse 

6import shutil 

7import os 

8import sys 

9import warnings 

10from pathlib import Path 

11 

12from sparkle.CLI.help import argparse_custom as ac 

13from sparkle.CLI.help import snapshot_help as snh 

14 

15from sparkle.platform import Settings 

16from sparkle.CLI.help import global_variables as gv 

17 

18 

19def parser_function() -> argparse.ArgumentParser: 

20 """Parse CLI arguments for the initialise command.""" 

21 parser = argparse.ArgumentParser( 

22 description="Initialise the Sparkle platform in the current directory." 

23 ) 

24 parser.add_argument( 

25 *ac.DownloadExamplesArgument.names, **ac.DownloadExamplesArgument.kwargs 

26 ) 

27 parser.add_argument( 

28 *ac.NoSavePlatformArgument.names, **ac.NoSavePlatformArgument.kwargs 

29 ) 

30 parser.add_argument( 

31 *ac.RebuildRunsolverArgument.names, **ac.RebuildRunsolverArgument.kwargs 

32 ) 

33 return parser 

34 

35 

36def detect_sparkle_platform_exists(check: callable = all) -> Path: 

37 """Return whether a Sparkle platform is currently active. 

38 

39 The default working directories are checked for existence, for each directory in the 

40 CWD. If any of the parents of the CWD yield true, this path is returned 

41 

42 Args: 

43 check: Method to check if the working directory exists. Defaults to all. 

44 

45 Returns: 

46 Path to the Sparkle platform if it exists, None otherwise. 

47 """ 

48 cwd = Path.cwd() 

49 while str(cwd) != cwd.root: 

50 if check([(cwd / wd).exists() for wd in Settings.DEFAULT_working_dirs]): 

51 return cwd 

52 cwd = cwd.parent 

53 return None 

54 

55 

56def check_for_initialise() -> None: 

57 """Function to check if initialize command was executed and execute it otherwise. 

58 

59 Args: 

60 argv: List of the arguments from the caller. 

61 requirements: The requirements that have to be executed before the calling 

62 function. 

63 """ 

64 platform_path = detect_sparkle_platform_exists() 

65 if platform_path is None: 

66 print("-----------------------------------------------") 

67 print( 

68 "No Sparkle platform found; " 

69 "The platform will now be initialized automatically." 

70 ) 

71 print("-----------------------------------------------") 

72 initialise_sparkle() 

73 elif platform_path != Path.cwd(): 

74 print( 

75 f"[WARNING] Sparkle platform found in {platform_path} instead of " 

76 f"{Path.cwd()}. Switching to CWD to {platform_path}" 

77 ) 

78 os.chdir(platform_path) 

79 

80 

81def initialise_sparkle( 

82 save_existing_platform: bool = True, 

83 interactive: bool = False, 

84 download_examples: bool = False, 

85 rebuild_runsolver: bool = False, 

86) -> None: 

87 """Initialize a new Sparkle platform. 

88 

89 Args: 

90 save_existing_platform: If present, save the current platform as a snapshot. 

91 interactive: Ask for user input or not. 

92 download_examples: Downloads examples from the Sparkle Github. 

93 WARNING: May take a some time to complete due to the large amount of data. 

94 rebuild_runsolver: Will clean the RunSolver executable and rebuild it. 

95 """ 

96 print("Start initialising Sparkle platform ...") 

97 # NOTE: Import here for speedup 

98 from sparkle.configurator.implementations import IRACE, SMAC2, ParamILS 

99 from sparkle.structures import PerformanceDataFrame, FeatureDataFrame 

100 

101 if detect_sparkle_platform_exists(check=all): 

102 print("Current Sparkle platform found!") 

103 if save_existing_platform: 

104 print("Saving as snapshot...") 

105 snh.save_current_platform() 

106 snh.remove_current_platform(filter=[Settings.DEFAULT_settings_dir]) 

107 print("Your settings directory was not removed.") 

108 

109 for working_dir in Settings.DEFAULT_working_dirs: 

110 working_dir.mkdir(exist_ok=True) 

111 

112 # Check if Settings file exists, otherwise initialise a default one 

113 if not Path(Settings.DEFAULT_settings_path).exists(): 

114 print("Settings file does not exist, initializing default settings ...") 

115 gv.__settings = Settings(Settings.DEFAULT_example_settings_path) 

116 gv.settings().write_settings_ini(Path(Settings.DEFAULT_settings_path)) 

117 

118 # Initialise the FeatureDataFrame 

119 FeatureDataFrame(Settings.DEFAULT_feature_data_path) 

120 

121 # Initialise the Performance DF with the static dimensions 

122 # TODO: We have many sparkle settings values regarding ``number of runs'' 

123 # E.g. configurator, parallel portfolio, and here too. Should we unify this more, or 

124 # just make another setting that does this specifically for performance data? 

125 PerformanceDataFrame( 

126 Settings.DEFAULT_performance_data_path, 

127 objectives=gv.settings().objectives, 

128 n_runs=1, 

129 ) 

130 

131 if rebuild_runsolver: 

132 print("Cleaning Runsolver ...") 

133 runsolver_clean = subprocess.run( 

134 ["make", "clean"], cwd=Settings.DEFAULT_runsolver_dir, capture_output=True 

135 ) 

136 if runsolver_clean.returncode != 0: 

137 warnings.warn( 

138 f"[{runsolver_clean.returncode}] Cleaning of Runsolver failed " 

139 f"with the following msg: {runsolver_clean.stdout.decode()}" 

140 ) 

141 

142 # Check that Runsolver is compiled, otherwise, compile 

143 if not Settings.DEFAULT_runsolver_exec.exists(): 

144 print("Runsolver does not exist, trying to compile...") 

145 if not (Settings.DEFAULT_runsolver_dir / "Makefile").exists(): 

146 warnings.warn( 

147 "Runsolver executable doesn't exist and cannot find makefile." 

148 " Please verify the contents of the directory: " 

149 f"{Settings.DEFAULT_runsolver_dir}" 

150 ) 

151 else: 

152 compile_runsolver = subprocess.run( 

153 ["make"], cwd=Settings.DEFAULT_runsolver_dir, capture_output=True 

154 ) 

155 if compile_runsolver.returncode != 0: 

156 warnings.warn( 

157 "Compilation of Runsolver failed with the following msg:" 

158 f"[{compile_runsolver.returncode}] " 

159 f"{compile_runsolver.stderr.decode()}" 

160 ) 

161 else: 

162 print("Runsolver compiled successfully!") 

163 

164 # If Runsolver is compiled, check that it can be executed 

165 if Settings.DEFAULT_runsolver_exec.exists(): 

166 runsolver_check = subprocess.run( 

167 [Settings.DEFAULT_runsolver_exec, "--version"], capture_output=True 

168 ) 

169 if runsolver_check.returncode != 0: 

170 print( 

171 "WARNING: Runsolver executable cannot be run successfully. " 

172 "Please verify the following error messages:\n" 

173 f"{runsolver_check.stderr.decode()}" 

174 ) 

175 

176 # Check that java is available for SMAC2 

177 if shutil.which("java") is None: 

178 # NOTE: An automatic resolution of Java at this point would be good 

179 # However, loading modules from Python has thusfar not been successfull. 

180 print( 

181 "WARNING: Could not find Java as an executable! Java 1.8.0_402 is required" 

182 " to use SMAC2 or ParamILS as a configurator. Consider installing Java." 

183 ) 

184 

185 # Check for each configurator that it is available 

186 if interactive and not SMAC2.configurator_executable.exists(): 

187 print("SMAC2 is not installed, would you like to install? (Y/n) ...") 

188 if input().lower() == "y": 

189 print("Installing SMAC2 ...") 

190 SMAC2.download_requirements() 

191 if interactive and not ParamILS.configurator_executable.exists(): 

192 print("ParamILS is not installed, would you like to install? (Y/n) ...") 

193 if input().lower() == "y": 

194 print("Installing ParamILS ...") 

195 ParamILS.download_requirements() 

196 if interactive and not IRACE.check_requirements(): 

197 if shutil.which("R") is None: 

198 print( 

199 "R is not installed, which is required for the IRACE " 

200 "configurator (installation). Consider installing R." 

201 ) 

202 else: 

203 print("IRACE is not installed, would you like to install? (Y/n) ...") 

204 if input().lower() == "y": 

205 print("Installing IRACE ...") 

206 IRACE.download_requirements() 

207 irace = IRACE() 

208 print(f"Installed IRACE version {irace.version}") 

209 

210 if download_examples: 

211 # Download Sparkle examples from Github 

212 print("Downloading examples ...") 

213 curl = subprocess.Popen( 

214 ["curl", "https://codeload.github.com/ADA-research/Sparkle/tar.gz/main"], 

215 stdout=subprocess.PIPE, 

216 ) 

217 outpath = Path("outfile.tar.gz") 

218 with curl.stdout, outpath.open("wb") as outfile: 

219 tar = subprocess.Popen( 

220 ["tar", "-xz", "--strip=1", "Sparkle-main/Examples"], 

221 stdin=curl.stdout, 

222 stdout=outfile, 

223 ) 

224 curl.wait() # Wait for the download to complete 

225 tar.wait() # Wait for the extraction to complete 

226 outpath.unlink(missing_ok=True) 

227 

228 print("New Sparkle platform initialised!") 

229 

230 

231def main(argv: list[str]) -> None: 

232 """Main function of the command.""" 

233 # Define command line arguments 

234 parser = parser_function() 

235 # Process command line arguments 

236 args = parser.parse_args(argv) 

237 initialise_sparkle( 

238 save_existing_platform=args.no_save, 

239 interactive=True, 

240 download_examples=args.download_examples, 

241 rebuild_runsolver=args.rebuild_runsolver, 

242 ) 

243 sys.exit(0) 

244 

245 

246if __name__ == "__main__": 

247 main(sys.argv[1:])