Coverage for o2/models/settings.py: 97%

161 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-05-16 11:18 +0000

1import os 

2from dataclasses import dataclass 

3from enum import Enum 

4from typing import ClassVar, Literal, Optional, Union 

5 

6from o2.models.legacy_approach import LegacyApproach 

7 

8 

9class AgentType(Enum): 

10 """The type of agent to use for the optimization task.""" 

11 

12 TABU_SEARCH = "tabu_search" 

13 SIMULATED_ANNEALING = "simulated_annealing" 

14 PROXIMAL_POLICY_OPTIMIZATION = "proximal_policy_optimization" 

15 TABU_SEARCH_RANDOM = "tabu_search_random" 

16 SIMULATED_ANNEALING_RANDOM = "simulated_annealing_random" 

17 PROXIMAL_POLICY_OPTIMIZATION_RANDOM = "proximal_policy_optimization_random" 

18 

19 

20class CostType(Enum): 

21 """The type of cost to use for the optimization task.""" 

22 

23 TOTAL_COST = "total_cost" 

24 """The total cost, including fixed costs per task / batch 

25 and resource costs (e.g. hourly wages).""" 

26 

27 RESOURCE_COST = "resource_cost" 

28 """The resource cost, excluding fixed costs per task / batch.""" 

29 

30 FIXED_COST = "fixed_cost" 

31 """The fixed cost per task / batch. No resource costs.""" 

32 

33 WAITING_TIME_AND_PROCESSING_TIME = "wt_pt" 

34 """Instead of using an financial cost use 

35 waiting time (incl. idle time) and processing time""" 

36 

37 AVG_WT_AND_PT_PER_TASK_INSTANCE = "avg_wt_pt_per_task_instance" 

38 """Instead of using an financial cost use average 

39 waiting time (incl. idle time) and processing time per task instance""" 

40 

41 

42class ActionVariationSelection(Enum): 

43 """The method to use for selecting action variations.""" 

44 

45 SINGLE_RANDOM = "single_random" 

46 """Select a single random rule""" 

47 

48 FIRST_IN_ORDER = "first_in_order" 

49 """Select the first rule in the list""" 

50 

51 FIRST_MAX_VARIANTS_PER_ACTION_IN_ORDER = "first_max_variants_per_action_in_order" 

52 """Select the max_variants_per_action first rules in the list 

53 

54 E.g. if max_variants_per_action=3 you will get the first 3 rules. 

55 NOTE: For inner loops this will only pick 1 rule, so that the constraints are not broken. 

56 """ 

57 

58 RANDOM_MAX_VARIANTS_PER_ACTION = "random_max_variants_per_action" 

59 """Select a random rule, but only consider the best max_variants_per_action rules 

60 

61 NOTE: For inner loops this will only pick 1 rule, so that the constraints are not broken. 

62 """ 

63 

64 ALL_RANDOM = "all_random" 

65 """Select all rules randomly""" 

66 

67 ALL_IN_ORDER = "all_in_order" 

68 """Select all rules in order""" 

69 

70 @property 

71 def ordered(self) -> "ActionVariationSelection": 

72 """Return the selection enum, that should be used for ordered selection. 

73 

74 This is used, when the action variations are already ordered, so random selection is not needed. 

75 Meaning: For random selection, this will return the enum with non-random selection. 

76 For the other cases, it will return the same enum. 

77 """ 

78 if self == ActionVariationSelection.SINGLE_RANDOM: 

79 return ActionVariationSelection.FIRST_IN_ORDER 

80 elif self == ActionVariationSelection.RANDOM_MAX_VARIANTS_PER_ACTION: 

81 return ActionVariationSelection.FIRST_MAX_VARIANTS_PER_ACTION_IN_ORDER 

82 elif self == ActionVariationSelection.ALL_RANDOM: 

83 return ActionVariationSelection.ALL_IN_ORDER 

84 else: 

85 return self 

86 

87 @property 

88 def inner(self) -> "ActionVariationSelection": 

89 """Return the selection enum, that should be used for inner selection. 

90 

91 This is used in the inner loop, where we only want to select one rule. 

92 Meaning: For multi-selection, this will return the enum with single selection. 

93 For single selection, it will return the same enum. 

94 """ 

