Coverage for src / sparkle / platform / settings_objects.py: 96%

582 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-21 15:31 +0000

1"""Classes and Enums to control settings.""" 

2 

3from __future__ import annotations 

4from typing import TYPE_CHECKING 

5 

6 

7import argparse 

8import configparser 

9from enum import Enum 

10from pathlib import Path 

11from typing import Any, NamedTuple, Optional 

12 

13from runrunner import Runner 

14 

15from sparkle.platform.cli_types import VerbosityLevel 

16from sparkle.types import SparkleObjective, resolve_objective 

17 

18 

19if TYPE_CHECKING: 

20 from sparkle.configurator.configurator import Configurator 

21 

22 

23class Option(NamedTuple): 

24 """Class to define an option in the Settings.""" 

25 

26 name: str 

27 section: str 

28 type: Any 

29 default_value: Any 

30 alternatives: tuple[str, ...] 

31 help: str = "" 

32 cli_kwargs: dict[str, Any] = {} 

33 

34 def __str__(self: Option) -> str: 

35 """Return the option name.""" 

36 return self.name 

37 

38 def __eq__(self: Option, other: Any) -> bool: 

39 """Check if two options are equal.""" 

40 if isinstance(other, Option): 

41 return ( 

42 self.name == other.name 

43 and self.section == other.section 

44 and self.type == other.type 

45 and self.default_value == other.default_value 

46 and self.alternatives == other.alternatives 

47 ) 

48 if isinstance(other, str): 

49 return self.name == other or other in self.alternatives 

50 return False 

51 

52 @property 

53 def args(self: Option) -> list[str]: 

54 """Return the option names as a command line arguments.""" 

55 return [ 

56 f"--{name.replace('_', '-')}" 

57 for name in [self.name] + list(self.alternatives) 

58 ] 

59 

60 @property 

61 def kwargs(self: Option) -> dict[str, Any]: 

62 """Return the option attributes as kwargs.""" 

63 kw = {"help": self.help, **self.cli_kwargs} 

64 

65 # If this option uses a boolean flag action, argparse must NOT receive 'type' 

66 action = kw.get("action") 

67 if action in ("store_true", "store_false"): 

68 return kw 

69 

70 # Otherwise include the base 'type' 

71 return {"type": self.type, **kw} 

72 

73 

74class Settings: 

75 """Class to read, write, set, and get settings.""" 

76 

77 # CWD Prefix 

78 cwd_prefix = Path() # Empty for now 

79 

80 # Library prefix 

81 lib_prefix = Path(__file__).parent.parent.resolve() 

82 

83 # Default directory names 

84 rawdata_dir = Path("Raw_Data") 

85 analysis_dir = Path("Analysis") 

86 DEFAULT_settings_dir = Path("Settings") 

87 __settings_file = Path("sparkle_settings.ini") 

88 __latest_settings_file = Path("latest.ini") 

89 

90 # Default settings path 

91 DEFAULT_settings_path = Path(cwd_prefix / DEFAULT_settings_dir / __settings_file) 

92 DEFAULT_previous_settings_path = Path( 

93 cwd_prefix / DEFAULT_settings_dir / __latest_settings_file 

94 ) 

95 DEFAULT_reference_dir = DEFAULT_settings_dir / "Reference_Lists" 

96 

97 # Default library pathing 

98 DEFAULT_components = lib_prefix / "Components" 

99 

100 # Report Component: Bilbiography 

101 bibliography_path = DEFAULT_components / "latex_source" / "report.bib" 

102 

103 # Example settings path 

104 DEFAULT_example_settings_path = Path(DEFAULT_components / "sparkle_settings.ini") 

105 

106 # Wrapper templates pathing 

107 DEFAULT_solver_wrapper_template = DEFAULT_components / "sparkle_solver_wrapper.sh" 

108 

109 # Runsolver component 

110 DEFAULT_runsolver_dir = DEFAULT_components / "runsolver" / "src" 

111 DEFAULT_runsolver_exec = DEFAULT_runsolver_dir / "runsolver" 

112 

113 # Ablation component 

114 DEFAULT_ablation_dir = DEFAULT_components / "ablationAnalysis-0.9.4" 

115 DEFAULT_ablation_exec = DEFAULT_ablation_dir / "ablationAnalysis" 

116 DEFAULT_ablation_validation_exec = DEFAULT_ablation_dir / "ablationValidation" 

117 

118 # Default input directory pathing 

119 DEFAULT_solver_dir = cwd_prefix / "Solvers" 

120 DEFAULT_instance_dir = cwd_prefix / "Instances" 

121 DEFAULT_extractor_dir = cwd_prefix / "Extractors" 

122 DEFAULT_snapshot_dir = cwd_prefix / "Snapshots" 

123 

124 # Default output directory pathing 

125 DEFAULT_tmp_output = cwd_prefix / "Tmp" 

126 DEFAULT_output = cwd_prefix / "Output" 

127 DEFAULT_configuration_output = DEFAULT_output / "Configuration" 

128 DEFAULT_selection_output = DEFAULT_output / "Selection" 

129 DEFAULT_parallel_portfolio_output = DEFAULT_output / "Parallel_Portfolio" 

130 DEFAULT_ablation_output = DEFAULT_output / "Ablation" 

131 DEFAULT_log_output = DEFAULT_output / "Log" 

132 

133 # Default output subdirs 

134 DEFAULT_output_analysis = DEFAULT_output / analysis_dir 

135 

136 # Old default output dirs which should be part of something else 

137 DEFAULT_feature_data = DEFAULT_output / "Feature_Data" 

138 DEFAULT_performance_data = DEFAULT_output / "Performance_Data" 

139 

140 # Collection of all working dirs for platform 

141 DEFAULT_working_dirs = [ 

142 DEFAULT_solver_dir, 

143 DEFAULT_instance_dir, 

144 DEFAULT_extractor_dir, 

145 DEFAULT_output, 

146 DEFAULT_configuration_output, 

147 DEFAULT_selection_output, 

148 DEFAULT_output_analysis, 

149 DEFAULT_tmp_output, 

150 DEFAULT_log_output, 

151 DEFAULT_feature_data, 

152 DEFAULT_performance_data, 

153 DEFAULT_settings_dir, 

154 DEFAULT_reference_dir, 

155 ] 

156 

157 # Old default file paths from GV which should be turned into variables 

158 DEFAULT_feature_data_path = DEFAULT_feature_data / "feature_data.csv" 

159 DEFAULT_performance_data_path = DEFAULT_performance_data / "performance_data.csv" 

160 

161 # Define sections and options 

162 # GENERAL Options 

163 SECTION_general: str = "general" 

164 OPTION_objectives = Option( 

165 "objectives", 

166 SECTION_general, 

167 str, 

168 None, 

169 ("sparkle_objectives",), 

170 "A list of Sparkle objectives.", 

171 cli_kwargs={"nargs": "+"}, 

172 ) 

173 OPTION_configurator = Option( 

174 "configurator", 

175 SECTION_general, 

176 str, 

177 None, 

178 tuple(), 

179 "Name of the configurator to use.", 

180 ) 

181 OPTION_solver_cutoff_time = Option( 

182 "solver_cutoff_time", 

183 SECTION_general, 

184 int, 

185 None, 

186 ("target_cutoff_time", "cutoff_time_each_solver_call"), 

187 "Solver cutoff time in seconds.", 

188 ) 

189 OPTION_extractor_cutoff_time = Option( 

190 "extractor_cutoff_time", 

191 SECTION_general, 

192 int, 

193 None, 

194 tuple("cutoff_time_each_feature_computation"), 

195 "Extractor cutoff time in seconds.", 

196 ) 

