Coverage for sparkle/platform/settings_objects.py: 31%

544 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-27 09:10 +0000

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

2from __future__ import annotations 

3import configparser 

4from enum import Enum 

5from pathlib import Path 

6from pathlib import PurePath 

7 

8from sparkle.types import SparkleObjective, resolve_objective 

9from sparkle.types.objective import PAR 

10from sparkle.solver import Selector 

11from sparkle.configurator.configurator import Configurator 

12from sparkle.solver.verifier import SATVerifier 

13from sparkle.configurator import implementations as cim 

14 

15from runrunner import Runner 

16from sparkle.platform.cli_types import VerbosityLevel 

17 

18 

19class SettingState(Enum): 

20 """Enum of possible setting states.""" 

21 

22 NOT_SET = 0 

23 DEFAULT = 1 

24 FILE = 2 

25 CMD_LINE = 3 

26 

27 

28class Settings: 

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

30 # CWD Prefix 

31 cwd_prefix = Path() # Empty for now 

32 

33 # Library prefix 

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

35 

36 # Default directory names 

37 rawdata_dir = Path("Raw_Data") 

38 analysis_dir = Path("Analysis") 

39 __settings_dir = Path("Settings") 

40 __settings_file = Path("sparkle_settings.ini") 

41 

42 # Default settings path 

43 DEFAULT_settings_path = PurePath(cwd_prefix / __settings_dir / __settings_file) 

44 

45 # Default library pathing 

46 DEFAULT_components = lib_prefix / "Components" 

47 

48 # Example settings path 

49 DEFAULT_example_settings_path = PurePath(DEFAULT_components / "sparkle_settings.ini") 

50 

51 # Runsolver component 

52 DEFAULT_runsolver_dir = DEFAULT_components / "runsolver" / "src" 

53 DEFAULT_runsolver_exec = DEFAULT_runsolver_dir / "runsolver" 

54 

55 # Ablation component 

56 DEFAULT_ablation_dir = DEFAULT_components / "ablationAnalysis-0.9.4" 

57 DEFAULT_ablation_exec = DEFAULT_ablation_dir / "ablationAnalysis" 

58 DEFAULT_ablation_validation_exec = DEFAULT_ablation_dir / "ablationValidation" 

59 

60 # Autofolio component 

61 DEFAULT_general_sparkle_selector = DEFAULT_components / "AutoFolio/scripts/autofolio" 

62 

63 # Report component 

64 DEFAULT_latex_source = DEFAULT_components / "Sparkle-latex-source" 

65 DEFAULT_latex_bib = DEFAULT_latex_source / "SparkleReport.bib" 

66 

67 # Default input directory pathing 

68 DEFAULT_solver_dir = cwd_prefix / "Solvers" 

69 DEFAULT_instance_dir = cwd_prefix / "Instances" 

70 DEFAULT_extractor_dir = cwd_prefix / "Extractors" 

71 DEFAULT_snapshot_dir = cwd_prefix / "Snapshots" 

72 

73 # Default output directory pathing 

74 DEFAULT_tmp_output = cwd_prefix / "Tmp" 

75 DEFAULT_output = cwd_prefix / "Output" 

76 DEFAULT_configuration_output = DEFAULT_output / "Configuration" 

77 DEFAULT_selection_output = DEFAULT_output / "Selection" 

78 DEFAULT_validation_output = DEFAULT_output / "Validation" 

79 DEFAULT_parallel_portfolio_output = DEFAULT_output / "Parallel_Portfolio" 

80 DEFAULT_ablation_output = DEFAULT_output / "Ablation" 

81 DEFAULT_log_output = DEFAULT_output / "Log" 

82 

83 # Default output subdirs 

84 DEFAULT_configuration_output_raw = DEFAULT_configuration_output / rawdata_dir 

85 DEFAULT_configuration_output_analysis = DEFAULT_configuration_output / analysis_dir 

86 DEFAULT_selection_output_raw = DEFAULT_selection_output / rawdata_dir 

87 DEFAULT_selection_output_analysis = DEFAULT_selection_output / analysis_dir 

88 DEFAULT_parallel_portfolio_output_raw =\ 

89 DEFAULT_parallel_portfolio_output / rawdata_dir 

90 DEFAULT_parallel_portfolio_output_analysis =\ 

91 DEFAULT_parallel_portfolio_output / analysis_dir 

92 

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

94 DEFAULT_feature_data = DEFAULT_output / "Feature_Data" 

95 DEFAULT_performance_data = DEFAULT_output / "Performance_Data" 

96 

97 # Collection of all working dirs for platform 

98 DEFAULT_working_dirs = [ 

99 DEFAULT_output, DEFAULT_configuration_output, 

100 DEFAULT_selection_output, DEFAULT_validation_output, 

101 DEFAULT_tmp_output, 

102 DEFAULT_log_output, 

103 DEFAULT_solver_dir, DEFAULT_instance_dir, 

104 DEFAULT_feature_data, DEFAULT_performance_data, 

105 DEFAULT_extractor_dir, 

106 ] 

