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

589 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-08 12:00 +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", argparse.BooleanOptionalAction): 

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_groupwise_computation = Option( 

198 "groupwise_computation", 

199 SECTION_general, 

200 bool, 

201 True, 

202 tuple(), 

203 "Run extractors per feature group (use --no-groupwise-computation to disable).", 

204 cli_kwargs={ 

205 "action": argparse.BooleanOptionalAction, 

206 "default": None, 

207 }, 

208 ) 

209 OPTION_run_on = Option( 

210 "run_on", 

211 SECTION_general, 

212 Runner, 

213 None, 

214 tuple(), 

215 "On which compute resource to execute.", 

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

217 ) 

218 OPTION_verbosity = Option( 

219 "verbosity", 

220 SECTION_general, 

221 VerbosityLevel, 

222 VerbosityLevel.STANDARD, 

223 ("verbosity_level",), 

224 "Verbosity level.", 

225 ) 

226 OPTION_seed = Option( 

227 "seed", 

228 SECTION_general, 

229 int, 

230 None, 

231 tuple(), 

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

233 ) 

234 OPTION_appendices = Option( 

235 "appendices", 

236 SECTION_general, 

237 bool, 

238 False, 

239 tuple(), 

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

241 cli_kwargs={ 

242 "action": "store_true", 

243 "default": None, 

244 }, 

245 ) 

246 

247 # CONFIGURATION Options 

248 SECTION_configuration = "configuration" 

249 OPTION_configurator_number_of_runs = Option( 

250 "number_of_runs", 

251 SECTION_configuration, 

252 int, 

253 None, 

254 tuple(), 

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

256 ) 

257 OPTION_configurator_solver_call_budget = Option( 

258 "solver_calls", 

259 SECTION_configuration, 

260 int, 

261 None, 

262 tuple(), 

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

264 "run of the solver.", 

265 ) 

266 OPTION_configurator_max_iterations = Option( 

267 "max_iterations", 

268 SECTION_configuration, 

269 int, 

270 None, 

271 ("maximum_iterations",), 

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

273 ) 

274 

275 # ABLATION Options 

276 SECTION_ablation = "ablation" 

277 OPTION_ablation_racing = Option( 

278 "racing", 

279 SECTION_ablation, 

280 bool, 

281 False, 

282 ("ablation_racing",), 

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

284 ) 

285 OPTION_ablation_clis_per_node = Option( 

286 "clis_per_node", 

287 SECTION_ablation, 

288 int, 

289 None, 

290 ( 

291 "max_parallel_runs_per_node", 

292 "maximum_parallel_runs_per_node", 

293 ), 

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

295 "node.", 

296 ) 

297 

298 # SELECTION Options 

299 SECTION_selection = "selection" 

300 OPTION_selection_class = Option( 

301 "selector_class", 

302 SECTION_selection, 

303 str, 

304 None, 

305 ("class",), 

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

307 ) 

308 OPTION_selection_model = Option( 

309 "selector_model", 

310 SECTION_selection, 

311 str, 

312 None, 

313 ("model",), 

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

315 ) 

316 OPTION_minimum_marginal_contribution = Option( 

317 "minimum_marginal_contribution", 

318 SECTION_selection, 

319 float, 

320 0.01, 

321 ( 

322 "minimum_marginal_contribution", 

323 "minimum_contribution", 

324 "contribution_threshold", 

325 ), 

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

327 ) 

328 

329 # SMAC2 Options 

330 SECTION_smac2 = "smac2" 

331 OPTION_smac2_wallclock_time_budget = Option( 

332 "wallclock_time_budget", 

333 SECTION_smac2, 

334 int, 

335 None, 

336 ("wallclock_time",), 

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

338 ) 

339 OPTION_smac2_cpu_time_budget = Option( 

340 "cpu_time_budget", 

341 SECTION_smac2, 

342 int, 

343 None, 

344 ("cpu_time",), 

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

346 ) 

347 OPTION_smac2_target_cutoff_length = Option( 

348 "target_cutoff_length", 

349 SECTION_smac2, 

350 str, 

351 None, 

352 ("cutoff_length", "solver_cutoff_length"), 

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

354 ) 

355 OPTION_smac2_count_tuner_time = Option( 

356 "use_cpu_time_in_tunertime", 

357 SECTION_smac2, 

358 bool, 

359 None, 

360 ("countSMACTimeAsTunerTime",), 

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

362 ) 

363 OPTION_smac2_cli_cores = Option( 

364 "cli_cores", 

365 SECTION_smac2, 

366 int, 

367 None, 

368 tuple(), 

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

370 ) 

371 OPTION_smac2_max_iterations = Option( 

372 "max_iterations", 

373 SECTION_smac2, 

374 int, 

375 None, 

376 ( 

377 "iteration_limit", 

378 "numIterations", 

379 "numberOfIterations", 

380 ), 

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

382 ) 

383 

384 # SMAC3 Options 

385 SECTION_smac3 = "smac3" 

386 OPTION_smac3_number_of_trials = Option( 

387 "n_trials", 

388 SECTION_smac3, 

389 int, 

390 None, 

391 ("n_trials", "number_of_trials", "solver_calls"), 

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

393 ) 