197 OPTION_run_on = Option( 

198 "run_on", 

199 SECTION_general, 

200 Runner, 

201 None, 

202 tuple(), 

203 "On which compute resource to execute.", 

204 cli_kwargs={"choices": [Runner.LOCAL, Runner.SLURM]}, 

205 ) 

206 OPTION_verbosity = Option( 

207 "verbosity", 

208 SECTION_general, 

209 VerbosityLevel, 

210 VerbosityLevel.STANDARD, 

211 ("verbosity_level",), 

212 "Verbosity level.", 

213 ) 

214 OPTION_seed = Option( 

215 "seed", 

216 SECTION_general, 

217 int, 

218 None, 

219 tuple(), 

220 "Seed to use for pseudo-random number generators.", 

221 ) 

222 OPTION_appendices = Option( 

223 "appendices", 

224 SECTION_general, 

225 bool, 

226 False, 

227 tuple(), 

228 "Include the appendix section in the generated report.", 

229 cli_kwargs={ 

230 "action": "store_true", 

231 "default": None, 

232 }, 

233 ) 

234 

235 # CONFIGURATION Options 

236 SECTION_configuration = "configuration" 

237 OPTION_configurator_number_of_runs = Option( 

238 "number_of_runs", 

239 SECTION_configuration, 

240 int, 

241 None, 

242 tuple(), 

243 "The number of independent configurator jobs/runs.", 

244 ) 

245 OPTION_configurator_solver_call_budget = Option( 

246 "solver_calls", 

247 SECTION_configuration, 

248 int, 

249 None, 

250 tuple(), 

251 "The maximum number of calls (evaluations) a configurator can do in a single " 

252 "run of the solver.", 

253 ) 

254 OPTION_configurator_max_iterations = Option( 

255 "max_iterations", 

256 SECTION_configuration, 

257 int, 

258 None, 

259 ("maximum_iterations",), 

260 "The maximum number of iterations a configurator can do in a single job.", 

261 ) 

262 

263 # ABLATION Options 

264 SECTION_ablation = "ablation" 

265 OPTION_ablation_racing = Option( 

266 "racing", 

267 SECTION_ablation, 

268 bool, 

269 False, 

270 ("ablation_racing",), 

271 "Set a flag indicating whether racing should be used for ablation.", 

272 ) 

273 OPTION_ablation_clis_per_node = Option( 

274 "clis_per_node", 

275 SECTION_ablation, 

276 int, 

277 None, 

278 ( 

279 "max_parallel_runs_per_node", 

280 "maximum_parallel_runs_per_node", 

281 ), 

282 "The maximum number of ablation analysis jobs to run in parallel on a single " 

283 "node.", 

284 ) 

285 

286 # SELECTION Options 

287 SECTION_selection = "selection" 

288 OPTION_selection_class = Option( 

289 "selector_class", 

290 SECTION_selection, 

291 str, 

292 None, 

293 ("class",), 

294 "Can contain any of the class names as defined in asf.selectors.", 

295 ) 

296 OPTION_selection_model = Option( 

297 "selector_model", 

298 SECTION_selection, 

299 str, 

300 None, 

301 ("model",), 

302 "Can be any of the sklearn.ensemble models.", 

303 ) 

304 OPTION_minimum_marginal_contribution = Option( 

305 "minimum_marginal_contribution", 

306 SECTION_selection, 

307 float, 

308 0.01, 

309 ( 

310 "minimum_marginal_contribution", 

311 "minimum_contribution", 

312 "contribution_threshold", 

313 ), 

314 "The minimum marginal contribution a solver (configuration) must have to be used for the selector.", 

315 ) 

316 

317 # SMAC2 Options 

318 SECTION_smac2 = "smac2" 

319 OPTION_smac2_wallclock_time_budget = Option( 

320 "wallclock_time_budget", 

321 SECTION_smac2, 

322 int, 

323 None, 

324 ("wallclock_time",), 

325 "The wallclock time budget in seconds for each SMAC2 run.", 

326 ) 

327 OPTION_smac2_cpu_time_budget = Option( 

328 "cpu_time_budget", 

329 SECTION_smac2, 

330 int, 

331 None, 

332 ("cpu_time",), 

333 "The cpu time budget in seconds for each SMAC2 run.", 

334 ) 

335 OPTION_smac2_target_cutoff_length = Option( 

336 "target_cutoff_length", 

337 SECTION_smac2, 

338 str, 

339 None, 

340 ("cutoff_length", "solver_cutoff_length"), 

341 "The target cutoff length for SMAC2 solver call.", 

342 ) 

343 OPTION_smac2_count_tuner_time = Option( 

344 "use_cpu_time_in_tunertime", 

345 SECTION_smac2, 

346 bool, 

347 None, 

348 ("countSMACTimeAsTunerTime",), 

349 "Whether to count and deducted SMAC2 CPU time from the CPU time budget.", 

350 ) 

351 OPTION_smac2_cli_cores = Option( 

352 "cli_cores", 

353 SECTION_smac2, 

354 int, 

355 None, 

356 tuple(), 

357 "Number of cores to use to execute SMAC2 runs.", 

358 ) 

359 OPTION_smac2_max_iterations = Option( 

360 "max_iterations", 

361 SECTION_smac2, 

362 int, 

363 None, 

364 ( 

365 "iteration_limit", 

366 "numIterations", 

367 "numberOfIterations", 

368 ), 

369 "The maximum number of iterations SMAC2 configurator can do in a single job.", 

370 ) 

371 

372 # SMAC3 Options 

373 SECTION_smac3 = "smac3" 

374 OPTION_smac3_number_of_trials = Option( 

375 "n_trials", 

376 SECTION_smac3, 

377 int, 

378 None, 

379 ("n_trials", "number_of_trials", "solver_calls"), 

380 "Maximum calls SMAC3 is allowed to make to the Solver in a single run/job.", 

381 ) 

382 OPTION_smac3_facade = Option( 

383 "facade", 

384 SECTION_smac3, 

385 str, 

386 "AlgorithmConfigurationFacade", 

387 ("facade", "smac_facade", "smac3_facade"), 

388 "The SMAC3 Facade to use. See the SMAC3 documentation for more options.", 

389 ) 

390 OPTION_smac3_facade_max_ratio = Option( 

391 "facade_max_ratio", 

392 SECTION_smac3, 

393 float, 

394 None, 

395 ("facade_max_ratio", "smac3_facade_max_ratio", "smac3_facade_max_ratio"), 

396 "The SMAC3 Facade max ratio. See the SMAC3 documentation for more options.", 

397 ) 

398 OPTION_smac3_crash_cost = Option( 

399 "crash_cost", 

400 SECTION_smac3, 

401 float, 

402 None, 

403 tuple(), 

404 "Defines the cost for a failed trial, defaults in SMAC3 to np.inf.", 

405 ) 

406 OPTION_smac3_termination_cost_threshold = Option( 

407 "termination_cost_threshold", 

408 SECTION_smac3, 

409 float, 

410 None, 

411 tuple(), 

412 "Defines a cost threshold when the SMAC3 optimization should stop.", 

413 ) 

414 OPTION_smac3_wallclock_time_budget = Option( 

415 "walltime_limit", 

416 SECTION_smac3, 

417 float, 

418 None, 

419 ("wallclock_time", "wallclock_budget", "wallclock_time_budget"), 

420 "The maximum time in seconds that SMAC3 is allowed to run per job.", 

421 ) 

422 OPTION_smac3_cpu_time_budget = Option( 

423 "cputime_limit", 

424 SECTION_smac3, 

425 float, 

426 None, 

427 ("cpu_time", "cpu_budget", "cpu_time_budget"), 

428 "The maximum CPU time in seconds that SMAC3 is allowed to run per job.", 

429 ) 