95 if self == ActionVariationSelection.ALL_IN_ORDER: 

96 return ActionVariationSelection.FIRST_IN_ORDER 

97 elif self == ActionVariationSelection.ALL_RANDOM: 

98 return ActionVariationSelection.SINGLE_RANDOM 

99 elif self == ActionVariationSelection.FIRST_MAX_VARIANTS_PER_ACTION_IN_ORDER: 

100 return ActionVariationSelection.FIRST_IN_ORDER 

101 elif self == ActionVariationSelection.RANDOM_MAX_VARIANTS_PER_ACTION: 

102 return ActionVariationSelection.SINGLE_RANDOM 

103 else: 

104 return self 

105 

106 @property 

107 def infinite_max_variants(self) -> "ActionVariationSelection": 

108 """Choice if the max variants is infinite.""" 

109 if self == ActionVariationSelection.FIRST_MAX_VARIANTS_PER_ACTION_IN_ORDER: 

110 return ActionVariationSelection.ALL_IN_ORDER 

111 elif self == ActionVariationSelection.RANDOM_MAX_VARIANTS_PER_ACTION: 

112 return ActionVariationSelection.ALL_RANDOM 

113 else: 

114 return self 

115 

116 

117@dataclass() 

118class Settings: 

119 """Settings for the Optimos v2 application. 

120 

121 This class is initialized with sensible defaults, but can be changed to 

122 suit the needs of the user, e.g. to run it in legacy optimos mode. 

123 """ 

124 

125 agent: AgentType = AgentType.TABU_SEARCH 

126 """The agent to use for the optimization task.""" 

127 

128 max_non_improving_actions = 1000 

129 """The maximum number of actions before discarding a base solution. 

130 

131 Non-improving actions are all actions, which solutions are not dominating the 

132 current Pareto Front. 

133 """ 

134 

135 iterations_per_solution: Optional[int] = None 

136 """The number of iterations to run for each base solution. 

137 

138 If this is set, the optimizer will run for this number of iterations for each 

139 base solution, before selecting a new one. If this is not set, the optimizer 

140 will run until the max_iterations is reached. 

141 """ 

142 

143 max_iterations = 1000 

144 """The maximum (total) number of iterations before the application stops.""" 

145 

146 max_solutions: Optional[int] = None 

147 """The maximum number of solutions to evaluate. 

148 

149 If this is set, the optimizer will stop after this number of solutions has been evaluated. 

150 Often, rather then setting this, it's better to set max_iterations. 

151 """ 

152 

153 optimos_legacy_mode = False 

154 """Should this application behave like an approximation of the original OPTIMOS?""" 

155 

156 batching_only = False 

157 """Should only batching rules be optimized?""" 

158 

159 only_allow_low_last = False 

160 """Should `low` rated Actions be tried last? 

161 

162 E.g. ONLY if no other/higher Action is available. 

163 """ 

164 

165 print_chosen_actions = False 

166 """Should the chosen actions be printed? 

167 

168 This is useful for debugging, but can be very verbose.""" 

169 

170 legacy_approach: LegacyApproach = LegacyApproach.CALENDAR_FIRST 

171 """ 

172 This Setting is used to simulate different approaches used by legacy optimos. 

173 

174 While _FIRST / _ONLY are self explanatory, the the _NEXT aka combined mode is a bit more complex. 

175 The combined mode is a mode where the calendar and resources are optimized at the same time. 

176 Legacy Optimos will first try calendar and then resources, if the calendar optimization 

177 finds a result, the resources are still optimized. 

178 

179 To reproduce this in the new Optimos, we have to do the following: 

180 - We initialize the setting with calendar first, 

181 - In this case calendar optimizations will get a higher priority, 

182 the resource optimizations will get a "low" priority, that will only(!) 

183 be executed after the calendar optimization 

184 - Then as soon as one of the calendar optimizations is successful, 

185 we switch this setting to resources first, so that the resources 

186 are optimized first, and the calendar is optimized afterwards. 

187 """ 