394 OPTION_smac3_facade = Option( 

395 "facade", 

396 SECTION_smac3, 

397 str, 

398 "AlgorithmConfigurationFacade", 

399 ("facade", "smac_facade", "smac3_facade"), 

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

401 ) 

402 OPTION_smac3_facade_max_ratio = Option( 

403 "facade_max_ratio", 

404 SECTION_smac3, 

405 float, 

406 None, 

407 ("facade_max_ratio", "smac3_facade_max_ratio", "smac3_facade_max_ratio"), 

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

409 ) 

410 OPTION_smac3_crash_cost = Option( 

411 "crash_cost", 

412 SECTION_smac3, 

413 float, 

414 None, 

415 tuple(), 

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

417 ) 

418 OPTION_smac3_termination_cost_threshold = Option( 

419 "termination_cost_threshold", 

420 SECTION_smac3, 

421 float, 

422 None, 

423 tuple(), 

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

425 ) 

426 OPTION_smac3_wallclock_time_budget = Option( 

427 "walltime_limit", 

428 SECTION_smac3, 

429 float, 

430 None, 

431 ("wallclock_time", "wallclock_budget", "wallclock_time_budget"), 

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

433 ) 

434 OPTION_smac3_cpu_time_budget = Option( 

435 "cputime_limit", 

436 SECTION_smac3, 

437 float, 

438 None, 

439 ("cpu_time", "cpu_budget", "cpu_time_budget"), 

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

441 ) 

442 OPTION_smac3_use_default_config = Option( 

443 "use_default_config", 

444 SECTION_smac3, 

445 bool, 

446 None, 

447 tuple(), 

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

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

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

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

452 ) 

453 OPTION_smac3_min_budget = Option( 

454 "min_budget", 

455 SECTION_smac3, 

456 float, 

457 None, 

458 ("minimum_budget",), 

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

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

461 "optimization.", 

462 ) 

463 OPTION_smac3_max_budget = Option( 

464 "max_budget", 

465 SECTION_smac3, 

466 float, 

467 None, 

468 ("maximum_budget",), 

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

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

471 "optimization.", 

472 ) 

473 

474 # IRACE Options 

475 SECTION_irace = "irace" 

476 OPTION_irace_max_time = Option( 

477 "max_time", 

478 SECTION_irace, 

479 int, 

480 0, 

481 ("maximum_time",), 

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

483 ) 

484 OPTION_irace_max_experiments = Option( 

485 "max_experiments", 

486 SECTION_irace, 

487 int, 

488 0, 

489 ("maximum_experiments",), 

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

491 ) 

492 OPTION_irace_first_test = Option( 

493 "first_test", 

494 SECTION_irace, 

495 int, 

496 None, 

497 tuple(), 

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

499 "IRACE Default: 5.", 

500 ) 

501 OPTION_irace_mu = Option( 

502 "mu", 

503 SECTION_irace, 

504 int, 

505 None, 

506 tuple(), 

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

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

509 ) 

510 OPTION_irace_max_iterations = Option( 

511 "max_iterations", 

512 SECTION_irace, 

513 int, 

514 None, 

515 ("nb_iterations", "iterations", "max_iterations"), 

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

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

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

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

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

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

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

523 ) 

524 

525 # ParamILS Options 

526 SECTION_paramils = "paramils" 

527 OPTION_paramils_min_runs = Option( 

528 "min_runs", 

529 SECTION_paramils, 

530 int, 

531 None, 

532 ("minimum_runs",), 

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

534 ) 

535 OPTION_paramils_max_runs = Option( 

536 "max_runs", 

537 SECTION_paramils, 

538 int, 

539 None, 

540 ("maximum_runs",), 

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

542 ) 

543 OPTION_paramils_cpu_time_budget = Option( 

544 "cputime_budget", 

545 SECTION_paramils, 

546 int, 

547 None, 

548 ( 

549 "cputime_limit", 

550 "cputime_limit", 

551 "tunertime_limit", 

552 "tuner_timeout", 

553 "tunerTimeout", 

554 ), 

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

556 ) 

557 OPTION_paramils_random_restart = Option( 

558 "random_restart", 

559 SECTION_paramils, 

560 float, 

561 None, 

562 tuple(), 

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

564 ) 

565 OPTION_paramils_focused = Option( 

566 "focused_approach", 

567 SECTION_paramils, 

568 bool, 

569 False, 

570 ("focused",), 

571 "Set the focused approach for ParamILS.", 

572 ) 

573 OPTION_paramils_count_tuner_time = Option( 

574 "use_cpu_time_in_tunertime", 

575 SECTION_paramils, 

576 bool, 

577 None, 

578 tuple(), 

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

580 ) 

581 OPTION_paramils_cli_cores = Option( 

582 "cli_cores", 

583 SECTION_paramils, 

584 int, 

585 None, 

586 tuple(), 

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

588 ) 

589 OPTION_paramils_max_iterations = Option( 

590 "max_iterations", 

591 SECTION_paramils, 

592 int, 

593 None, 

594 ( 

595 "iteration_limit", 

596 "numIterations", 

597 "numberOfIterations", 

598 "maximum_iterations", 

599 ), 

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

601 ) 