107 

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

109 DEFAULT_feature_data_path =\ 

110 DEFAULT_feature_data / "feature_data.csv" 

111 DEFAULT_performance_data_path =\ 

112 DEFAULT_performance_data / "performance_data.csv" 

113 

114 # Constant default values 

115 DEFAULT_general_sparkle_objective = PAR(10) 

116 DEFAULT_general_sparkle_configurator = cim.SMAC2.__name__ 

117 DEFAULT_general_solution_verifier = str(None) 

118 DEFAULT_general_target_cutoff_time = 60 

119 DEFAULT_general_extractor_cutoff_time = 60 

120 DEFAULT_number_of_jobs_in_parallel = 25 

121 DEFAULT_general_verbosity = VerbosityLevel.STANDARD 

122 DEFAULT_general_check_interval = 10 

123 

124 DEFAULT_config_wallclock_time = 600 

125 DEFAULT_config_cpu_time = None 

126 DEFAULT_config_solver_calls = None 

127 DEFAULT_config_number_of_runs = 25 

128 DEFAULT_configurator_target_cutoff_length = "max" 

129 

130 DEFAULT_portfolio_construction_timeout = None 

131 

132 DEFAULT_slurm_max_parallel_runs_per_node = 8 

133 

134 DEFAULT_ablation_racing = False 

135 

136 DEFAULT_parallel_portfolio_check_interval = 4 

137 DEFAULT_parallel_portfolio_num_seeds_per_solver = 1 

138 

139 def __init__(self: Settings, file_path: PurePath = None) -> None: 

140 """Initialise a settings object.""" 

141 # Settings 'dictionary' in configparser format 

142 self.__settings = configparser.ConfigParser() 

143 

144 # Setting flags 

145 self.__general_sparkle_objective_set = SettingState.NOT_SET 

146 self.__general_sparkle_configurator_set = SettingState.NOT_SET 

147 self.__general_sparkle_selector_set = SettingState.NOT_SET 

148 self.__general_solution_verifier_set = SettingState.NOT_SET 

149 self.__general_target_cutoff_time_set = SettingState.NOT_SET 

150 self.__general_extractor_cutoff_time_set = SettingState.NOT_SET 

151 self.__general_verbosity_set = SettingState.NOT_SET 

152 self.__general_check_interval_set = SettingState.NOT_SET 

153 

154 self.__config_wallclock_time_set = SettingState.NOT_SET 

155 self.__config_cpu_time_set = SettingState.NOT_SET 

156 self.__config_solver_calls_set = SettingState.NOT_SET 

157 self.__config_number_of_runs_set = SettingState.NOT_SET 

158 

159 self.__run_on_set = SettingState.NOT_SET 

160 self.__number_of_jobs_in_parallel_set = SettingState.NOT_SET 

161 self.__slurm_max_parallel_runs_per_node_set = SettingState.NOT_SET 

162 self.__configurator_target_cutoff_length_set = SettingState.NOT_SET 

163 self.__slurm_extra_options_set = dict() 

164 self.__ablation_racing_flag_set = SettingState.NOT_SET 

165 

166 self.__parallel_portfolio_check_interval_set = SettingState.NOT_SET 

167 self.__parallel_portfolio_num_seeds_per_solver_set = SettingState.NOT_SET 

168 

169 self.__general_sparkle_configurator = None 

170 

171 if file_path is None: 

172 # Initialise settings from default file path 

173 self.read_settings_ini() 

174 else: 

175 # Initialise settings from a given file path 

176 self.read_settings_ini(file_path) 

177 

178 def read_settings_ini(self: Settings, file_path: PurePath = DEFAULT_settings_path, 

179 state: SettingState = SettingState.FILE) -> None: 

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

181 # Read file 

182 file_settings = configparser.ConfigParser() 

183 file_settings.read(file_path) 

184 

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

186 # successfully 

187 if file_settings.sections() != []: 

188 section = "general" 

189 option_names = ("objective", ) 

190 for option in option_names: 

191 if file_settings.has_option(section, option): 

192 value = [resolve_objective(obj) for obj in 

193 file_settings.get(section, option).split(",")] 

194 self.set_general_sparkle_objectives(value, state) 

195 file_settings.remove_option(section, option) 

196 

197 # Comma so python understands it's a tuple... 

198 option_names = ("configurator", ) 

199 for option in option_names: 

200 if file_settings.has_option(section, option): 

201 value = file_settings.get(section, option) 

202 self.set_general_sparkle_configurator(value, state) 

203 file_settings.remove_option(section, option) 

204 

205 option_names = ("selector", ) 

206 for option in option_names: 

207 if file_settings.has_option(section, option): 

208 value = file_settings.get(section, option) 

209 self.set_general_sparkle_selector(value, state) 