188 

189 max_number_of_actions_per_iteration = os.cpu_count() or 1 

190 """The maximum number of actions to select for for (parallel) evaluation.""" 

191 

192 max_distance_to_new_base_solution = float("inf") 

193 """The max distance to the new base solution to be considered for TABU evaluation. 

194 With distance being the min euclidean distance to any solution in 

195 the current Pareto Front. 

196 

197 NOTE: This is an absolute number. You most likely want to use 

198 error_radius_in_percent instead, to limit the distance to the new base solution. 

199 """ 

200 

201 never_select_new_base_solution = False 

202 """Disables the logic to automatically select a new base solution. 

203 

204 This is useful if you want to go greedy without backing off to a previous 

205 base solution.E.g. in the PPO Training. 

206 """ 

207 

208 error_radius_in_percent: Optional[float] = 0.02 

209 """This is the "error" for the hill climbing agent to still consider a solution. 

210 

211 Also this will be used to calculate the sa_cooling_factor if this is set to "auto". 

212 

213 When set the Tabu-Search will behave similar to what is commonly called hill climbing. 

214 (Even though a Tabu-List is still maintained) 

215 """ 

216 

217 sa_cooling_factor: Union[float, Literal["auto"]] = "auto" 

218 """The cooling factor for the simulated annealing agent. 

219 

220 It's a float between 0 and 1 that will be multiplied with the temperature 

221 every iteration. If this is set to "auto", the cooling factor will be 

222 calculated so that after sa_cooling_iteration_percent the 

223 temperature will be error_radius_in_percent 

224 """ 

225 

226 sa_cooling_iteration_percent = 0.55 

227 """Percentage of iterations after which the SA should basically be hill climbing. 

228 

229 This is only relevant if sa_cooling_factor is set to "auto". 

230 NOTE: Reaching this threshold will not stop the cooling process. 

231 It's only used to calculate the cooling factor. 

232 """ 

233 

234 sa_initial_temperature: Union[float, Literal["auto"]] = "auto" 

235 """The initial temperature for the simulated annealing agent.""" 

236 

237 sa_strict_ordered = False 

238 """Should the SA agent be strict ordered? 

239 

240 If this is set to True, the SA agent will take solutions in temperature range ordered by their distance 

241 to the current pareto front (Similar to the hill climbing approach). This is not strictly SA, because that 

242 would require a random selection of the solutions, but it will speed up the optimization. 

243 

244 Also setting this to True will disable the random acceptance of a bad solutions. 

245 """ 

246 

247 ppo_model_path = "models/ppo_maskable-20241025-075307" 

248 """The path to the PPO model to use for the PPO agent.""" 

249 

250 ppo_use_existing_model = False 

251 """Should the PPO agent use an existing model?""" 

252 

253 ppo_steps_per_iteration = 50 

254 """The number of steps per iteration for the PPO agent.""" 

255 

256 log_to_tensor_board = False 

257 """Should the evaluation be logged to TensorBoard?""" 

258 

259 throw_on_iteration_errors = False 

260 """Should the application throw an error if an iteration fails? 

261 

262 Useful for debugging, but should be disabled for production. 

263 """ 

264 

265 disable_action_validity_check = False 

266 """Disables the logic to check if actions produces sensible results, before actually evaluating them. 

267 

268 This is used by the Random Agents, as they should be able to select any action, 

269 even if it's not valid. (The validity check is considered to be part of the metrics) 

270 """ 

271 

272 action_variation_selection: ActionVariationSelection = ActionVariationSelection.ALL_IN_ORDER 

273 """The method to use for selecting action variations. 

274 

275 Context: 

276 Actions usually have a sorted component and then an options component. 

277 E.g. the ModifySizeRuleByCostAction will first select the task with the highest cost, 

278 and then get a list of all size rules for that task. Now the question is, how to select 

279 the rule(s) which should be modified (the "variants"). This setting will set the method how to select 

280 the variant(s). 

281 

282 See the ActionVariationSelection and max_variants_per_action for more details. 

283 """ 

284 

285 max_variants_per_action: Optional[int] = None 