602 OPTION_paramils_number_initial_configurations = Option( 

603 "initial_configurations", 

604 SECTION_paramils, 

605 int, 

606 None, 

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

608 ) 

609 

610 SECTION_parallel_portfolio = "parallel_portfolio" 

611 OPTION_parallel_portfolio_check_interval = Option( 

612 "check_interval", 

613 SECTION_parallel_portfolio, 

614 int, 

615 None, 

616 tuple(), 

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

618 ) 

619 OPTION_parallel_portfolio_number_of_seeds_per_solver = Option( 

620 "num_seeds_per_solver", 

621 SECTION_parallel_portfolio, 

622 int, 

623 None, 

624 ("solver_seeds",), 

625 "The number of seeds per solver.", 

626 ) 

627 

628 SECTION_slurm = "slurm" 

629 OPTION_slurm_parallel_jobs = Option( 

630 "number_of_jobs_in_parallel", 

631 SECTION_slurm, 

632 int, 

633 None, 

634 ("num_job_in_parallel",), 

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

636 ) 

637 OPTION_slurm_prepend_script = Option( 

638 "prepend_script", 

639 SECTION_slurm, 

640 str, 

641 None, 

642 ("job_prepend", "prepend"), 

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

644 ) 

645 

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

647 SECTION_general: [ 

648 OPTION_objectives, 

649 OPTION_configurator, 

650 OPTION_solver_cutoff_time, 

651 OPTION_extractor_cutoff_time, 

652 OPTION_groupwise_computation, 

653 OPTION_run_on, 

654 OPTION_appendices, 

655 OPTION_verbosity, 

656 OPTION_seed, 

657 ], 

658 SECTION_configuration: [ 

659 OPTION_configurator_number_of_runs, 

660 OPTION_configurator_solver_call_budget, 

661 OPTION_configurator_max_iterations, 

662 ], 

663 SECTION_ablation: [ 

664 OPTION_ablation_racing, 

665 OPTION_ablation_clis_per_node, 

666 ], 

667 SECTION_selection: [ 

668 OPTION_selection_class, 

669 OPTION_selection_model, 

670 OPTION_minimum_marginal_contribution, 

671 ], 

672 SECTION_smac2: [ 

673 OPTION_smac2_wallclock_time_budget, 

674 OPTION_smac2_cpu_time_budget, 

675 OPTION_smac2_target_cutoff_length, 

676 OPTION_smac2_count_tuner_time, 

677 OPTION_smac2_cli_cores, 

678 OPTION_smac2_max_iterations, 

679 ], 

680 SECTION_smac3: [ 

681 OPTION_smac3_number_of_trials, 

682 OPTION_smac3_facade, 

683 OPTION_smac3_facade_max_ratio, 

684 OPTION_smac3_crash_cost, 

685 OPTION_smac3_termination_cost_threshold, 

686 OPTION_smac3_wallclock_time_budget, 

687 OPTION_smac3_cpu_time_budget, 

688 OPTION_smac3_use_default_config, 

689 OPTION_smac3_min_budget, 

690 OPTION_smac3_max_budget, 

691 ], 

692 SECTION_irace: [ 

693 OPTION_irace_max_time, 

694 OPTION_irace_max_experiments, 

695 OPTION_irace_first_test, 

696 OPTION_irace_mu, 

697 OPTION_irace_max_iterations, 

698 ], 

699 SECTION_paramils: [ 

700 OPTION_paramils_min_runs, 

701 OPTION_paramils_max_runs, 

702 OPTION_paramils_cpu_time_budget, 

703 OPTION_paramils_random_restart, 

704 OPTION_paramils_focused, 

705 OPTION_paramils_count_tuner_time, 

706 OPTION_paramils_cli_cores, 

707 OPTION_paramils_max_iterations, 

708 OPTION_paramils_number_initial_configurations, 

709 ], 

710 SECTION_parallel_portfolio: [ 

711 OPTION_parallel_portfolio_check_interval, 

712 OPTION_parallel_portfolio_number_of_seeds_per_solver, 

713 ], 

714 SECTION_slurm: [OPTION_slurm_parallel_jobs, OPTION_slurm_prepend_script], 

715 } 

716 

717 def __init__( 

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

719 ) -> None: 

720 """Initialise a settings object. 

721 

722 Args: 

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

724 argsv: The CLI arguments to process. 

725 """ 

726 # Settings 'dictionary' in configparser format 

727 self.__settings = configparser.ConfigParser() 

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

729 self.__settings.add_section(section) 

730 self.__settings[section] = {} 

731 

732 # General attributes 

733 self.__sparkle_objectives: list[SparkleObjective] = None 

734 self.__general_sparkle_configurator: Configurator = None 

735 self.__solver_cutoff_time: int = None 

736 self.__extractor_cutoff_time: int = None 

737 self.__groupwise_computation: bool = None 

738 self.__run_on: Runner = None 

739 self.__appendices: bool = False 

740 self.__verbosity_level: VerbosityLevel = None 

741 self.__seed: Optional[int] = None 

742 

743 # Configuration attributes 

744 self.__configurator_solver_call_budget: int = None 

745 self.__configurator_number_of_runs: int = None 

746 self.__configurator_max_iterations: int = None 

747 

748 # Ablation attributes 

749 self.__ablation_racing_flag: bool = None 