210 file_settings.remove_option(section, option) 

211 

212 option_names = ("solution_verifier", ) 

213 for option in option_names: 

214 if file_settings.has_option(section, option): 

215 value = file_settings.get(section, option).lower() 

216 self.set_general_solution_verifier(value, state) 

217 file_settings.remove_option(section, option) 

218 

219 option_names = ("target_cutoff_time", 

220 "cutoff_time_each_solver_call") 

221 for option in option_names: 

222 if file_settings.has_option(section, option): 

223 value = file_settings.getint(section, option) 

224 self.set_general_target_cutoff_time(value, state) 

225 file_settings.remove_option(section, option) 

226 

227 option_names = ("extractor_cutoff_time", 

228 "cutoff_time_each_feature_computation") 

229 for option in option_names: 

230 if file_settings.has_option(section, option): 

231 value = file_settings.getint(section, option) 

232 self.set_general_extractor_cutoff_time(value, state) 

233 file_settings.remove_option(section, option) 

234 

235 option_names = ("run_on", ) 

236 for option in option_names: 

237 if file_settings.has_option(section, option): 

238 value = file_settings.get(section, option) 

239 self.set_run_on(value, state) 

240 file_settings.remove_option(section, option) 

241 

242 option_names = ("verbosity", ) 

243 for option in option_names: 

244 if file_settings.has_option(section, option): 

245 value = VerbosityLevel.from_string( 

246 file_settings.get(section, option)) 

247 self.set_general_verbosity(value, state) 

248 file_settings.remove_option(section, option) 

249 

250 option_names = ("check_interval", ) 

251 for option in option_names: 

252 if file_settings.has_option(section, option): 

253 value = int(file_settings.get(section, option)) 

254 self.set_general_check_interval(value, state) 

255 file_settings.remove_option(section, option) 

256 

257 section = "configuration" 

258 option_names = ("wallclock_time", ) 

259 for option in option_names: 

260 if file_settings.has_option(section, option): 

261 value = file_settings.getint(section, option) 

262 self.set_config_wallclock_time(value, state) 

263 file_settings.remove_option(section, option) 

264 

265 option_names = ("cpu_time", ) 

266 for option in option_names: 

267 if file_settings.has_option(section, option): 

268 value = file_settings.getint(section, option) 

269 self.set_config_cpu_time(value, state) 

270 file_settings.remove_option(section, option) 

271 

272 option_names = ("solver_calls", ) 

273 for option in option_names: 

274 if file_settings.has_option(section, option): 

275 value = file_settings.getint(section, option) 

276 self.set_config_solver_calls(value, state) 

277 file_settings.remove_option(section, option) 

278 

279 option_names = ("number_of_runs", ) 

280 for option in option_names: 

281 if file_settings.has_option(section, option): 

282 value = file_settings.getint(section, option) 

283 self.set_config_number_of_runs(value, state) 

284 file_settings.remove_option(section, option) 

285 

286 option_names = ("target_cutoff_length", "smac_each_run_cutoff_length") 

287 for option in option_names: 

288 if file_settings.has_option(section, option): 

289 value = file_settings.get(section, option) 

290 self.set_configurator_target_cutoff_length(value, state) 

291 file_settings.remove_option(section, option) 

292 

293 section = "slurm" 

294 option_names = ("number_of_jobs_in_parallel", "num_job_in_parallel") 

295 for option in option_names: 

296 if file_settings.has_option(section, option): 

297 value = file_settings.getint(section, option) 

298 self.set_number_of_jobs_in_parallel(value, state) 

299 file_settings.remove_option(section, option) 

300 

301 option_names = ("max_parallel_runs_per_node", "clis_per_node") 

302 for option in option_names: 

303 if file_settings.has_option(section, option): 

304 value = file_settings.getint(section, option) 

305 self.set_slurm_max_parallel_runs_per_node(value, state) 

306 file_settings.remove_option(section, option) 

307 

308 section = "ablation" 

309 option_names = ("racing", "ablation_racing") 

310 for option in option_names: 

311 if file_settings.has_option(section, option): 

312 value = file_settings.getboolean(section, option) 

313 self.set_ablation_racing_flag(value, state) 

314 file_settings.remove_option(section, option) 

315 

316 section = "parallel_portfolio" 

317 option_names = ("check_interval", ) 

318 for option in option_names: 

319 if file_settings.has_option(section, option): 

320 value = int(file_settings.get(section, option)) 

321 self.set_parallel_portfolio_check_interval(value, state) 

322 file_settings.remove_option(section, option) 

323 

324 option_names = ("num_seeds_per_solver", ) 

325 for option in option_names: 

326 if file_settings.has_option(section, option): 

327 value = int(file_settings.get(section, option)) 

328 self.set_parallel_portfolio_number_of_seeds_per_solver(value, state) 

329 file_settings.remove_option(section, option) 

330 

