Coverage for sparkle/configurator/implementations/paramils.py: 70%

152 statements  

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

1"""Configurator class to use different configurators like SMAC.""" 

2from __future__ import annotations 

3from pathlib import Path 

4import shutil 

5 

6from runrunner import Runner, Run 

7 

8from sparkle.configurator.configurator import Configurator 

9from sparkle.configurator.implementations.smac2 import SMAC2Scenario 

10from sparkle.solver import Solver 

11from sparkle.structures import PerformanceDataFrame, FeatureDataFrame 

12from sparkle.instance import InstanceSet 

13from sparkle.types import SparkleObjective 

14 

15 

16class ParamILS(Configurator): 

17 """Class for ParamILS (Java) configurator.""" 

18 configurator_path = Path(__file__).parent / "ParamILS" 

19 configurator_executable = configurator_path / "paramils" 

20 target_algorithm = "paramils_target_algorithm.py" 

21 configurator_target = configurator_path / target_algorithm 

22 

23 version = "3.0.0" 

24 full_name = "Parameter Iterated Local Search" 

25 

26 def __init__(self: ParamILS) -> None: 

27 """Returns the ParamILS (Java) configurator, V3.0.0.""" 

28 return super().__init__( 

29 multi_objective_support=False) 

30 

31 @property 

32 def name(self: ParamILS) -> str: 

33 """Returns the name of the configurator.""" 

34 return ParamILS.__name__ 

35 

36 @staticmethod 

37 def scenario_class() -> ParamILSScenario: 

38 """Returns the ParamILS scenario class.""" 

39 return ParamILSScenario 

40 

41 @staticmethod 

42 def check_requirements(verbose: bool = False) -> bool: 

43 """Check that ParamILS is installed.""" 

44 import warnings 

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

46 if verbose: 

47 warnings.warn( 

48 "ParamILS requires Java 1.8.0_402, but Java is not installed. " 

49 "Please ensure Java is installed." 

50 ) 

51 return False 

52 if not ParamILS.configurator_executable.exists(): 

53 if verbose: 

54 warnings.warn( 

55 "ParamILS executable not found. Please ensure ParamILS is installed " 

56 f"in the expected Path ({ParamILS.configurator_path}).") 

57 return False 

58 return True 

59 

60 @staticmethod 

61 def download_requirements( 

62 paramils_zip_url: str = 

63 "https://github.com/ADA-research/Sparkle/raw/refs/heads/" 

64 "development/Resources/Configurators/ParamILS-v.3.0.0.zip" 

65 ) -> None: 

66 """Download ParamILS.""" 

67 if ParamILS.configurator_executable.exists(): 

68 return # Already installed 

69 from urllib.request import urlopen 

70 import zipfile, io 

71 r = urlopen(paramils_zip_url, timeout=60) 

72 z = zipfile.ZipFile(io.BytesIO(r.read())) 

73 z.extractall(ParamILS.configurator_path) 

74 ParamILS.configurator_executable.chmod(0o755) 

75 

76 def configure(self: ParamILS, 

77 scenario: ParamILSScenario, 

78 data_target: PerformanceDataFrame, 

79 validate_after: bool = True, 

80 sbatch_options: list[str] = [], 

81 slurm_prepend: str | list[str] | Path = None, 

82 num_parallel_jobs: int = None, 

83 base_dir: Path = None, 

84 run_on: Runner = Runner.SLURM) -> list[Run]: 

85 """Start configuration job. 

86 

87 Args: 

88 scenario: ConfigurationScenario object 

89 data_target: PerformanceDataFrame where to store the found configurations 

90 validate_after: Whether the Validator will be called after the configuration 

91 sbatch_options: List of slurm batch options to use 

92 slurm_prepend: Slurm script to prepend to the sbatch 

93 num_parallel_jobs: The maximum number of jobs to run parallel. 

94 base_dir: The path where the sbatch scripts will be created for Slurm. 

95 run_on: On which platform to run the jobs. Default: Slurm. 

96 

97 Returns: 

98 A RunRunner Run object. 

99 """ 

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

101 raise RuntimeError( 

102 "ParamILS requires Java 1.8.0_402, but Java is not installed. " 

103 "Please ensure Java is installed and try again." 

104 ) 

105 scenario.create_scenario() 

106 configuration_ids = scenario.configuration_ids 

107 # We set the seed over the last n run ids in the dataframe 

108 # TODO: Setting seeds like this is weird and should be inspected. 

