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
« 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."""
3from __future__ import annotations
4from pathlib import Path
5import shutil
6import random
8from runrunner import Runner, Run
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
18class ParamILS(Configurator):
19 """Class for ParamILS (Java) configurator."""
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
26 version = "3.0.0"
27 full_name = "Parameter Iterated Local Search"
29 def __init__(self: ParamILS) -> None:
30 """Returns the ParamILS (Java) configurator, V3.0.0."""
31 return super().__init__(multi_objective_support=False)
33 @property
34 def name(self: ParamILS) -> str:
35 """Returns the name of the configurator."""
36 return ParamILS.__name__
38 @staticmethod
39 def scenario_class() -> ParamILSScenario:
40 """Returns the ParamILS scenario class."""
41 return ParamILSScenario
43 @staticmethod
44 def check_requirements(verbose: bool = False) -> bool:
45 """Check that ParamILS is installed."""
46 import warnings
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
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
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)
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.
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.
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 )
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 )
174 def get_status_from_logs(self: ParamILS) -> None:
175 """Method to scan the log files of the configurator for warnings."""
176 return
179class ParamILSScenario(SMAC2Scenario):
180 """Class to handle ParamILS configuration scenarios."""
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.
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
257 @property
258 def configurator(self: ParamILSScenario) -> ParamILS:
259 """Return the type of configurator the scenario belongs to."""
260 return ParamILS
262 def create_scenario_file(self: ParamILSScenario) -> Path:
263 """Create a file with the configuration scenario."""
264 from sparkle.tools.parameters import PCSConvention
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
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
292 config = {}
293 with scenario_file.open() as file:
294 import ast
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
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))
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"]
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 )