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
« 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
6from runrunner import Runner, Run
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
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
23 version = "3.0.0"
24 full_name = "Parameter Iterated Local Search"
26 def __init__(self: ParamILS) -> None:
27 """Returns the ParamILS (Java) configurator, V3.0.0."""
28 return super().__init__(
29 multi_objective_support=False)
31 @property
32 def name(self: ParamILS) -> str:
33 """Returns the name of the configurator."""
34 return ParamILS.__name__
36 @staticmethod
37 def scenario_class() -> ParamILSScenario:
38 """Returns the ParamILS scenario class."""
39 return ParamILSScenario
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
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)
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.
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.
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 )
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)
160 def get_status_from_logs(self: ParamILS) -> None:
161 """Method to scan the log files of the configurator for warnings."""
162 return
165class ParamILSScenario(SMAC2Scenario):
166 """Class to handle ParamILS configuration scenarios."""
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.
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
230 @property
231 def configurator(self: ParamILSScenario) -> ParamILS:
232 """Return the type of configurator the scenario belongs to."""
233 return ParamILS
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
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
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))
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"]
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 )