286 """The maximum number of variants per action. 

287 

288 This can be used esp. in many-task/many-resource scenarios, where 

289 the number of possible actions is very high. It limits the number of variants 

290 per action. With a variant being a specific sub-setting of an action, e.g. a specific 

291 size rule or a specific resource for a task. 

292 

293 NOTE: This number is _not_ per iteration, but per base_solution. 

294 """ 

295 

296 override_action_variation_selection_for_inner_loop: bool = False 

297 """Should the `inner` parameter of the action variation selection be ignored? 

298 

299 Only activate if you know what you are doing! 

300 

301 It's useful if you want to force the optimizer to output EVERY variant. 

302 Usually this is not needed, as we can rely on randomness,e.g via `ActionVariationSelection.ALL_RANDOM`, 

303 but if you want a deterministic output, you can set this to True (E.g. for testing). 

304 """ 

305 

306 DISABLE_PARALLEL_EVALUATION: ClassVar[bool] = False 

307 """Should the parallel evaluation be disabled? This is useful for debugging. 

308 

309 This will override any MAX_THREADS_ACTION_EVALUATION/MAX_THREADS_MEDIAN_CALCULATION settings. 

310 """ 

311 

312 MAX_THREADS_ACTION_EVALUATION: ClassVar[int] = os.cpu_count() or 1 

313 """The maximum number of threads to use for parallel evaluation 

314 of actions.""" 

315 

316 MAX_THREADS_MEDIAN_CALCULATION: ClassVar[int] = 1 

317 """The maximum number of threads to use for parallel median calculation. 

318 

319 If you are already using parallel evaluation of actions, you might want to keep this at 1, 

320 not to have to much processes running at the same time. 

321 """ 

322 

323 MAX_YIELDS_PER_ACTION: ClassVar[Optional[int]] = None 

324 """The maximum number of yields per action. 

325 

326 This will be enforced even over multiple iterations (as long as the base_solution 

327 is not changed). Usually it doesn't make sense to set this, as the number of solutions can be controlled 

328 by the max_number_of_actions_per_iteration. 

329 """ 

330 

331 ADD_SIZE_RULE_TO_NEW_RULES: ClassVar[bool] = True 

332 """Should a size rule be added to new rules? 

333 

334 This is esp. relevant for date time rules, as it allows the optimizer, 

335 very easily to later increase / decrease it. But as Prosimos isn't quite 

336 behaving the same with a size rule, this may be disabled by the user. 

337 """ 

338 

339 SHOW_SIMULATION_ERRORS: ClassVar[bool] = False 

340 """Should the simulation errors be shown? 

341 

342 Most of the time this is not needed, as the errors might just indicate an invalid 

343 state, which will just result in the action not being chosen. 

344 """ 

345 

346 RAISE_SIMULATION_ERRORS: ClassVar[bool] = False 

347 """Should the simulation errors be raised? 

348 

349 This is useful for debugging, but should be disabled for production. 

350 """ 

351 

352 DUMP_DISCARDED_SOLUTIONS: ClassVar[bool] = False 

353 """Should the solutions be dumped (to file system via pickle) after they have been dominated? 

354 

355 This should only be activated for evaluation, as it will cause IO & processing overhead. 

356 """ 

357 

358 COST_TYPE: ClassVar[CostType] = CostType.AVG_WT_AND_PT_PER_TASK_INSTANCE 

359 """The type of cost to use for the optimization task. 

360 

361 Because this won't differ during the optimization, it's a class variable. 

362 """ 

363 

364 NUMBER_OF_CASES: Optional[int] = None 

365 """Override the number of cases to simulate per run. 

366 

367 Will use information from the model if not set (1000 cases by default).""" 

368 

369 EQUAL_DOMINATION_ALLOWED: ClassVar[bool] = False 

370 """Should equal domination be allowed? 

371 

372 If this is set to True, the pareto front will allow solutions with the same 

373 x and y values. 

374 """ 

375 

376 LOG_LEVEL: ClassVar[str] = "DEBUG" 

377 """The log level to use for the application.""" 

378 

379 LOG_FILE: ClassVar[Optional[str]] = None 