750 self.__ablation_max_parallel_runs_per_node: int = None 

751 

752 # Selection attributes 

753 self.__selection_model: str = None 

754 self.__selection_class: str = None 

755 self.__minimum_marginal_contribution: float = None 

756 

757 # SMAC2 attributes 

758 self.__smac2_wallclock_time_budget: int = None 

759 self.__smac2_cpu_time_budget: int = None 

760 self.__smac2_target_cutoff_length: str = None 

761 self.__smac2_use_tunertime_in_cpu_time_budget: bool = None 

762 self.__smac2_cli_cores: int = None 

763 self.__smac2_max_iterations: int = None 

764 

765 # SMAC3 attributes 

766 self.__smac3_number_of_trials: int = None 

767 self.__smac3_facade: str = None 

768 self.__smac3_facade_max_ratio: float = None 

769 self.__smac3_crash_cost: float = None 

770 self.__smac3_termination_cost_threshold: float = None 

771 self.__smac3_wallclock_time_limit: int = None 

772 self.__smac3_cputime_limit: int = None 

773 self.__smac3_use_default_config: bool = None 

774 self.__smac3_min_budget: float = None 

775 self.__smac3_max_budget: float = None 

776 

777 # IRACE attributes 

778 self.__irace_max_time: int = None 

779 self.__irace_max_experiments: int = None 

780 self.__irace_first_test: int = None 

781 self.__irace_mu: int = None 

782 self.__irace_max_iterations: int = None 

783 

784 # ParamILS attributes 

785 self.__paramils_cpu_time_budget: int = None 

786 self.__paramils_min_runs: int = None 

787 self.__paramils_max_runs: int = None 

788 self.__paramils_random_restart: float = None 

789 self.__paramils_focused_approach: bool = None 

790 self.__paramils_use_cpu_time_in_tunertime: bool = None 

791 self.__paramils_cli_cores: int = None 

792 self.__paramils_max_iterations: int = None 

793 self.__paramils_number_initial_configurations: int = None 

794 

795 # Parallel portfolio attributes 

796 self.__parallel_portfolio_check_interval: int = None 

797 self.__parallel_portfolio_num_seeds_per_solver: int = None 

798 

799 # Slurm attributes 

800 self.__slurm_jobs_in_parallel: int = None 

801 self.__slurm_job_prepend: str = None 

802 

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

804 self.random_state: Optional[int] = None 

805 

806 if file_path and file_path.exists(): 

807 self.read_settings_ini(file_path) 

808 

809 if argsv: 

810 self.apply_arguments(argsv) 

811 

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

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

814 if not file_path.exists(): 

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

816 # Read file 

817 file_settings = configparser.ConfigParser() 

818 file_settings.read(file_path) 

819 

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

821 # successfully 

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

823 # Print error if unable to read the settings 

824 print( 

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

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

827 ) 

828 return 

829 

830 for section in file_settings.sections(): 

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

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

833 continue 

834 for option_name in file_settings.options(section): 

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

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

837 self.__settings.set( 

838 section, 

839 option_name, 

840 file_settings.get(section, option_name), 

841 ) 

842 else: 

843 print( 

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

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

846 ) 

847 continue 

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

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

850 self.__settings.set( 

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

852 ) 

853 del file_settings 

854 

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

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

857 # Create needed directories if they don't exist 

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

859 # We don't write empty sections 

860 for section in self.__settings.sections(): 

861 if not self.__settings[section]: 

862 self.__settings.remove_section(section) 

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

864 self.__settings.write(fout) 

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

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

867 self.__settings.add_section(section) 

868 

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

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

871 # Write to latest settings file 

872 self.write_settings_ini(self.DEFAULT_previous_settings_path) 

873 

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

875 """Apply the arguments to the settings.""" 

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

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

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

879 self.read_settings_ini(argsv.settings_file) 

880 # Match all possible arguments to the settings 

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

882 value = argsv.__dict__[argument] 

883 if value is None: 

884 continue # Skip None 

885 if isinstance(value, Enum): 

886 value = value.name 

887 elif isinstance(value, list): 

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

889 else: 

890 value = str(value) 

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

892 if argument in self.sections_options[section]: 

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

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

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

896 break 

897 

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

899 """Abstract getter method.""" 

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

901 if option.type is bool: 

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

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

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

905 if issubclass(option.type, Enum): 

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

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

908 return value 

909 return option.default_value 

910 

911 # General settings ### 

912 @property 

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

914 """Get the objectives for Sparkle.""" 

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

916 Settings.SECTION_general, "objectives" 

917 ): 

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

919 if "status:metric" not in objectives: 

920 objectives += ",status:metric" 

921 if "cpu_time:metric" not in objectives: 

922 objectives += ",cpu_time:metric" 

923 if "wall_time:metric" not in objectives: 

924 objectives += ",wall_time:metric" 

925 if "memory:metric" not in objectives: 

926 objectives += ",memory:metric" 

927 self.__sparkle_objectives = [ 

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

929 ] 

930 return self.__sparkle_objectives 

931 

932 @property 

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

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

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

936 Settings.OPTION_configurator.section, Settings.OPTION_configurator.name 

937 ): 

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

939 from sparkle.configurator.implementations import resolve_configurator 