331 # TODO: Report on any unknown settings that were read 

332 sections = file_settings.sections() 

333 

334 for section in sections: 

335 for option in file_settings[section]: 

336 # TODO: Should check the options are valid Slurm options 

337 if section == "slurm": 

338 value = file_settings.get(section, option) 

339 self.add_slurm_extra_option(option, value, state) 

340 else: 

341 print(f'Unrecognised section - option combination: "{section} ' 

342 f'{option}" in file {file_path} ignored') 

343 

344 # Print error if unable to read the settings 

345 else: 

346 print(f"ERROR: Failed to read settings from {file_path} The file may have " 

347 "been empty, located in a different path, or be in another format than" 

348 " INI. Default Settings values be used.") 

349 

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

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

352 # Write to latest settings file 

353 self.write_settings_ini(self.__settings_dir / "latest.ini") 

354 

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

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

357 # Create needed directories if they don't exist 

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

359 slurm_extra_section_options = None 

360 if self.__settings.has_section("slurm_extra"): 

361 # Slurm extra options are not written as a seperate section 

362 slurm_extra_section_options = {} 

363 for key in self.__settings["slurm_extra"]: 

364 self.__settings["slurm"][key] = self.__settings["slurm_extra"][key] 

365 slurm_extra_section_options[key] = self.__settings["slurm_extra"][key] 

366 self.__settings.remove_section("slurm_extra") 

367 # Write the settings to file 

368 with file_path.open("w") as settings_file: 

369 self.__settings.write(settings_file) 

370 # Rebuild slurm extra if needed 

371 if slurm_extra_section_options is not None: 

372 self.__settings.add_section("slurm_extra") 

373 for key in slurm_extra_section_options: 

374 self.__settings["slurm_extra"][key] = slurm_extra_section_options[key] 

375 

376 def __init_section(self: Settings, section: str) -> None: 

377 if section not in self.__settings: 

378 self.__settings[section] = {} 

379 

380 @staticmethod 

381 def __check_setting_state(current_state: SettingState, 

382 new_state: SettingState, name: str) -> bool: 

383 change_setting_ok = True 

384 

385 if current_state == SettingState.FILE and new_state == SettingState.DEFAULT: 

386 change_setting_ok = False 

387 print(f"Warning: Attempting to overwrite setting for {name} with default " 

388 "value; keeping the value read from file!") 

389 elif (current_state == SettingState.CMD_LINE 

390 and new_state == SettingState.DEFAULT): 

391 change_setting_ok = False 

392 print(f"Warning: Attempting to overwrite setting for {name} with default " 

393 "value; keeping the value read from command line!") 

394 elif current_state == SettingState.CMD_LINE and new_state == SettingState.FILE: 

395 change_setting_ok = False 

396 print(f"Warning: Attempting to overwrite setting for {name} with value from " 

397 "file; keeping the value read from command line!") 

398 

399 return change_setting_ok 

400 

401 # General settings ### 

402 def set_general_sparkle_objectives( 

403 self: Settings, 

404 value: list[SparkleObjective] = [DEFAULT_general_sparkle_objective, ], 

405 origin: SettingState = SettingState.DEFAULT) -> None: 

406 """Set the sparkle objective.""" 

407 section = "general" 

408 name = "objective" 

409 if value is not None and self.__check_setting_state( 

410 self.__general_sparkle_objective_set, origin, name): 

411 if isinstance(value, list): 

412 value = ",".join([str(obj) for obj in value]) 

413 else: 

414 value = str(value) 

415 # Append standard Sparkle Objectives 

416 if "status" not in value: 

417 value += ",status" 

418 if "cpu_time" not in value: 

419 value += ",cpu_time" 

420 if "wall_time" not in value: 

421 value += ",wall_time" 

422 if "memory" not in value: 

423 value += ",memory" 

424 self.__init_section(section) 

425 self.__general_sparkle_objective_set = origin 

426 self.__settings[section][name] = value 

427 

428 def get_general_sparkle_objectives(self: Settings) -> list[SparkleObjective]: 

429 """Return the performance measure.""" 

430 if self.__general_sparkle_objective_set == SettingState.NOT_SET: 

431 self.set_general_sparkle_objectives() 

432 

433 return [resolve_objective(obj) 

434 for obj in self.__settings["general"]["objective"].split(",")] 

435 

436 def set_general_sparkle_configurator( 

437 self: Settings, 

438 value: str = DEFAULT_general_sparkle_configurator, 

439 origin: SettingState = SettingState.DEFAULT) -> None: 

440 """Set the Sparkle configurator.""" 

441 section = "general" 

442 name = "configurator" 

443 if value is not None and self.__check_setting_state( 

444 self.__general_sparkle_configurator_set, origin, name): 

445 self.__init_section(section) 

446 self.__general_sparkle_configurator_set = origin 

447 self.__settings[section][name] = value 

448 