430 OPTION_smac3_use_default_config = Option( 

431 "use_default_config", 

432 SECTION_smac3, 

433 bool, 

434 None, 

435 tuple(), 

436 "If True, the configspace's default configuration is evaluated in the initial " 

437 "design. For historic benchmark reasons, this is False by default. Notice, that " 

438 "this will result in n_configs + 1 for the initial design. Respecting n_trials, " 

439 "this will result in one fewer evaluated configuration in the optimization.", 

440 ) 

441 OPTION_smac3_min_budget = Option( 

442 "min_budget", 

443 SECTION_smac3, 

444 float, 

445 None, 

446 ("minimum_budget",), 

447 "The minimum budget (epochs, subset size, number of instances, ...) that is used" 

448 " for the optimization. Use this argument if you use multi-fidelity or instance " 

449 "optimization.", 

450 ) 

451 OPTION_smac3_max_budget = Option( 

452 "max_budget", 

453 SECTION_smac3, 

454 float, 

455 None, 

456 ("maximum_budget",), 

457 "The maximum budget (epochs, subset size, number of instances, ...) that is used" 

458 " for the optimization. Use this argument if you use multi-fidelity or instance " 

459 "optimization.", 

460 ) 

461 

462 # IRACE Options 

463 SECTION_irace = "irace" 

464 OPTION_irace_max_time = Option( 

465 "max_time", 

466 SECTION_irace, 

467 int, 

468 0, 

469 ("maximum_time",), 

470 "The maximum time in seconds for each IRACE run/job.", 

471 ) 

472 OPTION_irace_max_experiments = Option( 

473 "max_experiments", 

474 SECTION_irace, 

475 int, 

476 0, 

477 ("maximum_experiments",), 

478 "The maximum number of experiments for each IRACE run/job.", 

479 ) 

480 OPTION_irace_first_test = Option( 

481 "first_test", 

482 SECTION_irace, 

483 int, 

484 None, 

485 tuple(), 

486 "Specifies how many instances are evaluated before the first elimination test. " 

487 "IRACE Default: 5.", 

488 ) 

489 OPTION_irace_mu = Option( 

490 "mu", 

491 SECTION_irace, 

492 int, 

493 None, 

494 tuple(), 

495 "Parameter used to define the number of configurations sampled and evaluated at " 

496 "each iteration. IRACE Default: 5.", 

497 ) 

498 OPTION_irace_max_iterations = Option( 

499 "max_iterations", 

500 SECTION_irace, 

501 int, 

502 None, 

503 ("nb_iterations", "iterations", "max_iterations"), 

504 "Maximum number of iterations to be executed. Each iteration involves the " 

505 "generation of new configurations and the use of racing to select the best " 

506 "configurations. By default (with 0), irace calculates a minimum number of " 

507 "iterations as N^iter = ⌊2 + log2 N param⌋, where N^param is the number of " 

508 "non-fixed parameters to be tuned. Setting this parameter may make irace stop " 

509 "sooner than it should without using all the available budget. IRACE recommends" 

510 " to use the default value (Empty).", 

511 ) 

512 

513 # ParamILS Options 

514 SECTION_paramils = "paramils" 

515 OPTION_paramils_min_runs = Option( 

516 "min_runs", 

517 SECTION_paramils, 

518 int, 

519 None, 

520 ("minimum_runs",), 

521 "Set the minimum number of runs for ParamILS for each run/job.", 

522 ) 

523 OPTION_paramils_max_runs = Option( 

524 "max_runs", 

525 SECTION_paramils, 

526 int, 

527 None, 

528 ("maximum_runs",), 

529 "Set the maximum number of runs for ParamILS for each run/job.", 

530 ) 

531 OPTION_paramils_cpu_time_budget = Option( 

532 "cputime_budget", 

533 SECTION_paramils, 

534 int, 

535 None, 

536 ( 

537 "cputime_limit", 

538 "cputime_limit", 

539 "tunertime_limit", 

540 "tuner_timeout", 

541 "tunerTimeout", 

542 ), 

543 "The maximum CPU time for each ParamILS run/job.", 

544 ) 

545 OPTION_paramils_random_restart = Option( 

546 "random_restart", 

547 SECTION_paramils, 

548 float, 

549 None, 

550 tuple(), 

551 "Set the random restart chance for ParamILS.", 

552 ) 

553 OPTION_paramils_focused = Option( 

554 "focused_approach", 

555 SECTION_paramils, 

556 bool, 

557 False, 

558 ("focused",), 

559 "Set the focused approach for ParamILS.", 

560 ) 

561 OPTION_paramils_count_tuner_time = Option( 

562 "use_cpu_time_in_tunertime", 

563 SECTION_paramils, 

564 bool, 

565 None, 

566 tuple(), 

567 "Whether to count and deducted ParamILS CPU time from the CPU time budget.", 

568 ) 

569 OPTION_paramils_cli_cores = Option( 

570 "cli_cores", 

571 SECTION_paramils, 

572 int, 

573 None, 

574 tuple(), 

575 "Number of cores to use for ParamILS runs.", 

576 ) 

577 OPTION_paramils_max_iterations = Option( 

578 "max_iterations", 

579 SECTION_paramils, 

580 int, 

581 None, 

582 ( 

583 "iteration_limit", 

584 "numIterations", 

585 "numberOfIterations", 

586 "maximum_iterations", 

587 ), 

588 "The maximum number of ParamILS iterations per run/job.", 

589 ) 

590 OPTION_paramils_number_initial_configurations = Option( 

591 "initial_configurations", 

592 SECTION_paramils, 

593 int, 

594 None, 

595 "The number of initial configurations ParamILS should evaluate.", 

596 ) 

597 

598 SECTION_parallel_portfolio = "parallel_portfolio" 

599 OPTION_parallel_portfolio_check_interval = Option( 

600 "check_interval", 

601 SECTION_parallel_portfolio, 

602 int, 

603 None, 

604 tuple(), 

605 "The interval time in seconds when Solvers are checked for their status.", 

606 ) 

607 OPTION_parallel_portfolio_number_of_seeds_per_solver = Option( 

608 "num_seeds_per_solver", 

609 SECTION_parallel_portfolio, 

610 int, 

611 None, 

612 ("solver_seeds",), 

613 "The number of seeds per solver.", 

614 ) 

615 

616 SECTION_slurm = "slurm" 

617 OPTION_slurm_parallel_jobs = Option( 

618 "number_of_jobs_in_parallel", 

619 SECTION_slurm, 

620 int, 

621 None, 

622 ("num_job_in_parallel",), 

623 "The number of jobs to run in parallel.", 

624 ) 

625 OPTION_slurm_prepend_script = Option( 

626 "prepend_script", 

627 SECTION_slurm, 

628 str, 

629 None, 

630 ("job_prepend", "prepend"), 

631 "Slurm script to prepend to the sbatch.", 

632 ) 

633 