940 

941 self.__general_sparkle_configurator = resolve_configurator( 

942 self.__settings.get( 

943 Settings.OPTION_configurator.section, 

944 Settings.OPTION_configurator.name, 

945 ) 

946 )() 

947 return self.__general_sparkle_configurator 

948 

949 @property 

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

951 """Solver cutoff time in seconds.""" 

952 if self.__solver_cutoff_time is None: 

953 self.__solver_cutoff_time = self._abstract_getter( 

954 Settings.OPTION_solver_cutoff_time 

955 ) 

956 return self.__solver_cutoff_time 

957 

958 @property 

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

960 """Extractor cutoff time in seconds.""" 

961 if self.__extractor_cutoff_time is None: 

962 self.__extractor_cutoff_time = self._abstract_getter( 

963 Settings.OPTION_extractor_cutoff_time 

964 ) 

965 return self.__extractor_cutoff_time 

966 

967 @property 

968 def groupwise_computation(self: Settings) -> bool: 

969 """Whether to run extractors per feature group.""" 

970 if self.__groupwise_computation is None: 

971 self.__groupwise_computation = self._abstract_getter( 

972 Settings.OPTION_groupwise_computation 

973 ) 

974 return self.__groupwise_computation 

975 

976 @property 

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

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

979 if self.__run_on is None: 

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

981 return self.__run_on 

982 

983 @property 

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

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

986 return self._abstract_getter(Settings.OPTION_appendices) 

987 

988 @property 

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

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

991 if self.__verbosity_level is None: 

992 if self.__settings.has_option( 

993 Settings.OPTION_verbosity.section, Settings.OPTION_verbosity.name 

994 ): 

995 self.__verbosity_level = VerbosityLevel[ 

996 self.__settings.get( 

997 Settings.OPTION_verbosity.section, 

998 Settings.OPTION_verbosity.name, 

999 ) 

1000 ] 

1001 else: 

1002 self.__verbosity_level = Settings.OPTION_verbosity.default_value 

1003 return self.__verbosity_level 

1004 

1005 @property 

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

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

1008 if self.__seed is not None: 

1009 return self.__seed 

1010 

1011 section, name = Settings.OPTION_seed.section, Settings.OPTION_seed.name 

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

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

1014 self.__seed = int(value) 

1015 else: 

1016 self.__seed = Settings.OPTION_seed.default_value 

1017 

1018 return self.__seed 

1019 

1020 @seed.setter 

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

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

1023 self.__seed = value 

1024 self.__settings.set( 

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

1026 ) 

1027 

1028 # Configuration settings ### 

1029 @property 

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

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

1032 if self.__configurator_solver_call_budget is None: 

1033 self.__configurator_solver_call_budget = self._abstract_getter( 

1034 Settings.OPTION_configurator_solver_call_budget 

1035 ) 

1036 return self.__configurator_solver_call_budget 

1037 

1038 @property 

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

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

1041 if self.__configurator_number_of_runs is None: 

1042 self.__configurator_number_of_runs = self._abstract_getter( 

1043 Settings.OPTION_configurator_number_of_runs 

1044 ) 

1045 return self.__configurator_number_of_runs 

1046 

1047 @property 

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

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

1050 if self.__configurator_max_iterations is None: 

1051 self.__configurator_max_iterations = self._abstract_getter( 

1052 Settings.OPTION_configurator_max_iterations 

1053 ) 

1054 return self.__configurator_max_iterations 

1055 

1056 # Ablation settings ### 

1057 @property 

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

1059 """Get the ablation racing flag.""" 

1060 if self.__ablation_racing_flag is None: 

1061 self.__ablation_racing_flag = self._abstract_getter( 

1062 Settings.OPTION_ablation_racing 

1063 ) 

1064 return self.__ablation_racing_flag 

1065 

1066 @property 

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

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

1069 if self.__ablation_max_parallel_runs_per_node is None: 

1070 self.__ablation_max_parallel_runs_per_node = self._abstract_getter( 

1071 Settings.OPTION_ablation_clis_per_node 

1072 ) 

1073 return self.__ablation_max_parallel_runs_per_node 

1074 

1075 # Selection settings ### 

1076 @property 

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

1078 """Get the selection model.""" 

1079 if self.__selection_model is None: 

1080 self.__selection_model = self._abstract_getter( 

1081 Settings.OPTION_selection_model 

1082 ) 

1083 return self.__selection_model 

1084 

1085 @property 

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

1087 """Get the selection class.""" 

1088 if self.__selection_class is None: 

1089 self.__selection_class = self._abstract_getter( 

1090 Settings.OPTION_selection_class 

1091 ) 

1092 return self.__selection_class 

1093 

1094 @property 

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

1096 """Get the minimum marginal contribution.""" 

1097 if self.__minimum_marginal_contribution is None: 

1098 self.__minimum_marginal_contribution = self._abstract_getter( 

1099 Settings.OPTION_minimum_marginal_contribution 

1100 ) 

1101 return self.__minimum_marginal_contribution 

1102 

1103 # Configuration: SMAC2 specific settings ### 

1104 @property 

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

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

1107 if self.__smac2_wallclock_time_budget is None: 