449 def get_general_sparkle_configurator(self: Settings) -> Configurator: 

450 """Return the configurator init method.""" 

451 if self.__general_sparkle_configurator_set == SettingState.NOT_SET: 

452 self.set_general_sparkle_configurator() 

453 if self.__general_sparkle_configurator is None: 

454 configurator_subclass =\ 

455 cim.resolve_configurator(self.__settings["general"]["configurator"]) 

456 if configurator_subclass is not None: 

457 self.__general_sparkle_configurator = configurator_subclass( 

458 objectives=self.get_general_sparkle_objectives(), 

459 base_dir=Settings.DEFAULT_tmp_output, 

460 output_path=Settings.DEFAULT_configuration_output_raw) 

461 else: 

462 print("WARNING: Configurator class name not recognised:" 

463 f'{self.__settings["general"]["configurator"]}. ' 

464 "Configurator not set.") 

465 return self.__general_sparkle_configurator 

466 

467 def set_general_sparkle_selector( 

468 self: Settings, 

469 value: Path = DEFAULT_general_sparkle_selector, 

470 origin: SettingState = SettingState.DEFAULT) -> None: 

471 """Set the Sparkle selector.""" 

472 section = "general" 

473 name = "selector" 

474 if value is not None and self.__check_setting_state( 

475 self.__general_sparkle_selector_set, origin, name): 

476 self.__init_section(section) 

477 self.__general_sparkle_selector_set = origin 

478 self.__settings[section][name] = str(value) 

479 

480 def get_general_sparkle_selector(self: Settings) -> Selector: 

481 """Return the selector init method.""" 

482 if self.__general_sparkle_selector_set == SettingState.NOT_SET: 

483 self.set_general_sparkle_selector() 

484 return Selector(Path(self.__settings["general"]["selector"]), 

485 self.DEFAULT_selection_output_raw) 

486 

487 def set_general_solution_verifier( 

488 self: Settings, value: str = DEFAULT_general_solution_verifier, 

489 origin: SettingState = SettingState.DEFAULT) -> None: 

490 """Set the solution verifier to use.""" 

491 section = "general" 

492 name = "solution_verifier" 

493 

494 if value is not None and self.__check_setting_state( 

495 self.__general_solution_verifier_set, origin, name): 

496 self.__init_section(section) 

497 self.__general_solution_verifier_set = origin 

498 self.__settings[section][name] = value 

499 

500 def get_general_solution_verifier(self: Settings) -> object: 

501 """Return the solution verifier to use.""" 

502 if self.__general_solution_verifier_set == SettingState.NOT_SET: 

503 self.set_general_solution_verifier() 

504 name = self.__settings["general"]["solution_verifier"].lower() 

505 if name == str(SATVerifier()).lower(): 

506 return SATVerifier() 

507 return None 

508 

509 def set_general_target_cutoff_time( 

510 self: Settings, value: int = DEFAULT_general_target_cutoff_time, 

511 origin: SettingState = SettingState.DEFAULT) -> None: 

512 """Set the cutoff time in seconds for target algorithms.""" 

513 section = "general" 

514 name = "target_cutoff_time" 

515 

516 if value is not None and self.__check_setting_state( 

517 self.__general_target_cutoff_time_set, origin, name): 

518 self.__init_section(section) 

519 self.__general_target_cutoff_time_set = origin 

520 self.__settings[section][name] = str(value) 

521 

522 def get_general_target_cutoff_time(self: Settings) -> int: 

523 """Return the cutoff time in seconds for target algorithms.""" 

524 if self.__general_target_cutoff_time_set == SettingState.NOT_SET: 

525 self.set_general_target_cutoff_time() 

526 return int(self.__settings["general"]["target_cutoff_time"]) 

527 

528 def set_general_extractor_cutoff_time( 

529 self: Settings, value: int = DEFAULT_general_extractor_cutoff_time, 

530 origin: SettingState = SettingState.DEFAULT) -> None: 

531 """Set the cutoff time in seconds for feature extraction.""" 

532 section = "general" 

533 name = "extractor_cutoff_time" 

534 

535 if value is not None and self.__check_setting_state( 

536 self.__general_extractor_cutoff_time_set, origin, name): 

537 self.__init_section(section) 

538 self.__general_extractor_cutoff_time_set = origin 

539 self.__settings[section][name] = str(value) 

540 

541 def get_general_extractor_cutoff_time(self: Settings) -> int: 

542 """Return the cutoff time in seconds for feature extraction.""" 

543 if self.__general_extractor_cutoff_time_set == SettingState.NOT_SET: 

544 self.set_general_extractor_cutoff_time() 

545 return int(self.__settings["general"]["extractor_cutoff_time"]) 

546 

547 def set_number_of_jobs_in_parallel( 

548 self: Settings, value: int = DEFAULT_number_of_jobs_in_parallel, 

549 origin: SettingState = SettingState.DEFAULT) -> None: 