634 sections_options: dict[str, list[Option]] = { 

635 SECTION_general: [ 

636 OPTION_objectives, 

637 OPTION_configurator, 

638 OPTION_solver_cutoff_time, 

639 OPTION_extractor_cutoff_time, 

640 OPTION_run_on, 

641 OPTION_appendices, 

642 OPTION_verbosity, 

643 OPTION_seed, 

644 ], 

645 SECTION_configuration: [ 

646 OPTION_configurator_number_of_runs, 

647 OPTION_configurator_solver_call_budget, 

648 OPTION_configurator_max_iterations, 

649 ], 

650 SECTION_ablation: [ 

651 OPTION_ablation_racing, 

652 OPTION_ablation_clis_per_node, 

653 ], 

654 SECTION_selection: [ 

655 OPTION_selection_class, 

656 OPTION_selection_model, 

657 OPTION_minimum_marginal_contribution, 

658 ], 

659 SECTION_smac2: [ 

660 OPTION_smac2_wallclock_time_budget, 

661 OPTION_smac2_cpu_time_budget, 

662 OPTION_smac2_target_cutoff_length, 

663 OPTION_smac2_count_tuner_time, 

664 OPTION_smac2_cli_cores, 

665 OPTION_smac2_max_iterations, 

666 ], 

667 SECTION_smac3: [ 

668 OPTION_smac3_number_of_trials, 

669 OPTION_smac3_facade, 

670 OPTION_smac3_facade_max_ratio, 

671 OPTION_smac3_crash_cost, 

672 OPTION_smac3_termination_cost_threshold, 

673 OPTION_smac3_wallclock_time_budget, 

674 OPTION_smac3_cpu_time_budget, 

675 OPTION_smac3_use_default_config, 

676 OPTION_smac3_min_budget, 

677 OPTION_smac3_max_budget, 

678 ], 

679 SECTION_irace: [ 

680 OPTION_irace_max_time, 

681 OPTION_irace_max_experiments, 

682 OPTION_irace_first_test, 

683 OPTION_irace_mu, 

684 OPTION_irace_max_iterations, 

685 ], 

686 SECTION_paramils: [ 

687 OPTION_paramils_min_runs, 

688 OPTION_paramils_max_runs, 

689 OPTION_paramils_cpu_time_budget, 

690 OPTION_paramils_random_restart, 

691 OPTION_paramils_focused, 

692 OPTION_paramils_count_tuner_time, 

693 OPTION_paramils_cli_cores, 

694 OPTION_paramils_max_iterations, 

695 OPTION_paramils_number_initial_configurations, 

696 ], 

697 SECTION_parallel_portfolio: [ 

698 OPTION_parallel_portfolio_check_interval, 

699 OPTION_parallel_portfolio_number_of_seeds_per_solver, 

700 ], 

701 SECTION_slurm: [OPTION_slurm_parallel_jobs, OPTION_slurm_prepend_script], 

702 } 

703 

704 def __init__( 

705 self: Settings, file_path: Path, argsv: argparse.Namespace = None 

706 ) -> None: 

707 """Initialise a settings object. 

708 

709 Args: 

710 file_path (Path): Path to the settings file. 

711 argsv: The CLI arguments to process. 

712 """ 

713 # Settings 'dictionary' in configparser format 

714 self.__settings = configparser.ConfigParser() 

715 for section in self.sections_options.keys(): 

716 self.__settings.add_section(section) 

717 self.__settings[section] = {} 

718 

719 # General attributes 

720 self.__sparkle_objectives: list[SparkleObjective] = None 

721 self.__general_sparkle_configurator: Configurator = None 

722 self.__solver_cutoff_time: int = None 

723 self.__extractor_cutoff_time: int = None 

724 self.__run_on: Runner = None 

725 self.__appendices: bool = False 

726 self.__verbosity_level: VerbosityLevel = None 

727 self.__seed: Optional[int] = None 

728 

729 # Configuration attributes 

730 self.__configurator_solver_call_budget: int = None 

731 self.__configurator_number_of_runs: int = None 

732 self.__configurator_max_iterations: int = None 

733 

734 # Ablation attributes 

735 self.__ablation_racing_flag: bool = None 

736 self.__ablation_max_parallel_runs_per_node: int = None 

737 

738 # Selection attributes 

739 self.__selection_model: str = None 

740 self.__selection_class: str = None 

741 self.__minimum_marginal_contribution: float = None 

742 

743 # SMAC2 attributes 

744 self.__smac2_wallclock_time_budget: int = None 

745 self.__smac2_cpu_time_budget: int = None 

746 self.__smac2_target_cutoff_length: str = None 

747 self.__smac2_use_tunertime_in_cpu_time_budget: bool = None 

748 self.__smac2_cli_cores: int = None 

749 self.__smac2_max_iterations: int = None 

750 

751 # SMAC3 attributes 

752 self.__smac3_number_of_trials: int = None 

753 self.__smac3_facade: str = None 

754 self.__smac3_facade_max_ratio: float = None 

755 self.__smac3_crash_cost: float = None 

756 self.__smac3_termination_cost_threshold: float = None 

757 self.__smac3_wallclock_time_limit: int = None 

758 self.__smac3_cputime_limit: int = None 

759 self.__smac3_use_default_config: bool = None 

760 self.__smac3_min_budget: float = None 

761 self.__smac3_max_budget: float = None 

762 

763 # IRACE attributes 

764 self.__irace_max_time: int = None 

765 self.__irace_max_experiments: int = None 

766 self.__irace_first_test: int = None 

767 self.__irace_mu: int = None 

768 self.__irace_max_iterations: int = None 

769 

770 # ParamILS attributes 

771 self.__paramils_cpu_time_budget: int = None 

772 self.__paramils_min_runs: int = None 

773 self.__paramils_max_runs: int = None 

774 self.__paramils_random_restart: float = None 

775 self.__paramils_focused_approach: bool = None 

776 self.__paramils_use_cpu_time_in_tunertime: bool = None 

777 self.__paramils_cli_cores: int = None 

778 self.__paramils_max_iterations: int = None 

779 self.__paramils_number_initial_configurations: int = None 

780 

781 # Parallel portfolio attributes 

782 self.__parallel_portfolio_check_interval: int = None 

783 self.__parallel_portfolio_num_seeds_per_solver: int = None 

784 

785 # Slurm attributes 

786 self.__slurm_jobs_in_parallel: int = None 

787 self.__slurm_job_prepend: str = None 

788 

789 # The seed that has been used to set the random state 

790 self.random_state: Optional[int] = None 

791 

792 if file_path and file_path.exists(): 

793 self.read_settings_ini(file_path) 

794 

795 if argsv: 

796 self.apply_arguments(argsv) 

797 

798 def read_settings_ini(self: Settings, file_path: Path) -> None: 

799 """Read the settings from an INI file.""" 

800 if not file_path.exists(): 

801 raise ValueError(f"Settings file {file_path} does not exist.") 

802 # Read file 

803 file_settings = configparser.ConfigParser() 

804 file_settings.read(file_path) 

805 

806 # Set internal settings based on data read from FILE if they were read 

807 # successfully 

808 if file_settings.sections() == []: 

809 # Print error if unable to read the settings 

810 print( 

811 f"ERROR: Failed to read settings from {file_path} The file may " 

812 "have been empty or be in another format than INI." 

813 ) 

814 return 

815 

816 for section in file_settings.sections(): 

817 if section not in self.__settings.sections(): 

818 print(f'Unrecognised section: "{section}" in file {file_path} ignored') 

819 continue 

820 for option_name in file_settings.options(section): 

821 if option_name not in self.sections_options[section]: 

822 if section == Settings.SECTION_slurm: # Flexible section 

823 self.__settings.set( 

824 section, 

825 option_name, 

826 file_settings.get(section, option_name), 

827 ) 

828 else: 

829 print( 

830 f'Unrecognised section - option combination: "{section} ' 

831 f'{option_name}" in file {file_path} ignored' 

832 ) 

833 continue 

834 option_index = self.sections_options[section].index(option_name) 

835 option = self.sections_options[section][option_index] 

836 self.__settings.set( 

837 section, option.name, file_settings.get(section, option_name) 

838 ) 

839 del file_settings 

840 

841 def write_settings_ini(self: Settings, file_path: Path) -> None: 

842 """Write the settings to an INI file.""" 

843 # Create needed directories if they don't exist 

844 file_path.parent.mkdir(parents=True, exist_ok=True) 

845 # We don't write empty sections 

846 for s in self.__settings.sections(): 

847 if not self.__settings[s]: 

848 self.__settings.remove_section(s) 

849 with file_path.open("w") as fout: 

850 self.__settings.write(fout) 