109 seeds = [i for i in range(scenario.number_of_runs)] 

110 output = [f"{(scenario.results_directory).absolute()}/" 

111 f"{scenario.name}_seed_{config_id}_paramils.txt" 

112 for config_id in configuration_ids] 

113 # NOTE: Could add --rungroup $dirname to change the created directory name 

114 cmds = [f"python3 {Configurator.configurator_cli_path.absolute()} " 

115 f"{ParamILS.__name__} {output_file} {data_target.csv_filepath} " 

116 f"{scenario.scenario_file_path} {configuration_id} " 

117 f"{ParamILS.configurator_executable.absolute()} " 

118 f"--scenario-file {scenario.scenario_file_path} " 

119 f"--seed {seed} " 

120 for output_file, configuration_id, seed 

121 in zip(output, configuration_ids, seeds)] 

122 if num_parallel_jobs is not None: 

123 num_parallel_jobs = max(num_parallel_jobs, len(cmds)) 

124 return super().configure( 

125 configuration_commands=cmds, 

126 data_target=data_target, 

127 output=output, 

128 slurm_prepend=slurm_prepend, 

129 num_parallel_jobs=num_parallel_jobs, 

130 scenario=scenario, 

131 configuration_ids=configuration_ids, 

132 validate_after=validate_after, 

133 sbatch_options=sbatch_options, 

134 base_dir=base_dir, 

135 run_on=run_on, 

136 ) 

137 

138 @staticmethod 

139 def organise_output(output_source: Path, 

140 output_target: Path = None, 

141 scenario: ParamILSScenario = None, 

142 configuration_id: str = None) -> None | dict: 

143 """Retrieves configurations from SMAC files and places them in output.""" 

144 # Extract from log file 

145 configuration = {"configuration_id": configuration_id} 

146 skipping = True 

147 for line in output_source.open().readlines(): 

148 if skipping: 

149 if "[INFO ] Differences with initial configuration:" in line: 

150 skipping = False 

151 continue 

152 if ":" not in line or "->" not in line: 

153 break 

154 variable = line.split(":")[0].strip() 

155 value = line.split("->")[1].strip() 

156 configuration[variable] = value 

157 return Configurator.save_configuration(scenario, configuration_id, 

158 configuration, output_target) 

159 

160 def get_status_from_logs(self: ParamILS) -> None: 

161 """Method to scan the log files of the configurator for warnings.""" 

162 return 

163 

164 

165class ParamILSScenario(SMAC2Scenario): 

166 """Class to handle ParamILS configuration scenarios.""" 

167 

168 def __init__(self: ParamILSScenario, 

169 solver: Solver, 

170 instance_set: InstanceSet, 

171 sparkle_objectives: list[SparkleObjective], 

172 number_of_runs: int, 

173 parent_directory: Path, 

174 solver_calls: int = None, 

175 max_iterations: int = None, 

176 solver_cutoff_time: int = None, 

177 cli_cores: int = None, 

178 use_cpu_time_in_tunertime: bool = None, 

179 feature_data: FeatureDataFrame | Path = None, 

180 tuner_timeout: int = None, 

181 focused_ils: bool = True, 

182 initial_configurations: int = None, 

183 min_runs: int = None, 

184 max_runs: int = None, 

185 random_restart: float = None, 

186 )\ 

187 -> None: 

188 """Initialize scenario paths and names. 

189 

190 Args: 

191 solver: Solver that should be configured. 

192 instance_set: Instances object for the scenario. 

193 sparkle_objectives: SparkleObjectives used for each run of the configuration. 

194 parent_directory: Directory in which the scenario should be created. 

195 number_of_runs: The number of configurator runs to perform 

196 for configuring the solver. 

197 solver_calls: The number of times the solver is called for each 

198 configuration run 

199 max_iterations: The maximum number of iterations allowed for each 

200 configuration run. [iteration-limit, numIterations, numberOfIterations] 

201 solver_cutoff_time: The maximum number of seconds allowed for each 

202 Solver call. 

203 cli_cores: The maximum number of cores allowed for each 

204 configuration run. 

205 use_cpu_time_in_tunertime: Whether to use cpu_time in the tuner 

206 time limit. 

207 feature_data: The feature data for the instances in the scenario. 

208 tuner_timeout: The maximum number of seconds allowed for the tuner. 

209 focused_ils: Comparison approach of ParamILS. 

210 True for focused ILS, false for basic. 

211 initial_configurations: The number of initial configurations. 

212 min_runs: The minimum number of runs required for a single configuration. 

213 max_runs: The maximum number of runs allowed for a single configuration. 

214 random_restart: The probability to restart from a random configuration. 

215 """ 

