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

108 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-07-01 13:21 +0000

1#!/usr/bin/env python3 

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

3import subprocess 

4import argparse 

5import shutil 

6import os 

7import sys 

8import warnings 

9from pathlib import Path 

10 

11from sparkle.CLI.help import argparse_custom as ac 

12from sparkle.CLI.help import snapshot_help as snh 

13from sparkle.CLI.help import global_variables as gv 

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

15from sparkle.platform import Settings 

16from sparkle.structures import PerformanceDataFrame, FeatureDataFrame 

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 parser.add_argument(*ac.DownloadExamplesArgument.names, 

24 **ac.DownloadExamplesArgument.kwargs) 

25 parser.add_argument(*ac.NoSavePlatformArgument.names, 

26 **ac.NoSavePlatformArgument.kwargs) 

27 parser.add_argument(*ac.RebuildRunsolverArgument.names, 

28 **ac.RebuildRunsolverArgument.kwargs) 

29 return parser 

30 

31 

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

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

34 

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

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

37 

38 Args: 

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

40 

41 Returns: 

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

43 """ 

44 cwd = Path.cwd() 

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

46 if check([(cwd / wd).exists() for wd in gv.settings().DEFAULT_working_dirs]): 

47 return cwd 

48 cwd = cwd.parent 

49 return None 

50 

51 

52def check_for_initialise() -> None: 

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

54 

55 Args: 

56 argv: List of the arguments from the caller. 

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

58 function. 

59 """ 

60 platform_path = detect_sparkle_platform_exists() 

61 if platform_path is None: 

62 print("-----------------------------------------------") 

63 print("No Sparkle platform found; " 

64 "The platform will now be initialized automatically.") 

65 print("-----------------------------------------------") 

66 initialise_sparkle() 

67 elif platform_path != Path.cwd(): 

68 print(f"[WARNING] Sparkle platform found in {platform_path} instead of " 

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

70 os.chdir(platform_path) 

71 

72 

73def initialise_sparkle(save_existing_platform: bool = True, 

74 interactive: bool = False, 

75 download_examples: bool = False, 

76 rebuild_runsolver: bool = False) -> None: 

77 """Initialize a new Sparkle platform. 

78 

79 Args: 

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

81 interactive: Ask for user input or not. 

82 download_examples: Downloads examples from the Sparkle Github. 

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

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

85 """ 

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

87 if detect_sparkle_platform_exists(check=all): 

88 print("Current Sparkle platform found!") 

89 if save_existing_platform: 

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

91 snh.save_current_platform() 

92 snh.remove_current_platform(filter=[gv.settings().DEFAULT_settings_dir]) 

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

94 

95 for working_dir in gv.settings().DEFAULT_working_dirs: 

96 working_dir.mkdir(exist_ok=True) 

97 

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

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

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

101 gv.__settings = Settings(Settings.DEFAULT_example_settings_path) 

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

103 

104 # Initialise the FeatureDataFrame 

105 FeatureDataFrame(gv.settings().DEFAULT_feature_data_path) 

106 

107 # Initialise the Performance DF with the static dimensions 

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

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

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

111 PerformanceDataFrame(gv.settings().DEFAULT_performance_data_path, 

112 objectives=gv.settings().get_general_sparkle_objectives(), 

113 n_runs=1) 

114 

115 if rebuild_runsolver: 

116 print("Cleaning Runsolver ...") 

117 runsolver_clean = subprocess.run(["make", "clean"], 

118 cwd=gv.settings().DEFAULT_runsolver_dir, 

119 capture_output=True) 

120 if runsolver_clean.returncode != 0: 

121 warnings.warn(f"[{runsolver_clean.returncode}] Cleaning of Runsolver failed " 

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

123 

124 # Check that Runsolver is compiled, otherwise, compile 

125 if not gv.settings().DEFAULT_runsolver_exec.exists(): 

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

127 if not (gv.settings().DEFAULT_runsolver_dir / "Makefile").exists(): 

128 warnings.warn("Runsolver executable doesn't exist and cannot find makefile." 

129 " Please verify the contents of the directory: " 

130 f"{gv.settings().DEFAULT_runsolver_dir}") 

131 else: 

132 compile_runsolver =\ 

133 subprocess.run(["make"], 

134 cwd=gv.settings().DEFAULT_runsolver_dir, 

135 capture_output=True) 

136 if compile_runsolver.returncode != 0: 

137 warnings.warn("Compilation of Runsolver failed with the following msg:" 

138 f"[{compile_runsolver.returncode}] " 

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

140 else: 

141 print("Runsolver compiled successfully!") 

142 

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

144 if gv.settings().DEFAULT_runsolver_exec.exists(): 

145 runsolver_check = subprocess.run([gv.settings().DEFAULT_runsolver_exec, 

146 "--version"], 

147 capture_output=True) 

148 if runsolver_check.returncode != 0: 

149 print("WARNING: Runsolver executable cannot be run successfully. " 

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

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

152 

153 # Check that java is available for SMAC2 

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

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

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

157 print("WARNING: Could not find Java as an executable! Java 1.8.0_402 is required" 

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

159 

160 # Check for each configurator that it is available 

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

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

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

164 print("Installing SMAC2 ...") 

165 SMAC2.download_requirements() 

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

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

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

169 print("Installing ParamILS ...") 

170 ParamILS.download_requirements() 

171 if interactive and not IRACE.check_requirements(): 

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

173 print("R is not installed, which is required for the IRACE " 

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

175 else: 

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

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

178 print("Installing IRACE ...") 

179 IRACE.download_requirements() 

180 irace = IRACE() 

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

182 

183 if download_examples: 

184 # Download Sparkle examples from Github 

185 print("Downloading examples ...") 

186 curl = subprocess.Popen( 

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

188 stdout=subprocess.PIPE) 

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

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

191 tar = subprocess.Popen(["tar", "-xz", "--strip=1", "Sparkle-main/Examples"], 

192 stdin=curl.stdout, stdout=outfile) 

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

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

195 outpath.unlink(missing_ok=True) 

196 

197 print("New Sparkle platform initialised!") 

198 

199 

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

201 """Main function of the command.""" 

202 # Define command line arguments 

203 parser = parser_function() 

204 # Process command line arguments 

205 args = parser.parse_args(argv) 

206 initialise_sparkle(save_existing_platform=args.no_save, 

207 interactive=True, 

208 download_examples=args.download_examples, 

209 rebuild_runsolver=args.rebuild_runsolver) 

210 sys.exit(0) 

211 

212 

213if __name__ == "__main__": 

214 main(sys.argv[1:])