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

152 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-09-29 10:17 +0000

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

2 

3from __future__ import annotations 

4from pathlib import Path 

5import shutil 

6import random 

7 

8from runrunner import Runner, Run 

9 

10from sparkle.configurator.configurator import Configurator 

11from sparkle.configurator.implementations.smac2 import SMAC2Scenario 

12from sparkle.solver import Solver 

13from sparkle.structures import PerformanceDataFrame, FeatureDataFrame 

14from sparkle.instance import InstanceSet 

15from sparkle.types import SparkleObjective 

16 

17 

18class ParamILS(Configurator): 

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

20 

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

22 configurator_executable = configurator_path / "paramils" 

23 target_algorithm = "paramils_target_algorithm.py" 

24 configurator_target = configurator_path / target_algorithm 

25 

26 version = "3.0.0" 

27 full_name = "Parameter Iterated Local Search" 

28 

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

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

31 return super().__init__(multi_objective_support=False) 

32 

33 @property 

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

35 """Returns the name of the configurator.""" 

36 return ParamILS.__name__ 

37 

38 @staticmethod 

39 def scenario_class() -> ParamILSScenario: 

40 """Returns the ParamILS scenario class.""" 

41 return ParamILSScenario 

42 

43 @staticmethod 

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

45 """Check that ParamILS is installed.""" 

46 import warnings 

47 

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

49 if verbose: 

50 warnings.warn( 

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

52 "Please ensure Java is installed." 

53 ) 

54 return False 

55 if not ParamILS.configurator_executable.exists(): 

56 if verbose: 

57 warnings.warn( 

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

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

60 ) 

61 return False 

62 return True 

63 

64 @staticmethod 

65 def download_requirements( 

66 paramils_zip_url: str = "https://github.com/ADA-research/Sparkle/raw/refs/heads/" 

67 "development/Resources/Configurators/ParamILS-v.3.0.0.zip", 

68 ) -> None: 

69 """Download ParamILS.""" 

70 if ParamILS.configurator_executable.exists(): 

71 return # Already installed 

72 from urllib.request import urlopen 

73 import zipfile 

74 import io 

75 

76 r = urlopen(paramils_zip_url, timeout=60) 

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

78 z.extractall(ParamILS.configurator_path) 

79 ParamILS.configurator_executable.chmod(0o755) 

80 

81 def configure( 

82 self: ParamILS, 

83 scenario: ParamILSScenario, 

84 data_target: PerformanceDataFrame, 

85 validate_after: bool = True, 

86 sbatch_options: list[str] = [], 

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

88 num_parallel_jobs: int = None, 

89 base_dir: Path = None, 

90 run_on: Runner = Runner.SLURM, 

91 ) -> list[Run]: 

92 """Start configuration job. 

93 

94 Args: 

95 scenario: ConfigurationScenario object 

96 data_target: PerformanceDataFrame where to store the found configurations 

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

98 sbatch_options: List of slurm batch options to use 

99 slurm_prepend: Slurm script to prepend to the sbatch 

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

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

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

103 

104 Returns: 

105 A RunRunner Run object. 

106 """ 

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

108 raise RuntimeError( 

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

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

111 ) 

112 scenario.create_scenario() 

113 configuration_ids = scenario.configuration_ids 

114 # The maximum seed size for ParamILS is 999 999 999 

115 seeds = [random.randint(0, 10**9 - 1) for _ in range(scenario.number_of_runs)] 

116 output = [ 

117 f"{(scenario.results_directory).absolute()}/" 

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

119 for config_id in configuration_ids 

120 ] 

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

122 cmds = [ 

123 f"python3 {Configurator.configurator_cli_path.absolute()} " 

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

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

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

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

128 f"--seed {seed} " 

129 for output_file, configuration_id, seed in zip( 

130 output, configuration_ids, seeds 

131 ) 

132 ] 

133 if num_parallel_jobs is not None: 

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

135 return super().configure( 

136 configuration_commands=cmds, 

137 data_target=data_target, 

138 output=output, 

139 slurm_prepend=slurm_prepend, 

140 num_parallel_jobs=num_parallel_jobs, 

141 scenario=scenario, 

142 configuration_ids=configuration_ids, 

143 validate_after=validate_after, 

144 sbatch_options=sbatch_options, 

145 base_dir=base_dir, 

146 run_on=run_on, 

147 ) 

148 

149 @staticmethod 

150 def organise_output( 

151 output_source: Path, 

152 output_target: Path = None, 

153 scenario: ParamILSScenario = None, 

154 configuration_id: str = None, 

155 ) -> None | dict: 

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

157 # Extract from log file 

158 configuration = {"configuration_id": configuration_id} 