216 super().__init__(solver, instance_set, sparkle_objectives, number_of_runs, 

217 parent_directory, solver_calls, max_iterations, None, 

218 None, solver_cutoff_time, None, cli_cores, 

219 use_cpu_time_in_tunertime, feature_data) 

220 self.solver = solver 

221 self.instance_set = instance_set 

222 self.tuner_timeout = tuner_timeout 

223 self.multi_objective = len(sparkle_objectives) > 1 # Not using MO yet in Sparkle 

224 self.focused = focused_ils 

225 self.initial_configurations = initial_configurations 

226 self.min_runs = min_runs 

227 self.max_runs = max_runs 

228 self.random_restart = random_restart 

229 

230 @property 

231 def configurator(self: ParamILSScenario) -> ParamILS: 

232 """Return the type of configurator the scenario belongs to.""" 

233 return ParamILS 

234 

235 def create_scenario_file(self: ParamILSScenario) -> Path: 

236 """Create a file with the configuration scenario.""" 

237 super().create_scenario_file() 

238 from sparkle.tools.parameters import PCSConvention 

239 scenario_file = super().create_scenario_file(ParamILS.configurator_target, 

240 PCSConvention.ParamILS) 

241 with scenario_file.open("+a") as fout: 

242 fout.write("check-instances-exist = True\n") 

243 if self.focused is not None: 

244 approach = "FOCUSED" if self.focused else "BASIC" 

245 fout.write(f"approach = {approach}\n") 

246 if self.initial_configurations: 

247 fout.write(f"R = {self.initial_configurations}\n") 

248 if self.min_runs: 

249 fout.write(f"min-runs = {self.min_runs}\n") 

250 if self.max_runs: 

251 fout.write(f"max-runs = {self.max_runs}\n") 

252 if self.random_restart: 

253 fout.write(f"random-restart = {self.random_restart}\n") 

254 if self.tuner_timeout: 

255 fout.write(f"tuner-timeout = {self.tuner_timeout}\n") 

256 return scenario_file 

257 

258 @staticmethod 

259 def from_file(scenario_file: Path) -> ParamILSScenario: 

260 """Reads scenario file and initalises ConfigurationScenario.""" 

261 from sparkle.types import resolve_objective 

262 from sparkle.instance import Instance_Set 

263 config = {} 

264 with scenario_file.open() as file: 

265 import ast 

266 for line in file: 

267 key, value = line.strip().split(" = ") 

268 key = key.replace("-", "_") 

269 try: 

270 config[key] = ast.literal_eval(value) 

271 except Exception: 

272 config[key] = value 

273 

274 _, solver_path, _, objective_str = config["algo"].split(" ") 

275 objective = resolve_objective(objective_str) 

276 solver = Solver(Path(solver_path.strip())) 

277 # Extract the instance set from the instance file 

278 instance_file_path = Path(config["instance_file"]) 

279 instance_set_path = Path(instance_file_path.open().readline().strip()).parent 

280 instance_set = Instance_Set(Path(instance_set_path)) 

281 

282 del config["algo"] 

283 del config["run_obj"] 

284 del config["deterministic"] 

285 del config["paramfile"] 

286 del config["instance_file"] 

287 del config["test_instance_file"] 

288 del config["outdir"] 

289 del config["validation"] 

290 del config["check_instances_exist"] 

291 

292 if "cutoffTime" in config: 

293 config["solver_cutoff_time"] = config.pop("cutoffTime") 

294 if "runcount-limit" in config: 

295 config["solver_calls"] = config.pop("runcount-limit") 

296 if "approach" in config: 

297 config["focused_ils"] = config.pop("approach") == "FOCUS" 

298 if "R" in config: 

299 config["initial_configurations"] = config.pop("R") 

300 if "runcount_limit" in config: 

301 config["solver_calls"] = config.pop("runcount_limit") 

302 results_folder = scenario_file.parent / "results" 

303 number_of_runs = len([p for p in results_folder.iterdir() if p.is_file()]) 

304 return ParamILSScenario(solver, 

305 instance_set, 

306 [objective], 

307 number_of_runs, 

308 scenario_file.parent.parent, 

309 **config 

310 )