1108 self.__smac2_wallclock_time_budget = self._abstract_getter( 

1109 Settings.OPTION_smac2_wallclock_time_budget 

1110 ) 

1111 return self.__smac2_wallclock_time_budget 

1112 

1113 @property 

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

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

1116 if self.__smac2_cpu_time_budget is None: 

1117 self.__smac2_cpu_time_budget = self._abstract_getter( 

1118 Settings.OPTION_smac2_cpu_time_budget 

1119 ) 

1120 return self.__smac2_cpu_time_budget 

1121 

1122 @property 

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

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

1125 if self.__smac2_target_cutoff_length is None: 

1126 self.__smac2_target_cutoff_length = self._abstract_getter( 

1127 Settings.OPTION_smac2_target_cutoff_length 

1128 ) 

1129 return self.__smac2_target_cutoff_length 

1130 

1131 @property 

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

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

1134 if self.__smac2_use_tunertime_in_cpu_time_budget is None: 

1135 self.__smac2_use_tunertime_in_cpu_time_budget = self._abstract_getter( 

1136 Settings.OPTION_smac2_count_tuner_time 

1137 ) 

1138 return self.__smac2_use_tunertime_in_cpu_time_budget 

1139 

1140 @property 

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

1142 """Return the SMAC2 CLI cores.""" 

1143 if self.__smac2_cli_cores is None: 

1144 self.__smac2_cli_cores = self._abstract_getter( 

1145 Settings.OPTION_smac2_cli_cores 

1146 ) 

1147 return self.__smac2_cli_cores 

1148 

1149 @property 

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

1151 """Return the SMAC2 max iterations.""" 

1152 if self.__smac2_max_iterations is None: 

1153 self.__smac2_max_iterations = self._abstract_getter( 

1154 Settings.OPTION_smac2_max_iterations 

1155 ) 

1156 return self.__smac2_max_iterations 

1157 

1158 # SMAC3 attributes ### 

1159 @property 

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

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

1162 if self.__smac3_number_of_trials is None: 

1163 self.__smac3_number_of_trials = self._abstract_getter( 

1164 Settings.OPTION_smac3_number_of_trials 

1165 ) 

1166 return self.__smac3_number_of_trials 

1167 

1168 @property 

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

1170 """Return the SMAC3 facade.""" 

1171 if self.__smac3_facade is None: 

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

1173 return self.__smac3_facade 

1174 

1175 @property 

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

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

1178 if self.__smac3_facade_max_ratio is None: 

1179 self.__smac3_facade_max_ratio = self._abstract_getter( 

1180 Settings.OPTION_smac3_facade_max_ratio 

1181 ) 

1182 return self.__smac3_facade_max_ratio 

1183 

1184 @property 

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

1186 """Return the SMAC3 crash cost.""" 

1187 if self.__smac3_crash_cost is None: 

1188 self.__smac3_crash_cost = self._abstract_getter( 

1189 Settings.OPTION_smac3_crash_cost 

1190 ) 

1191 return self.__smac3_crash_cost 

1192 

1193 @property 

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

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

1196 if self.__smac3_termination_cost_threshold is None: 

1197 self.__smac3_termination_cost_threshold = self._abstract_getter( 

1198 Settings.OPTION_smac3_termination_cost_threshold 

1199 ) 

1200 return self.__smac3_termination_cost_threshold 

1201 

1202 @property 

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

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

1205 if self.__smac3_wallclock_time_limit is None: 

1206 self.__smac3_wallclock_time_limit = self._abstract_getter( 

1207 Settings.OPTION_smac3_wallclock_time_budget 

1208 ) 

1209 return self.__smac3_wallclock_time_limit 

1210 

1211 @property 

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

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

1214 if self.__smac3_cputime_limit is None: 

1215 self.__smac3_cputime_limit = self._abstract_getter( 

1216 Settings.OPTION_smac3_cpu_time_budget 

1217 ) 

1218 return self.__smac3_cputime_limit 

1219 

1220 @property 

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

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

1223 if self.__smac3_use_default_config is None: 

1224 self.__smac3_use_default_config = self._abstract_getter( 

1225 Settings.OPTION_smac3_use_default_config 

1226 ) 

1227 return self.__smac3_use_default_config 

1228 

1229 @property 

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

1231 """Return the SMAC3 min budget.""" 

1232 if self.__smac3_min_budget is None: 

1233 self.__smac3_min_budget = self._abstract_getter( 

1234 Settings.OPTION_smac3_min_budget 

1235 ) 

1236 return self.__smac3_min_budget 

1237 

1238 @property 

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

1240 """Return the SMAC3 max budget.""" 

1241 if self.__smac3_max_budget is None: 

1242 self.__smac3_max_budget = self._abstract_getter( 

1243 Settings.OPTION_smac3_max_budget 

1244 ) 

1245 return self.__smac3_max_budget 

1246 

1247 # IRACE settings ### 

1248 @property 

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

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

1251 if self.__irace_max_time is None: 

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

1253 return self.__irace_max_time 

1254 

1255 @property 

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

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

1258 if self.__irace_max_experiments is None: 

1259 self.__irace_max_experiments = self._abstract_getter( 

1260 Settings.OPTION_irace_max_experiments 

1261 ) 

1262 return self.__irace_max_experiments 

1263 