380 """The log file to use for the application.""" 

381 

382 ARCHIVE_TENSORBOARD_LOGS: ClassVar[bool] = True 

383 """Should the tensorboard logs be archived? 

384 

385 This is useful to collect logs from multiple runs, but if the runs are 

386 run parallel, it might interfere with the runs. 

387 """ 

388 

389 ARCHIVE_SOLUTIONS: ClassVar[bool] = False 

390 """Should the solutions' evaluations and states be archived? 

391 

392 This is useful for large models, because keeping all solutions in memory 

393 might cause memory issues. When enabled, the solutions will be archived 

394 to the file system via pickle and loaded on demand. 

395 """ 

396 

397 DELETE_LOADED_SOLUTION_ARCHIVES: ClassVar[bool] = True 

398 """If an archived solution is loaded, should it be deleted? 

399 

400 This will save (some) disk space, but will be slower, as a solution might 

401 need to be deleted & rewritten to disk multiple times. 

402 """ 

403 

404 OVERWRITE_EXISTING_SOLUTION_ARCHIVES: ClassVar[bool] = True 

405 """If an archived solution is loaded, should it be overwritten? 

406 

407 This will save (some) disk space, but will be slower, as a solution might 

408 need to be deleted & rewritten to disk multiple times. 

409 """ 

410 

411 CHECK_FOR_TIMETABLE_EQUALITY: ClassVar[bool] = False 

412 """Should the equality of timetables be checked when comparing solutions? 

413 

414 This includes unordered comparison of firing rules. 

415 

416 This should be enabled for analysis, but might be to slow for actual 

417 optimization runs. 

418 """ 

419 

420 DISABLE_REMOVE_ACTION_RULE: ClassVar[bool] = True 

421 """Should the remove action rule be disabled? 

422 

423 Theoretically it doesn't make sense to remove an firing rule, as it was 

424 added at some point, so the evaluation without that action should have been 

425 done already. 

426 

427 But in practice it might make sense to disable this, as it might help the 

428 optimizer to find a better solution. 

429 """ 

430 

431 NUMBER_OF_SIMULATION_FOR_MEDIAN: ClassVar[int] = 5 

432 """The number of simulations to run for each new state. 

433 

434 Because of simulation variation/error, we run multiple simulations and take the median. 

435 It's recommended to set this to an odd number, to avoid ties. 

436 """ 

437 

438 USE_MEDIAN_SIMULATION_FOR_EVALUATION: ClassVar[bool] = True 

439 """Should the median simulation be used for evaluation? 

440 

441 Because of simulation variation/error, it's recommended to run multiple 

442 simulations and take the median. (See NUMBER_OF_SIMULATION_FOR_MEDIAN for more details) 

443 """ 

444 

445 @staticmethod 

446 def get_pareto_x_label() -> str: 

447 """Get the label for the x-axis (cost) of the pareto front.""" 

448 if Settings.COST_TYPE == CostType.WAITING_TIME_AND_PROCESSING_TIME: 

449 return "Processing Time" 

450 elif Settings.COST_TYPE == CostType.FIXED_COST: 

451 return "Fixed Cost" 

452 elif Settings.COST_TYPE == CostType.RESOURCE_COST: 

453 return "Resource Cost" 

454 elif Settings.COST_TYPE == CostType.TOTAL_COST: 

455 return "Total Cost" 

456 elif Settings.COST_TYPE == CostType.AVG_WT_AND_PT_PER_TASK_INSTANCE: 

457 return "Batch Processing per Task" 

458 else: 

459 raise ValueError(f"Unknown cost type: {Settings.COST_TYPE}") 

460 

461 @staticmethod 

462 def get_pareto_y_label() -> str: 

463 """Get the label for the y-axis (duration) of the pareto front.""" 

464 if Settings.COST_TYPE == CostType.WAITING_TIME_AND_PROCESSING_TIME: 

465 return "Waiting Time" 

466 elif Settings.COST_TYPE == CostType.AVG_WT_AND_PT_PER_TASK_INSTANCE: 

467 return "WT-Idle per Task" 

468 else: 

469 return "Total Duration"