851 for s in self.sections_options.keys(): 

852 if s not in self.__settings.sections(): 

853 self.__settings.add_section(s) 

854 

855 def write_used_settings(self: Settings) -> None: 

856 """Write the used settings to the default locations.""" 

857 # Write to latest settings file 

858 self.write_settings_ini(self.DEFAULT_previous_settings_path) 

859 

860 def apply_arguments(self: Settings, argsv: argparse.Namespace) -> None: 

861 """Apply the arguments to the settings.""" 

862 # Read a possible second file, that overwrites the first, where applicable 

863 # e.g. settings are not deleted, but overwritten where applicable 

864 if hasattr(argsv, "settings_file") and argsv.settings_file: 

865 self.read_settings_ini(argsv.settings_file) 

866 # Match all possible arguments to the settings 

867 for argument in argsv.__dict__.keys(): 

868 value = argsv.__dict__[argument] 

869 if value is None: 

870 continue # Skip None 

871 if isinstance(value, Enum): 

872 value = value.name 

873 elif isinstance(value, list): 

874 value = ",".join([str(s) for s in value]) 

875 else: 

876 value = str(value) 

877 for section in self.sections_options.keys(): 

878 if argument in self.sections_options[section]: 

879 index = self.sections_options[section].index(argument) 

880 option = self.sections_options[section][index] 

881 self.__settings.set(option.section, option.name, value) 

882 break 

883 

884 def _abstract_getter(self: Settings, option: Option) -> Any: 

885 """Abstract getter method.""" 

886 if self.__settings.has_option(option.section, option.name): 

887 if option.type is bool: 

888 return self.__settings.getboolean(option.section, option.name) 

889 value = self.__settings.get(option.section, option.name) 

890 if not isinstance(value, option.type): 

891 if issubclass(option.type, Enum): 

892 return option.type[value.upper()] 

893 return option.type(value) # Attempt to resolve str to type 

894 return value 

895 return option.default_value 

896 

897 # General settings ### 

898 @property 

899 def objectives(self: Settings) -> list[SparkleObjective]: 

900 """Get the objectives for Sparkle.""" 

901 if self.__sparkle_objectives is None and self.__settings.has_option( 

902 Settings.SECTION_general, "objectives" 

903 ): 

904 objectives = self.__settings[Settings.SECTION_general]["objectives"] 

905 if "status:metric" not in objectives: 

906 objectives += ",status:metric" 

907 if "cpu_time:metric" not in objectives: 

908 objectives += ",cpu_time:metric" 

909 if "wall_time:metric" not in objectives: 

910 objectives += ",wall_time:metric" 

911 if "memory:metric" not in objectives: 

912 objectives += ",memory:metric" 

913 self.__sparkle_objectives = [ 

914 resolve_objective(obj) for obj in objectives.split(",") 

915 ] 

916 return self.__sparkle_objectives 

917 

918 @property 

919 def configurator(self: Settings) -> Configurator: 

920 """Get the configurator class (instance).""" 

921 if self.__general_sparkle_configurator is None and self.__settings.has_option( 

922 Settings.OPTION_configurator.section, Settings.OPTION_configurator.name 

923 ): 

924 # NOTE: Import here for speed up if not using configurator 

925 from sparkle.configurator.implementations import resolve_configurator 

926 

927 self.__general_sparkle_configurator = resolve_configurator( 

928 self.__settings.get( 

929 Settings.OPTION_configurator.section, 

930 Settings.OPTION_configurator.name, 

931 ) 

932 )() 

933 return self.__general_sparkle_configurator 

934 

935 @property 

936 def solver_cutoff_time(self: Settings) -> int: 

937 """Solver cutoff time in seconds.""" 

938 if self.__solver_cutoff_time is None: 

939 self.__solver_cutoff_time = self._abstract_getter( 

940 Settings.OPTION_solver_cutoff_time 

941 ) 

942 return self.__solver_cutoff_time 

943 

944 @property 

945 def extractor_cutoff_time(self: Settings) -> int: 

946 """Extractor cutoff time in seconds.""" 

947 if self.__extractor_cutoff_time is None: 

948 self.__extractor_cutoff_time = self._abstract_getter( 

949 Settings.OPTION_extractor_cutoff_time 

950 ) 

951 return self.__extractor_cutoff_time 

952 

953 @property 

954 def run_on(self: Settings) -> Runner: 

955 """On which compute to run (Local or Slurm).""" 

956 if self.__run_on is None: 

957 self.__run_on = self._abstract_getter(Settings.OPTION_run_on) 

958 return self.__run_on 

959 

960 @property 

961 def appendices(self: Settings) -> bool: 

962 """Whether to include appendices in the report.""" 

963 return self._abstract_getter(Settings.OPTION_appendices) 

964 

965 @property 

966 def verbosity_level(self: Settings) -> VerbosityLevel: 

967 """Verbosity level to use in CLI commands.""" 

968 if self.__verbosity_level is None: 

969 if self.__settings.has_option( 

970 Settings.OPTION_verbosity.section, Settings.OPTION_verbosity.name 

971 ): 

972 self.__verbosity_level = VerbosityLevel[ 

973 self.__settings.get( 

974 Settings.OPTION_verbosity.section, 

975 Settings.OPTION_verbosity.name, 

976 ) 

977 ] 

978 else: 

979 self.__verbosity_level = Settings.OPTION_verbosity.default_value 

980 return self.__verbosity_level 

981 

982 @property 

983 def seed(self: Settings) -> int: 

984 """Seed to use in CLI commands.""" 

985 if self.__seed is not None: 

986 return self.__seed 

987 

988 section, name = Settings.OPTION_seed.section, Settings.OPTION_seed.name 

989 if self.__settings.has_option(section, name): 

990 value = self.__settings.get(section, name) 

991 self.__seed = int(value) 

992 else: 

993 self.__seed = Settings.OPTION_seed.default_value 

994 

995 return self.__seed 

996 

997 @seed.setter 

998 def seed(self: Settings, value: int) -> None: 

999 """Set the seed value (overwrites settings).""" 

1000 self.__seed = value 

1001 self.__settings.set( 

1002 Settings.OPTION_seed.section, Settings.OPTION_seed.name, str(self.__seed) 

1003 ) 

1004 

1005 # Configuration settings ### 

1006 @property 

1007 def configurator_solver_call_budget(self: Settings) -> int: 

1008 """The amount of calls a configurator can do to the solver.""" 

1009 if self.__configurator_solver_call_budget is None: 

1010 self.__configurator_solver_call_budget = self._abstract_getter( 

1011 Settings.OPTION_configurator_solver_call_budget 

1012 ) 

1013 return self.__configurator_solver_call_budget 

1014 

1015 @property 

1016 def configurator_number_of_runs(self: Settings) -> int: 

1017 """Get the amount of configurator runs to do.""" 

1018 if self.__configurator_number_of_runs is None: 

1019 self.__configurator_number_of_runs = self._abstract_getter( 

1020 Settings.OPTION_configurator_number_of_runs 

1021 ) 

1022 return self.__configurator_number_of_runs 

1023 

1024 @property 

1025 def configurator_max_iterations(self: Settings) -> int: 

1026 """Get the amount of configurator iterations to do.""" 

1027 if self.__configurator_max_iterations is None: 

1028 self.__configurator_max_iterations = self._abstract_getter( 

1029 Settings.OPTION_configurator_max_iterations 

1030 ) 

1031 return self.__configurator_max_iterations 

1032 

1033 # Ablation settings ### 

1034 @property 

1035 def ablation_racing_flag(self: Settings) -> bool: 

1036 """Get the ablation racing flag.""" 

1037 if self.__ablation_racing_flag is None: 

1038 self.__ablation_racing_flag = self._abstract_getter( 

1039 Settings.OPTION_ablation_racing 

1040 ) 