1264 @property 

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

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

1267 if self.__irace_first_test is None: 

1268 self.__irace_first_test = self._abstract_getter( 

1269 Settings.OPTION_irace_first_test 

1270 ) 

1271 return self.__irace_first_test 

1272 

1273 @property 

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

1275 """Return the mu for IRACE.""" 

1276 if self.__irace_mu is None: 

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

1278 return self.__irace_mu 

1279 

1280 @property 

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

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

1283 if self.__irace_max_iterations is None: 

1284 self.__irace_max_iterations = self._abstract_getter( 

1285 Settings.OPTION_irace_max_iterations 

1286 ) 

1287 return self.__irace_max_iterations 

1288 

1289 # ParamILS settings ### 

1290 @property 

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

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

1293 if self.__paramils_cpu_time_budget is None: 

1294 self.__paramils_cpu_time_budget = self._abstract_getter( 

1295 Settings.OPTION_paramils_cpu_time_budget 

1296 ) 

1297 return self.__paramils_cpu_time_budget 

1298 

1299 @property 

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

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

1302 if self.__paramils_min_runs is None: 

1303 self.__paramils_min_runs = self._abstract_getter( 

1304 Settings.OPTION_paramils_min_runs 

1305 ) 

1306 return self.__paramils_min_runs 

1307 

1308 @property 

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

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

1311 if self.__paramils_max_runs is None: 

1312 self.__paramils_max_runs = self._abstract_getter( 

1313 Settings.OPTION_paramils_max_runs 

1314 ) 

1315 return self.__paramils_max_runs 

1316 

1317 @property 

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

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

1320 if self.__paramils_random_restart is None: 

1321 self.__paramils_random_restart = self._abstract_getter( 

1322 Settings.OPTION_paramils_random_restart 

1323 ) 

1324 return self.__paramils_random_restart 

1325 

1326 @property 

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

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

1329 if self.__paramils_focused_approach is None: 

1330 self.__paramils_focused_approach = self._abstract_getter( 

1331 Settings.OPTION_paramils_focused 

1332 ) 

1333 return self.__paramils_focused_approach 

1334 

1335 @property 

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

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

1338 if self.__paramils_use_cpu_time_in_tunertime is None: 

1339 self.__paramils_use_cpu_time_in_tunertime = self._abstract_getter( 

1340 Settings.OPTION_paramils_count_tuner_time 

1341 ) 

1342 return self.__paramils_use_cpu_time_in_tunertime 

1343 

1344 @property 

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

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

1347 if self.__paramils_cli_cores is None: 

1348 self.__paramils_cli_cores = self._abstract_getter( 

1349 Settings.OPTION_paramils_cli_cores 

1350 ) 

1351 return self.__paramils_cli_cores 

1352 

1353 @property 

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

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

1356 if self.__paramils_max_iterations is None: 

1357 self.__paramils_max_iterations = self._abstract_getter( 

1358 Settings.OPTION_paramils_max_iterations 

1359 ) 

1360 return self.__paramils_max_iterations 

1361 

1362 @property 

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

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

1365 if self.__paramils_number_initial_configurations is None: 

1366 self.__paramils_number_initial_configurations = self._abstract_getter( 

1367 Settings.OPTION_paramils_number_initial_configurations 

1368 ) 

1369 return self.__paramils_number_initial_configurations 

1370 

1371 # Parallel Portfolio settings ### 

1372 @property 

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

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

1375 if self.__parallel_portfolio_check_interval is None: 

1376 self.__parallel_portfolio_check_interval = self._abstract_getter( 

1377 Settings.OPTION_parallel_portfolio_check_interval 

1378 ) 

1379 return self.__parallel_portfolio_check_interval 

1380 

1381 @property 

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

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

1384 if self.__parallel_portfolio_num_seeds_per_solver is None: 

1385 self.__parallel_portfolio_num_seeds_per_solver = self._abstract_getter( 

1386 Settings.OPTION_parallel_portfolio_number_of_seeds_per_solver 

1387 ) 

1388 return self.__parallel_portfolio_num_seeds_per_solver 

1389 

1390 # Slurm settings ### 

1391 @property 

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

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

1394 if self.__slurm_jobs_in_parallel is None: 

1395 self.__slurm_jobs_in_parallel = self._abstract_getter( 

1396 Settings.OPTION_slurm_parallel_jobs 

1397 ) 

1398 return self.__slurm_jobs_in_parallel 

1399 

1400 @property 

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

1402 """Return the slurm job prepend.""" 

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

1404 Settings.OPTION_slurm_prepend_script.section, 

1405 Settings.OPTION_slurm_prepend_script.name, 

1406 ): 

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

1408 Settings.OPTION_slurm_prepend_script.name 

1409 ] 

1410 try: 

1411 path = Path(value) 

1412 if path.is_file(): 

1413 with path.open() as f: 

1414 value = f.read() 

1415 f.close() 

1416 self.__slurm_job_prepend = str(value) 

1417 except TypeError: 

1418 pass 

1419 return self.__slurm_job_prepend 

1420 

1421 @property 

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

1423 """Return the sbatch settings.""" 

1424 sbatch_options = self.__settings[Settings.SECTION_slurm] 

1425 # Return all non-predefined keys 