550 """Set the number of runs Sparkle can do in parallel.""" 

551 section = "slurm" 

552 name = "number_of_jobs_in_parallel" 

553 

554 if value is not None and self.__check_setting_state( 

555 self.__number_of_jobs_in_parallel_set, origin, name): 

556 self.__init_section(section) 

557 self.__number_of_jobs_in_parallel_set = origin 

558 self.__settings[section][name] = str(value) 

559 

560 def get_number_of_jobs_in_parallel(self: Settings) -> int: 

561 """Return the number of runs Sparkle can do in parallel.""" 

562 if self.__number_of_jobs_in_parallel_set == SettingState.NOT_SET: 

563 self.set_number_of_jobs_in_parallel() 

564 

565 return int(self.__settings["slurm"]["number_of_jobs_in_parallel"]) 

566 

567 def set_general_verbosity( 

568 self: Settings, value: VerbosityLevel = DEFAULT_general_verbosity, 

569 origin: SettingState = SettingState.DEFAULT) -> None: 

570 """Set the general verbosity to use.""" 

571 section = "general" 

572 name = "verbosity" 

573 

574 if value is not None and self.__check_setting_state( 

575 self.__general_verbosity_set, origin, name): 

576 self.__init_section(section) 

577 self.__general_verbosity_set = origin 

578 self.__settings[section][name] = value.name 

579 

580 def get_general_verbosity(self: Settings) -> VerbosityLevel: 

581 """Return the general verbosity.""" 

582 if self.__general_verbosity_set == SettingState.NOT_SET: 

583 self.set_general_verbosity() 

584 

585 return VerbosityLevel.from_string( 

586 self.__settings["general"]["verbosity"]) 

587 

588 def set_general_check_interval( 

589 self: Settings, 

590 value: int = DEFAULT_general_check_interval, 

591 origin: SettingState = SettingState.DEFAULT) -> None: 

592 """Set the general check interval.""" 

593 section = "general" 

594 name = "check_interval" 

595 

596 if value is not None and self.__check_setting_state( 

597 self.__general_check_interval_set, origin, name): 

598 self.__init_section(section) 

599 self.__general_check_interval_set = origin 

600 self.__settings[section][name] = str(value) 

601 

602 def get_general_check_interval(self: Settings) -> int: 

603 """Return the general check interval.""" 

604 if self.__general_check_interval_set == SettingState.NOT_SET: 

605 self.set_general_check_interval() 

606 

607 return int( 

608 self.__settings["general"]["check_interval"]) 

609 

610 # Configuration settings ### 

611 

612 def set_config_wallclock_time( 

613 self: Settings, value: int = DEFAULT_config_wallclock_time, 

614 origin: SettingState = SettingState.DEFAULT) -> None: 

615 """Set the budget per configuration run in seconds (wallclock).""" 

616 section = "configuration" 

617 name = "wallclock_time" 

618 

619 if value is not None and self.__check_setting_state( 

620 self.__config_wallclock_time_set, origin, name): 

621 self.__init_section(section) 

622 self.__config_wallclock_time_set = origin 

623 self.__settings[section][name] = str(value) 

624 

625 def get_config_wallclock_time(self: Settings) -> int: 

626 """Return the budget per configuration run in seconds (wallclock).""" 

627 if self.__config_wallclock_time_set == SettingState.NOT_SET: 

628 self.set_config_wallclock_time() 

629 return int(self.__settings["configuration"]["wallclock_time"]) 

630 

631 def set_config_cpu_time( 

632 self: Settings, value: int = DEFAULT_config_cpu_time, 

633 origin: SettingState = SettingState.DEFAULT) -> None: 

634 """Set the budget per configuration run in seconds (cpu).""" 

635 section = "configuration" 

636 name = "cpu_time" 

637 

638 if value is not None and self.__check_setting_state( 

639 self.__config_cpu_time_set, origin, name): 

640 self.__init_section(section) 

641 self.__config_cpu_time_set = origin 

642 self.__settings[section][name] = str(value) 

643 

644 def get_config_cpu_time(self: Settings) -> int | None: 

645 """Return the budget per configuration run in seconds (cpu).""" 

646 if self.__config_cpu_time_set == SettingState.NOT_SET: 

647 self.set_config_cpu_time() 

648 return None 

649 

650 return int(self.__settings["configuration"]["cpu_time"]) 

651 

652 def set_config_solver_calls( 

653 self: Settings, value: int = DEFAULT_config_solver_calls, 

654 origin: SettingState = SettingState.DEFAULT) -> None: 

655 """Set the number of solver calls.""" 

656 section = "configuration" 

657 name = "solver_calls" 

658 

659 if value is not None and self.__check_setting_state( 

660 self.__config_solver_calls_set, origin, name): 

661 self.__init_section(section) 

662 self.__config_solver_calls_set = origin 

663 self.__settings[section][name] = str(value) 

664 