159 skipping = True 

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

161 if skipping: 

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

163 skipping = False 

164 continue 

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

166 break 

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

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

169 configuration[variable] = value 

170 return Configurator.save_configuration( 

171 scenario, configuration_id, configuration, output_target 

172 ) 

173 

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

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

176 return 

177 

178 

179class ParamILSScenario(SMAC2Scenario): 

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

181 

182 def __init__( 

183 self: ParamILSScenario, 

184 solver: Solver, 

185 instance_set: InstanceSet, 

186 sparkle_objectives: list[SparkleObjective], 

187 number_of_runs: int, 

188 parent_directory: Path, 

189 solver_calls: int = None, 

190 max_iterations: int = None, 

191 solver_cutoff_time: int = None, 

192 cli_cores: int = None, 

193 use_cpu_time_in_tunertime: bool = None, 

194 feature_data: FeatureDataFrame | Path = None, 

195 tuner_timeout: int = None, 

196 focused_ils: bool = True, 

197 initial_configurations: int = None, 

198 min_runs: int = None, 

199 max_runs: int = None, 

200 random_restart: float = None, 

201 timestamp: str = None, 

202 ) -> None: 

203 """Initialize scenario paths and names. 

204 

205 Args: 

206 solver: Solver that should be configured. 

207 instance_set: Instances object for the scenario. 

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

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

210 number_of_runs: The number of configurator runs to perform 

211 for configuring the solver. 

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

213 configuration run 

214 max_iterations: The maximum number of iterations allowed for each 

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

216 solver_cutoff_time: The maximum number of seconds allowed for each 

217 Solver call. 

218 cli_cores: The maximum number of cores allowed for each 

219 configuration run. 

220 use_cpu_time_in_tunertime: Whether to use cpu_time in the tuner 

221 time limit. 

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

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

224 focused_ils: Comparison approach of ParamILS. 

225 True for focused ILS, false for basic. 

226 initial_configurations: The number of initial configurations. 

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

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

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

230 timestamp: An optional timestamp for the directory name. 

231 """ 

232 super().__init__( 

233 solver, 

234 instance_set, 

235 sparkle_objectives, 

236 number_of_runs, 

237 parent_directory, 

238 solver_calls, 

239 max_iterations, 

240 None, 

241 None, 

242 solver_cutoff_time, 

243 None, 

244 cli_cores, 

245 use_cpu_time_in_tunertime, 

246 feature_data, 

247 timestamp, 

248 ) 

249 self.tuner_timeout = tuner_timeout 

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

251 self.focused = focused_ils 

252 self.initial_configurations = initial_configurations 

253 self.min_runs = min_runs 

254 self.max_runs = max_runs 

255 self.random_restart = random_restart 

256 

257 @property 

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

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

260 return ParamILS 

261 

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

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

264 from sparkle.tools.parameters import PCSConvention 

265 

266 scenario_file = super().create_scenario_file( 

267 ParamILS.configurator_target, PCSConvention.ParamILS 

268 ) 

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

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

271 if self.focused is not None: 

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

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

274 if self.initial_configurations: 

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

276 if self.min_runs: 

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

278 if self.max_runs: 

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

280 if self.random_restart: 

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

282 if self.tuner_timeout: 

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

284 return scenario_file 

285 

286 @staticmethod 

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

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

289 from sparkle.types import resolve_objective 

290 from sparkle.instance import Instance_Set 

291 

292 config = {} 

293 with scenario_file.open() as file: 

294 import ast 

295 

296 for line in file: 

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

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

299 try: 

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

301 except Exception: 

302 config[key] = value 

303 

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

305 objective = resolve_objective(objective_str) 

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

307 # Extract the instance set from the instance file 

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

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

310 instance_set = Instance_Set(Path(instance_set_path)) 

311 

312 del config["algo"] 

313 del config["run_obj"] 

314 del config["deterministic"] 

315 del config["paramfile"] 

316 del config["instance_file"] 

317 del config["test_instance_file"] 

318 del config["outdir"] 

319 del config["validation"] 

320 del config["check_instances_exist"] 

321 

322 if "cutoffTime" in config: 

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

324 if "runcount-limit" in config: 

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

326 if "approach" in config: 

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

328 if "R" in config: 

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

330 if "runcount_limit" in config: 

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

332 results_folder = scenario_file.parent / "results" 

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

334 timestamp = scenario_file.parent.name.split("_")[-1] 

335 return ParamILSScenario( 

336 solver, 

337 instance_set, 

338 [objective], 

339 number_of_runs, 

340 scenario_file.parent.parent, 

341 **config, 

342 timestamp=timestamp, 

343 )