1426 return [ 

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

1428 for key in sbatch_options.keys() 

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

1430 ] 

1431 

1432 # General functionalities ### 

1433 

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

1435 """Return the configurator output path.""" 

1436 return self.DEFAULT_configuration_output / configurator.name 

1437 

1438 def get_configurator_settings( 

1439 self: Settings, configurator_name: str 

1440 ) -> dict[str, any]: 

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

1442 configurator_settings = { 

1443 "solver_calls": self.configurator_solver_call_budget, 

1444 "solver_cutoff_time": self.solver_cutoff_time, 

1445 "max_iterations": self.configurator_max_iterations, 

1446 } 

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

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

1449 if ( 

1450 configurator_name == "SMAC2" 

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

1452 # Return all settings from the SMAC2 section 

1453 configurator_settings.update( 

1454 { 

1455 "cpu_time": self.smac2_cpu_time_budget, 

1456 "wallclock_time": self.smac2_wallclock_time_budget, 

1457 "target_cutoff_length": self.smac2_target_cutoff_length, 

1458 "use_cpu_time_in_tunertime": self.smac2_use_tunertime_in_cpu_time_budget, 

1459 "cli_cores": self.smac2_cli_cores, 

1460 "max_iterations": self.smac2_max_iterations 

1461 or configurator_settings["max_iterations"], 

1462 } 

1463 ) 

1464 elif ( 

1465 configurator_name == "SMAC3" 

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

1467 # Return all settings from the SMAC3 section 

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

1469 configurator_settings.update( 

1470 { 

1471 "smac_facade": self.smac3_facade, 

1472 "max_ratio": self.smac3_facade_max_ratio, 

1473 "crash_cost": self.smac3_crash_cost, 

1474 "termination_cost_threshold": self.smac3_termination_cost_threshold, 

1475 "walltime_limit": self.smac3_wallclock_time_budget, 

1476 "cputime_limit": self.smac3_cpu_time_budget, 

1477 "use_default_config": self.smac3_use_default_config, 

1478 "min_budget": self.smac3_min_budget, 

1479 "max_budget": self.smac3_max_budget, 

1480 "solver_calls": self.smac3_number_of_trials 

1481 or configurator_settings["solver_calls"], 

1482 } 

1483 ) 

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

1485 configurator_settings = { 

1486 key: value 

1487 for key, value in configurator_settings.items() 

1488 if value is not None 

1489 } 

1490 elif ( 

1491 configurator_name == "IRACE" 

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

1493 # Return all settings from the IRACE section 

1494 configurator_settings.update( 

1495 { 

1496 "solver_calls": self.irace_max_experiments, 

1497 "max_time": self.irace_max_time, 

1498 "first_test": self.irace_first_test, 

1499 "mu": self.irace_mu, 

1500 "max_iterations": self.irace_max_iterations 

1501 or configurator_settings["max_iterations"], 

1502 } 

1503 ) 

1504 if ( 

1505 configurator_settings["solver_calls"] == 0 

1506 and configurator_settings["max_time"] == 0 

1507 ): # Default to base 

1508 configurator_settings["solver_calls"] = ( 

1509 self.configurator_solver_call_budget 

1510 ) 

1511 elif ( 

1512 configurator_name == "ParamILS" 

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

1514 configurator_settings.update( 

1515 { 

1516 "tuner_timeout": self.paramils_cpu_time_budget, 

1517 "min_runs": self.paramils_min_runs, 

1518 "max_runs": self.paramils_max_runs, 

1519 "focused_ils": self.paramils_focused_approach, 

1520 "initial_configurations": self.paramils_number_initial_configurations, 

1521 "random_restart": self.paramils_random_restart, 

1522 "cli_cores": self.paramils_cli_cores, 

1523 "use_cpu_time_in_tunertime": self.paramils_use_cpu_time_in_tunertime, 

1524 "max_iterations": self.paramils_max_iterations 

1525 or configurator_settings["max_iterations"], 

1526 } 

1527 ) 

1528 return configurator_settings 

1529 

1530 @staticmethod 

1531 def check_settings_changes( 

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

1533 ) -> bool: 

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

1535 

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

1537 

1538 Args: 

1539 cur_settings: The current settings 

1540 prev_settings: The previous settings 

1541 verbose: Verbosity of the function 

1542 

1543 Returns: 

1544 True iff there are changes. 

1545 """ 

1546 cur_dict = cur_settings.__settings._sections 

1547 prev_dict = prev_settings.__settings._sections 

1548 

1549 cur_sections_set = set(cur_dict.keys()) 

1550 prev_sections_set = set(prev_dict.keys()) 

1551 

1552 sections_remained = cur_sections_set & prev_sections_set 

1553 option_changed = False 

1554 for section in sections_remained: 

1555 printed_section = False 

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

1557 if ( 

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

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

1560 names.remove("seed") 

1561 for name in names: 

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

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

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

1565 

1566 # If cur val is None, it is default 

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

1568 if not option_changed and verbose: # Print the initial 

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

1570 option_changed = True 

1571 

1572 # do we have yet to print the section? 

1573 if not printed_section and verbose: 

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

1575 printed_section = True 

1576 

1577 # print actual change 

1578 if verbose: 

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

1580 

1581 return option_changed