1041 return self.__ablation_racing_flag 

1042 

1043 @property 

1044 def ablation_max_parallel_runs_per_node(self: Settings) -> int: 

1045 """Get the ablation max parallel runs per node.""" 

1046 if self.__ablation_max_parallel_runs_per_node is None: 

1047 self.__ablation_max_parallel_runs_per_node = self._abstract_getter( 

1048 Settings.OPTION_ablation_clis_per_node 

1049 ) 

1050 return self.__ablation_max_parallel_runs_per_node 

1051 

1052 # Selection settings ### 

1053 @property 

1054 def selection_model(self: Settings) -> str: 

1055 """Get the selection model.""" 

1056 if self.__selection_model is None: 

1057 self.__selection_model = self._abstract_getter( 

1058 Settings.OPTION_selection_model 

1059 ) 

1060 return self.__selection_model 

1061 

1062 @property 

1063 def selection_class(self: Settings) -> str: 

1064 """Get the selection class.""" 

1065 if self.__selection_class is None: 

1066 self.__selection_class = self._abstract_getter( 

1067 Settings.OPTION_selection_class 

1068 ) 

1069 return self.__selection_class 

1070 

1071 @property 

1072 def minimum_marginal_contribution(self: Settings) -> float: 

1073 """Get the minimum marginal contribution.""" 

1074 if self.__minimum_marginal_contribution is None: 

1075 self.__minimum_marginal_contribution = self._abstract_getter( 

1076 Settings.OPTION_minimum_marginal_contribution 

1077 ) 

1078 return self.__minimum_marginal_contribution 

1079 

1080 # Configuration: SMAC2 specific settings ### 

1081 @property 

1082 def smac2_wallclock_time_budget(self: Settings) -> int: 

1083 """Return the SMAC2 wallclock budget per configuration run in seconds.""" 

1084 if self.__smac2_wallclock_time_budget is None: 

1085 self.__smac2_wallclock_time_budget = self._abstract_getter( 

1086 Settings.OPTION_smac2_wallclock_time_budget 

1087 ) 

1088 return self.__smac2_wallclock_time_budget 

1089 

1090 @property 

1091 def smac2_cpu_time_budget(self: Settings) -> int: 

1092 """Return the SMAC2 CPU budget per configuration run in seconds.""" 

1093 if self.__smac2_cpu_time_budget is None: 

1094 self.__smac2_cpu_time_budget = self._abstract_getter( 

1095 Settings.OPTION_smac2_cpu_time_budget 

1096 ) 

1097 return self.__smac2_cpu_time_budget 

1098 

1099 @property 

1100 def smac2_target_cutoff_length(self: Settings) -> str: 

1101 """Return the SMAC2 target cutoff length.""" 

1102 if self.__smac2_target_cutoff_length is None: 

1103 self.__smac2_target_cutoff_length = self._abstract_getter( 

1104 Settings.OPTION_smac2_target_cutoff_length 

1105 ) 

1106 return self.__smac2_target_cutoff_length 

1107 

1108 @property 

1109 def smac2_use_tunertime_in_cpu_time_budget(self: Settings) -> bool: 

1110 """Return whether SMAC2 time should be used in CPU time budget.""" 

1111 if self.__smac2_use_tunertime_in_cpu_time_budget is None: 

1112 self.__smac2_use_tunertime_in_cpu_time_budget = self._abstract_getter( 

1113 Settings.OPTION_smac2_count_tuner_time 

1114 ) 

1115 return self.__smac2_use_tunertime_in_cpu_time_budget 

1116 

1117 @property 

1118 def smac2_cli_cores(self: Settings) -> int: 

1119 """Return the SMAC2 CLI cores.""" 

1120 if self.__smac2_cli_cores is None: 

1121 self.__smac2_cli_cores = self._abstract_getter( 

1122 Settings.OPTION_smac2_cli_cores 

1123 ) 

1124 return self.__smac2_cli_cores 

1125 

1126 @property 

1127 def smac2_max_iterations(self: Settings) -> int: 

1128 """Return the SMAC2 max iterations.""" 

1129 if self.__smac2_max_iterations is None: 

1130 self.__smac2_max_iterations = self._abstract_getter( 

1131 Settings.OPTION_smac2_max_iterations 

1132 ) 

1133 return self.__smac2_max_iterations 

1134 

1135 # SMAC3 attributes ### 

1136 @property 

1137 def smac3_number_of_trials(self: Settings) -> int: 

1138 """Return the SMAC3 number of trials.""" 

1139 if self.__smac3_number_of_trials is None: 

1140 self.__smac3_number_of_trials = self._abstract_getter( 

1141 Settings.OPTION_smac3_number_of_trials 

1142 ) 

1143 return self.__smac3_number_of_trials 

1144 

1145 @property 

1146 def smac3_facade(self: Settings) -> str: 

1147 """Return the SMAC3 facade.""" 

1148 if self.__smac3_facade is None: 

1149 self.__smac3_facade = self._abstract_getter(Settings.OPTION_smac3_facade) 

1150 return self.__smac3_facade 

1151 

1152 @property 

1153 def smac3_facade_max_ratio(self: Settings) -> float: 

1154 """Return the SMAC3 facade max ratio.""" 

1155 if self.__smac3_facade_max_ratio is None: 

1156 self.__smac3_facade_max_ratio = self._abstract_getter( 

1157 Settings.OPTION_smac3_facade_max_ratio 

1158 ) 

1159 return self.__smac3_facade_max_ratio 

1160 

1161 @property 

1162 def smac3_crash_cost(self: Settings) -> float: 

1163 """Return the SMAC3 crash cost.""" 

1164 if self.__smac3_crash_cost is None: 

1165 self.__smac3_crash_cost = self._abstract_getter( 

1166 Settings.OPTION_smac3_crash_cost 

1167 ) 

1168 return self.__smac3_crash_cost 

1169 

1170 @property 

1171 def smac3_termination_cost_threshold(self: Settings) -> float: 

1172 """Return the SMAC3 termination cost threshold.""" 

1173 if self.__smac3_termination_cost_threshold is None: 

1174 self.__smac3_termination_cost_threshold = self._abstract_getter( 

1175 Settings.OPTION_smac3_termination_cost_threshold 

1176 ) 

1177 return self.__smac3_termination_cost_threshold 

1178 

1179 @property 

1180 def smac3_wallclock_time_budget(self: Settings) -> int: 

1181 """Return the SMAC3 walltime budget in seconds.""" 

1182 if self.__smac3_wallclock_time_limit is None: 

1183 self.__smac3_wallclock_time_limit = self._abstract_getter( 

1184 Settings.OPTION_smac3_wallclock_time_budget 

1185 ) 

1186 return self.__smac3_wallclock_time_limit 

1187 

1188 @property 

1189 def smac3_cpu_time_budget(self: Settings) -> int: 

1190 """Return the SMAC3 cputime budget in seconds.""" 

1191 if self.__smac3_cputime_limit is None: 

1192 self.__smac3_cputime_limit = self._abstract_getter( 

1193 Settings.OPTION_smac3_cpu_time_budget 

1194 ) 

1195 return self.__smac3_cputime_limit 

1196 

1197 @property 

1198 def smac3_use_default_config(self: Settings) -> bool: 

1199 """Return whether SMAC3 should use the default config.""" 

1200 if self.__smac3_use_default_config is None: 

1201 self.__smac3_use_default_config = self._abstract_getter( 

1202 Settings.OPTION_smac3_use_default_config 

1203 ) 

1204 return self.__smac3_use_default_config 

1205 

1206 @property 

1207 def smac3_min_budget(self: Settings) -> int: 

1208 """Return the SMAC3 min budget.""" 

1209 if self.__smac3_min_budget is None: 