665 def get_config_solver_calls(self: Settings) -> int | None: 

666 """Return the number of solver calls.""" 

667 if self.__config_solver_calls_set == SettingState.NOT_SET: 

668 self.set_config_solver_calls() 

669 return None 

670 

671 return int(self.__settings["configuration"]["solver_calls"]) 

672 

673 def set_config_number_of_runs( 

674 self: Settings, value: int = DEFAULT_config_number_of_runs, 

675 origin: SettingState = SettingState.DEFAULT) -> None: 

676 """Set the number of configuration runs.""" 

677 section = "configuration" 

678 name = "number_of_runs" 

679 

680 if value is not None and self.__check_setting_state( 

681 self.__config_number_of_runs_set, origin, name): 

682 self.__init_section(section) 

683 self.__config_number_of_runs_set = origin 

684 self.__settings[section][name] = str(value) 

685 

686 def get_config_number_of_runs(self: Settings) -> int: 

687 """Return the number of configuration runs.""" 

688 if self.__config_number_of_runs_set == SettingState.NOT_SET: 

689 self.set_config_number_of_runs() 

690 

691 return int(self.__settings["configuration"]["number_of_runs"]) 

692 

693 # Configuration: SMAC specific settings ### 

694 

695 def set_configurator_target_cutoff_length( 

696 self: Settings, value: str = DEFAULT_configurator_target_cutoff_length, 

697 origin: SettingState = SettingState.DEFAULT) -> None: 

698 """Set the target algorithm cutoff length.""" 

699 section = "configuration" 

700 name = "target_cutoff_length" 

701 

702 if value is not None and self.__check_setting_state( 

703 self.__configurator_target_cutoff_length_set, origin, name): 

704 self.__init_section(section) 

705 self.__configurator_target_cutoff_length_set = origin 

706 self.__settings[section][name] = str(value) 

707 

708 def get_configurator_target_cutoff_length(self: Settings) -> str: 

709 """Return the target algorithm cutoff length.""" 

710 if self.__configurator_target_cutoff_length_set == SettingState.NOT_SET: 

711 self.set_configurator_target_cutoff_length() 

712 return self.__settings["configuration"]["target_cutoff_length"] 

713 

714 # Slurm settings ### 

715 

716 def set_slurm_max_parallel_runs_per_node( 

717 self: Settings, 

718 value: int = DEFAULT_slurm_max_parallel_runs_per_node, 

719 origin: SettingState = SettingState.DEFAULT) -> None: 

720 """Set the number of algorithms Slurm can run in parallel per node.""" 

721 section = "slurm" 

722 name = "max_parallel_runs_per_node" 

723 

724 if value is not None and self.__check_setting_state( 

725 self.__slurm_max_parallel_runs_per_node_set, origin, name): 

726 self.__init_section(section) 

727 self.__slurm_max_parallel_runs_per_node_set = origin 

728 self.__settings[section][name] = str(value) 

729 

730 def get_slurm_max_parallel_runs_per_node(self: Settings) -> int: 

731 """Return the number of algorithms Slurm can run in parallel per node.""" 

732 if self.__slurm_max_parallel_runs_per_node_set == SettingState.NOT_SET: 

733 self.set_slurm_max_parallel_runs_per_node() 

734 

735 return int(self.__settings["slurm"]["max_parallel_runs_per_node"]) 

736 

737 # SLURM extra options 

738 

739 def add_slurm_extra_option(self: Settings, name: str, value: str, 

740 origin: SettingState = SettingState.DEFAULT) -> None: 

741 """Add additional Slurm options.""" 

742 section = "slurm_extra" 

743 

744 current_state = (self.__slurm_extra_options_set[name] 

745 if name in self.__slurm_extra_options_set 

746 else SettingState.NOT_SET) 

747 

748 if value is not None and self.__check_setting_state(current_state, origin, name): 

749 self.__init_section(section) 

750 self.__slurm_extra_options_set[name] = origin 

751 self.__settings[section][name] = str(value) 

752 

753 def get_slurm_extra_options(self: Settings, 

754 as_args: bool = False) -> dict | list: 

755 """Return a dict with additional Slurm options.""" 

756 section = "slurm_extra" 

757 options = dict() 

758 

759 if "slurm_extra" in self.__settings.sections(): 

760 for option in self.__settings["slurm_extra"]: 

761 options[option] = self.__settings.get(section, option) 

762 if as_args: 

763 return [f"--{key}={options[key]}" for key in options.keys()] 

764 return options 

765 

766 # Ablation settings ### 

767 

768 def set_ablation_racing_flag(self: Settings, value: bool = DEFAULT_ablation_racing, 

769 origin: SettingState = SettingState.DEFAULT) -> None: 

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

771 section = "ablation" 

772 name = "racing" 

773 

774 if value is not None and self.__check_setting_state( 

775 self.__ablation_racing_flag_set, origin, name): 