1210 self.__smac3_min_budget = self._abstract_getter( 

1211 Settings.OPTION_smac3_min_budget 

1212 ) 

1213 return self.__smac3_min_budget 

1214 

1215 @property 

1216 def smac3_max_budget(self: Settings) -> int: 

1217 """Return the SMAC3 max budget.""" 

1218 if self.__smac3_max_budget is None: 

1219 self.__smac3_max_budget = self._abstract_getter( 

1220 Settings.OPTION_smac3_max_budget 

1221 ) 

1222 return self.__smac3_max_budget 

1223 

1224 # IRACE settings ### 

1225 @property 

1226 def irace_max_time(self: Settings) -> int: 

1227 """Return the max time in seconds for IRACE.""" 

1228 if self.__irace_max_time is None: 

1229 self.__irace_max_time = self._abstract_getter(Settings.OPTION_irace_max_time) 

1230 return self.__irace_max_time 

1231 

1232 @property 

1233 def irace_max_experiments(self: Settings) -> int: 

1234 """Return the max experiments for IRACE.""" 

1235 if self.__irace_max_experiments is None: 

1236 self.__irace_max_experiments = self._abstract_getter( 

1237 Settings.OPTION_irace_max_experiments 

1238 ) 

1239 return self.__irace_max_experiments 

1240 

1241 @property 

1242 def irace_first_test(self: Settings) -> int: 

1243 """Return the first test for IRACE.""" 

1244 if self.__irace_first_test is None: 

1245 self.__irace_first_test = self._abstract_getter( 

1246 Settings.OPTION_irace_first_test 

1247 ) 

1248 return self.__irace_first_test 

1249 

1250 @property 

1251 def irace_mu(self: Settings) -> int: 

1252 """Return the mu for IRACE.""" 

1253 if self.__irace_mu is None: 

1254 self.__irace_mu = self._abstract_getter(Settings.OPTION_irace_mu) 

1255 return self.__irace_mu 

1256 

1257 @property 

1258 def irace_max_iterations(self: Settings) -> int: 

1259 """Return the max iterations for IRACE.""" 

1260 if self.__irace_max_iterations is None: 

1261 self.__irace_max_iterations = self._abstract_getter( 

1262 Settings.OPTION_irace_max_iterations 

1263 ) 

1264 return self.__irace_max_iterations 

1265 

1266 # ParamILS settings ### 

1267 @property 

1268 def paramils_cpu_time_budget(self: Settings) -> int: 

1269 """Return the CPU time budget for ParamILS.""" 

1270 if self.__paramils_cpu_time_budget is None: 

1271 self.__paramils_cpu_time_budget = self._abstract_getter( 

1272 Settings.OPTION_paramils_cpu_time_budget 

1273 ) 

1274 return self.__paramils_cpu_time_budget 

1275 

1276 @property 

1277 def paramils_min_runs(self: Settings) -> int: 

1278 """Return the min runs for ParamILS.""" 

1279 if self.__paramils_min_runs is None: 

1280 self.__paramils_min_runs = self._abstract_getter( 

1281 Settings.OPTION_paramils_min_runs 

1282 ) 

1283 return self.__paramils_min_runs 

1284 

1285 @property 

1286 def paramils_max_runs(self: Settings) -> int: 

1287 """Return the max runs for ParamILS.""" 

1288 if self.__paramils_max_runs is None: 

1289 self.__paramils_max_runs = self._abstract_getter( 

1290 Settings.OPTION_paramils_max_runs 

1291 ) 

1292 return self.__paramils_max_runs 

1293 

1294 @property 

1295 def paramils_random_restart(self: Settings) -> float: 

1296 """Return the random restart for ParamILS.""" 

1297 if self.__paramils_random_restart is None: 

1298 self.__paramils_random_restart = self._abstract_getter( 

1299 Settings.OPTION_paramils_random_restart 

1300 ) 

1301 return self.__paramils_random_restart 

1302 

1303 @property 

1304 def paramils_focused_approach(self: Settings) -> bool: 

1305 """Return the focused approach for ParamILS.""" 

1306 if self.__paramils_focused_approach is None: 

1307 self.__paramils_focused_approach = self._abstract_getter( 

1308 Settings.OPTION_paramils_focused 

1309 ) 

1310 return self.__paramils_focused_approach 

1311 

1312 @property 

1313 def paramils_use_cpu_time_in_tunertime(self: Settings) -> bool: 

1314 """Return the use cpu time for ParamILS.""" 

1315 if self.__paramils_use_cpu_time_in_tunertime is None: 

1316 self.__paramils_use_cpu_time_in_tunertime = self._abstract_getter( 

1317 Settings.OPTION_paramils_count_tuner_time 

1318 ) 

1319 return self.__paramils_use_cpu_time_in_tunertime 

1320 

1321 @property 

1322 def paramils_cli_cores(self: Settings) -> int: 

1323 """The number of CPU cores to use for ParamILS.""" 

1324 if self.__paramils_cli_cores is None: 

1325 self.__paramils_cli_cores = self._abstract_getter( 

1326 Settings.OPTION_paramils_cli_cores 

1327 ) 

1328 return self.__paramils_cli_cores 

1329 

1330 @property 

1331 def paramils_max_iterations(self: Settings) -> int: 

1332 """Return the max iterations for ParamILS.""" 

1333 if self.__paramils_max_iterations is None: 

1334 self.__paramils_max_iterations = self._abstract_getter( 

1335 Settings.OPTION_paramils_max_iterations 

1336 ) 

1337 return self.__paramils_max_iterations 

1338 

1339 @property 

1340 def paramils_number_initial_configurations(self: Settings) -> int: 

1341 """Return the number of initial configurations for ParamILS.""" 

1342 if self.__paramils_number_initial_configurations is None: 

1343 self.__paramils_number_initial_configurations = self._abstract_getter( 

1344 Settings.OPTION_paramils_number_initial_configurations 

1345 ) 

1346 return self.__paramils_number_initial_configurations 

1347 

1348 # Parallel Portfolio settings ### 

1349 @property 

1350 def parallel_portfolio_check_interval(self: Settings) -> int: 

1351 """Return the check interval for the parallel portfolio.""" 

1352 if self.__parallel_portfolio_check_interval is None: 

1353 self.__parallel_portfolio_check_interval = self._abstract_getter( 

1354 Settings.OPTION_parallel_portfolio_check_interval 

1355 ) 

1356 return self.__parallel_portfolio_check_interval 

1357 

1358 @property 

1359 def parallel_portfolio_num_seeds_per_solver(self: Settings) -> int: 

1360 """Return the number of seeds per solver for the parallel portfolio.""" 

1361 if self.__parallel_portfolio_num_seeds_per_solver is None: 

1362 self.__parallel_portfolio_num_seeds_per_solver = self._abstract_getter( 

1363 Settings.OPTION_parallel_portfolio_number_of_seeds_per_solver 

1364 ) 

1365 return self.__parallel_portfolio_num_seeds_per_solver 

1366 

1367 # Slurm settings ### 

1368 @property 

1369 def slurm_jobs_in_parallel(self: Settings) -> int: 

1370 """Return the (maximum) number of jobs to run in parallel.""" 

1371 if self.__slurm_jobs_in_parallel is None: 

1372 self.__slurm_jobs_in_parallel = self._abstract_getter( 

1373 Settings.OPTION_slurm_parallel_jobs 

1374 ) 

1375 return self.__slurm_jobs_in_parallel 

1376 

1377 @property 

1378 def slurm_job_prepend(self: Settings) -> str: 

1379 """Return the slurm job prepend.""" 

1380 if self.__slurm_job_prepend is None and self.__settings.has_option( 

1381 Settings.OPTION_slurm_prepend_script.section, 

1382 Settings.OPTION_slurm_prepend_script.name, 

1383 ): 