776 self.__init_section(section) 

777 self.__ablation_racing_flag_set = origin 

778 self.__settings[section][name] = str(value) 

779 

780 def get_ablation_racing_flag(self: Settings) -> bool: 

781 """Return a bool indicating whether the racing flag is set for ablation.""" 

782 if self.__ablation_racing_flag_set == SettingState.NOT_SET: 

783 self.set_ablation_racing_flag() 

784 

785 return bool(self.__settings["ablation"]["racing"]) 

786 

787 # Parallel Portfolio settings 

788 

789 def set_parallel_portfolio_check_interval( 

790 self: Settings, 

791 value: int = DEFAULT_parallel_portfolio_check_interval, 

792 origin: SettingState = SettingState.DEFAULT) -> None: 

793 """Set the parallel portfolio check interval.""" 

794 section = "parallel_portfolio" 

795 name = "check_interval" 

796 

797 if value is not None and self.__check_setting_state( 

798 self.__parallel_portfolio_check_interval_set, origin, name): 

799 self.__init_section(section) 

800 self.__parallel_portfolio_check_interval_set = origin 

801 self.__settings[section][name] = str(value) 

802 

803 def get_parallel_portfolio_check_interval(self: Settings) -> int: 

804 """Return the parallel portfolio check interval.""" 

805 if self.__parallel_portfolio_check_interval_set == SettingState.NOT_SET: 

806 self.set_parallel_portfolio_check_interval() 

807 

808 return int( 

809 self.__settings["parallel_portfolio"]["check_interval"]) 

810 

811 def set_parallel_portfolio_number_of_seeds_per_solver( 

812 self: Settings, 

813 value: int = DEFAULT_parallel_portfolio_num_seeds_per_solver, 

814 origin: SettingState = SettingState.DEFAULT) -> None: 

815 """Set the parallel portfolio seeds per solver to start.""" 

816 section = "parallel_portfolio" 

817 name = "num_seeds_per_solver" 

818 

819 if value is not None and self.__check_setting_state( 

820 self.__parallel_portfolio_num_seeds_per_solver_set, origin, name): 

821 self.__init_section(section) 

822 self.__parallel_portfolio_num_seeds_per_solver_set = origin 

823 self.__settings[section][name] = str(value) 

824 

825 def get_parallel_portfolio_number_of_seeds_per_solver(self: Settings) -> int: 

826 """Return the parallel portfolio seeds per solver to start.""" 

827 if self.__parallel_portfolio_num_seeds_per_solver_set == SettingState.NOT_SET: 

828 self.set_parallel_portfolio_number_of_seeds_per_solver() 

829 

830 return int( 

831 self.__settings["parallel_portfolio"]["num_seeds_per_solver"]) 

832 

833 def set_run_on(self: Settings, value: Runner = str, 

834 origin: SettingState = SettingState.DEFAULT) -> None: 

835 """Set the compute on which to run.""" 

836 section = "general" 

837 name = "run_on" 

838 

839 if value is not None and self.__check_setting_state( 

840 self.__run_on_set, origin, name): 

841 self.__init_section(section) 

842 self.__run_on_set = origin 

843 self.__settings[section][name] = value 

844 

845 def get_run_on(self: Settings) -> Runner: 

846 """Return the compute on which to run.""" 

847 return Runner(self.__settings["general"]["run_on"]) 

848 

849 @staticmethod 

850 def check_settings_changes(cur_settings: Settings, prev_settings: Settings) -> bool: 

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

852 

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

854 

855 Args: 

856 cur_settings: The current settings 

857 prev_settings: The previous settings 

858 

859 Returns: 

860 True iff there are no changes. 

861 """ 

862 cur_dict = cur_settings.__settings._sections 

863 prev_dict = prev_settings.__settings._sections 

864 

865 cur_sections_set = set(cur_dict.keys()) 

866 prev_sections_set = set(prev_dict.keys()) 

867 

868 sections_removed = prev_sections_set - cur_sections_set 

869 if sections_removed: 

870 print("Warning: the following sections have been removed:") 

871 for section in sections_removed: 

872 print(f" - Section '{section}'") 

873 

874 sections_added = cur_sections_set - prev_sections_set 

875 if sections_added: 

876 print("Warning: the following sections have been added:") 

877 for section in sections_added: 

878 print(f" - Section '{section}'") 

879 

880 sections_remained = cur_sections_set & prev_sections_set 

881 option_changed = False 

882 for section in sections_remained: 

883 printed_section = False 

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

885 for name in names: 

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

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

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

889 if cur_val != prev_val: 

890 # Have we printed the initial warning? 

891 if not option_changed: 

892 print("Warning: The following attributes/options have changed:") 

893 option_changed = True 

894 

895 # do we have yet to print the section? 

896 if not printed_section: 

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

898 printed_section = True 

899 

900 # print actual change 

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

902 

903 return not (sections_removed or sections_added or option_changed)