1384 value = self.__settings[Settings.OPTION_slurm_prepend_script.section][ 

1385 Settings.OPTION_slurm_prepend_script.name 

1386 ] 

1387 try: 

1388 path = Path(value) 

1389 if path.is_file(): 

1390 with path.open() as f: 

1391 value = f.read() 

1392 f.close() 

1393 self.__slurm_job_prepend = str(value) 

1394 except TypeError: 

1395 pass 

1396 return self.__slurm_job_prepend 

1397 

1398 @property 

1399 def sbatch_settings(self: Settings) -> list[str]: 

1400 """Return the sbatch settings.""" 

1401 sbatch_options = self.__settings[Settings.SECTION_slurm] 

1402 # Return all non-predefined keys 

1403 return [ 

1404 f"--{key}={sbatch_options[key]}" 

1405 for key in sbatch_options.keys() 

1406 if key not in Settings.sections_options[Settings.SECTION_slurm] 

1407 ] 

1408 

1409 # General functionalities ### 

1410 

1411 def get_configurator_output_path(self: Settings, configurator: Configurator) -> Path: 

1412 """Return the configurator output path.""" 

1413 return self.DEFAULT_configuration_output / configurator.name 

1414 

1415 def get_configurator_settings( 

1416 self: Settings, configurator_name: str 

1417 ) -> dict[str, any]: 

1418 """Return the settings of a specific configurator.""" 

1419 configurator_settings = { 

1420 "solver_calls": self.configurator_solver_call_budget, 

1421 "solver_cutoff_time": self.solver_cutoff_time, 

1422 "max_iterations": self.configurator_max_iterations, 

1423 } 

1424 # In the settings below, we default to the configurator general settings if no 

1425 # specific configurator settings are given, by using the [None] or [Value] 

1426 if ( 

1427 configurator_name == "SMAC2" 

1428 ): # NOTE: This is hardcoded, but doing it through imports slows done the ENTIRETY of the Sparkle substantially 

1429 # Return all settings from the SMAC2 section 

1430 configurator_settings.update( 

1431 { 

1432 "cpu_time": self.smac2_cpu_time_budget, 

1433 "wallclock_time": self.smac2_wallclock_time_budget, 

1434 "target_cutoff_length": self.smac2_target_cutoff_length, 

1435 "use_cpu_time_in_tunertime": self.smac2_use_tunertime_in_cpu_time_budget, 

1436 "cli_cores": self.smac2_cli_cores, 

1437 "max_iterations": self.smac2_max_iterations 

1438 or configurator_settings["max_iterations"], 

1439 } 

1440 ) 

1441 elif ( 

1442 configurator_name == "SMAC3" 

1443 ): # NOTE: This is hardcoded, but doing it through imports slows done the ENTIRETY of the Sparkle substantially 

1444 # Return all settings from the SMAC3 section 

1445 del configurator_settings["max_iterations"] # SMAC3 does not have this? 

1446 configurator_settings.update( 

1447 { 

1448 "smac_facade": self.smac3_facade, 

1449 "max_ratio": self.smac3_facade_max_ratio, 

1450 "crash_cost": self.smac3_crash_cost, 

1451 "termination_cost_threshold": self.smac3_termination_cost_threshold, 

1452 "walltime_limit": self.smac3_wallclock_time_budget, 

1453 "cputime_limit": self.smac3_cpu_time_budget, 

1454 "use_default_config": self.smac3_use_default_config, 

1455 "min_budget": self.smac3_min_budget, 

1456 "max_budget": self.smac3_max_budget, 

1457 "solver_calls": self.smac3_number_of_trials 

1458 or configurator_settings["solver_calls"], 

1459 } 

1460 ) 

1461 # Do not pass None values to SMAC3, its Scenario resolves default settings 

1462 configurator_settings = { 

1463 key: value 

1464 for key, value in configurator_settings.items() 

1465 if value is not None 

1466 } 

1467 elif ( 

1468 configurator_name == "IRACE" 

1469 ): # NOTE: This is hardcoded, but doing it through imports slows done the ENTIRETY of the Sparkle substantially 

1470 # Return all settings from the IRACE section 

1471 configurator_settings.update( 

1472 { 

1473 "solver_calls": self.irace_max_experiments, 

1474 "max_time": self.irace_max_time, 

1475 "first_test": self.irace_first_test, 

1476 "mu": self.irace_mu, 

1477 "max_iterations": self.irace_max_iterations 

1478 or configurator_settings["max_iterations"], 

1479 } 

1480 ) 

1481 if ( 

1482 configurator_settings["solver_calls"] == 0 

1483 and configurator_settings["max_time"] == 0 

1484 ): # Default to base 

1485 configurator_settings["solver_calls"] = ( 

1486 self.configurator_solver_call_budget 

1487 ) 

1488 elif ( 

1489 configurator_name == "ParamILS" 

1490 ): # NOTE: This is hardcoded, but doing it through imports slows done the ENTIRETY of the Sparkle substantially 

1491 configurator_settings.update( 

1492 { 

1493 "tuner_timeout": self.paramils_cpu_time_budget, 

1494 "min_runs": self.paramils_min_runs, 

1495 "max_runs": self.paramils_max_runs, 

1496 "focused_ils": self.paramils_focused_approach, 

1497 "initial_configurations": self.paramils_number_initial_configurations, 

1498 "random_restart": self.paramils_random_restart, 

1499 "cli_cores": self.paramils_cli_cores, 

1500 "use_cpu_time_in_tunertime": self.paramils_use_cpu_time_in_tunertime, 

1501 "max_iterations": self.paramils_max_iterations 

1502 or configurator_settings["max_iterations"], 

1503 } 

1504 ) 

1505 return configurator_settings 

1506 

1507 @staticmethod 

1508 def check_settings_changes( 

1509 cur_settings: Settings, prev_settings: Settings, verbose: bool = True 

1510 ) -> bool: 

1511 """Check if there are changes between the previous and the current settings. 

1512 

1513 Prints any section changes, printing None if no setting was found. 

1514 

1515 Args: 

1516 cur_settings: The current settings 

1517 prev_settings: The previous settings 

1518 verbose: Verbosity of the function 

1519 

1520 Returns: 

1521 True iff there are changes. 

1522 """ 

1523 cur_dict = cur_settings.__settings._sections 

1524 prev_dict = prev_settings.__settings._sections 

1525 

1526 cur_sections_set = set(cur_dict.keys()) 

1527 prev_sections_set = set(prev_dict.keys()) 

1528 

1529 sections_remained = cur_sections_set & prev_sections_set 

1530 option_changed = False 

1531 for section in sections_remained: 

1532 printed_section = False 

1533 names = set(cur_dict[section].keys()) | set(prev_dict[section].keys()) 

1534 if ( 

1535 section == "general" and "seed" in names 

1536 ): # Do not report on the seed, is supposed to change 

1537 names.remove("seed") 

1538 for name in names: 

1539 # if name is not present in one of the two dicts, get None as placeholder 

1540 cur_val = cur_dict[section].get(name, None) 

1541 prev_val = prev_dict[section].get(name, None) 

1542 

1543 # If cur val is None, it is default 

1544 if cur_val is not None and cur_val != prev_val: 

1545 if not option_changed and verbose: # Print the initial 

1546 print("[INFO] The following attributes/options have changed:") 

1547 option_changed = True 

1548 

1549 # do we have yet to print the section? 

1550 if not printed_section and verbose: 

1551 print(f" - In the section '{section}':") 

1552 printed_section = True 

1553 

1554 # print actual change 

1555 if verbose: 

1556 print(f" · '{name}' changed from '{prev_val}' to '{cur_val}'") 

1557 

1558 return option_changed