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
« 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
6from o2.models.legacy_approach import LegacyApproach
9class AgentType(Enum):
10 """The type of agent to use for the optimization task."""
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"
20class CostType(Enum):
21 """The type of cost to use for the optimization task."""
23 TOTAL_COST = "total_cost"
24 """The total cost, including fixed costs per task / batch
25 and resource costs (e.g. hourly wages)."""
27 RESOURCE_COST = "resource_cost"
28 """The resource cost, excluding fixed costs per task / batch."""
30 FIXED_COST = "fixed_cost"
31 """The fixed cost per task / batch. No resource costs."""
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"""
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"""
42class ActionVariationSelection(Enum):
43 """The method to use for selecting action variations."""
45 SINGLE_RANDOM = "single_random"
46 """Select a single random rule"""
48 FIRST_IN_ORDER = "first_in_order"
49 """Select the first rule in the list"""
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
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 """
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
61 NOTE: For inner loops this will only pick 1 rule, so that the constraints are not broken.
62 """
64 ALL_RANDOM = "all_random"
65 """Select all rules randomly"""
67 ALL_IN_ORDER = "all_in_order"
68 """Select all rules in order"""
70 @property
71 def ordered(self) -> "ActionVariationSelection":
72 """Return the selection enum, that should be used for ordered selection.
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
87 @property
88 def inner(self) -> "ActionVariationSelection":
89 """Return the selection enum, that should be used for inner selection.
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
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
117@dataclass()
118class Settings:
119 """Settings for the Optimos v2 application.
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 """
125 agent: AgentType = AgentType.TABU_SEARCH
126 """The agent to use for the optimization task."""
128 max_non_improving_actions = 1000
129 """The maximum number of actions before discarding a base solution.
131 Non-improving actions are all actions, which solutions are not dominating the
132 current Pareto Front.
133 """
135 iterations_per_solution: Optional[int] = None
136 """The number of iterations to run for each base solution.
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 """
143 max_iterations = 1000
144 """The maximum (total) number of iterations before the application stops."""
146 max_solutions: Optional[int] = None
147 """The maximum number of solutions to evaluate.
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 """
153 optimos_legacy_mode = False
154 """Should this application behave like an approximation of the original OPTIMOS?"""
156 batching_only = False
157 """Should only batching rules be optimized?"""
159 only_allow_low_last = False
160 """Should `low` rated Actions be tried last?
162 E.g. ONLY if no other/higher Action is available.
163 """
165 print_chosen_actions = False
166 """Should the chosen actions be printed?
168 This is useful for debugging, but can be very verbose."""
170 legacy_approach: LegacyApproach = LegacyApproach.CALENDAR_FIRST
171 """
172 This Setting is used to simulate different approaches used by legacy optimos.
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.
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 """
189 max_number_of_actions_per_iteration = os.cpu_count() or 1
190 """The maximum number of actions to select for for (parallel) evaluation."""
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.
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 """
201 never_select_new_base_solution = False
202 """Disables the logic to automatically select a new base solution.
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 """
208 error_radius_in_percent: Optional[float] = 0.02
209 """This is the "error" for the hill climbing agent to still consider a solution.
211 Also this will be used to calculate the sa_cooling_factor if this is set to "auto".
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 """
217 sa_cooling_factor: Union[float, Literal["auto"]] = "auto"
218 """The cooling factor for the simulated annealing agent.
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 """
226 sa_cooling_iteration_percent = 0.55
227 """Percentage of iterations after which the SA should basically be hill climbing.
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 """
234 sa_initial_temperature: Union[float, Literal["auto"]] = "auto"
235 """The initial temperature for the simulated annealing agent."""
237 sa_strict_ordered = False
238 """Should the SA agent be strict ordered?
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.
244 Also setting this to True will disable the random acceptance of a bad solutions.
245 """
247 ppo_model_path = "models/ppo_maskable-20241025-075307"
248 """The path to the PPO model to use for the PPO agent."""
250 ppo_use_existing_model = False
251 """Should the PPO agent use an existing model?"""
253 ppo_steps_per_iteration = 50
254 """The number of steps per iteration for the PPO agent."""
256 log_to_tensor_board = False
257 """Should the evaluation be logged to TensorBoard?"""
259 throw_on_iteration_errors = False
260 """Should the application throw an error if an iteration fails?
262 Useful for debugging, but should be disabled for production.
263 """
265 disable_action_validity_check = False
266 """Disables the logic to check if actions produces sensible results, before actually evaluating them.
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 """
272 action_variation_selection: ActionVariationSelection = ActionVariationSelection.ALL_IN_ORDER
273 """The method to use for selecting action variations.
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).
282 See the ActionVariationSelection and max_variants_per_action for more details.
283 """
285 max_variants_per_action: Optional[int] = None
286 """The maximum number of variants per action.
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.
293 NOTE: This number is _not_ per iteration, but per base_solution.
294 """
296 override_action_variation_selection_for_inner_loop: bool = False
297 """Should the `inner` parameter of the action variation selection be ignored?
299 Only activate if you know what you are doing!
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 """
306 DISABLE_PARALLEL_EVALUATION: ClassVar[bool] = False
307 """Should the parallel evaluation be disabled? This is useful for debugging.
309 This will override any MAX_THREADS_ACTION_EVALUATION/MAX_THREADS_MEDIAN_CALCULATION settings.
310 """
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."""
316 MAX_THREADS_MEDIAN_CALCULATION: ClassVar[int] = 1
317 """The maximum number of threads to use for parallel median calculation.
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 """
323 MAX_YIELDS_PER_ACTION: ClassVar[Optional[int]] = None
324 """The maximum number of yields per action.
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 """
331 ADD_SIZE_RULE_TO_NEW_RULES: ClassVar[bool] = True
332 """Should a size rule be added to new rules?
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 """
339 SHOW_SIMULATION_ERRORS: ClassVar[bool] = False
340 """Should the simulation errors be shown?
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 """
346 RAISE_SIMULATION_ERRORS: ClassVar[bool] = False
347 """Should the simulation errors be raised?
349 This is useful for debugging, but should be disabled for production.
350 """
352 DUMP_DISCARDED_SOLUTIONS: ClassVar[bool] = False
353 """Should the solutions be dumped (to file system via pickle) after they have been dominated?
355 This should only be activated for evaluation, as it will cause IO & processing overhead.
356 """
358 COST_TYPE: ClassVar[CostType] = CostType.AVG_WT_AND_PT_PER_TASK_INSTANCE
359 """The type of cost to use for the optimization task.
361 Because this won't differ during the optimization, it's a class variable.
362 """
364 NUMBER_OF_CASES: Optional[int] = None
365 """Override the number of cases to simulate per run.
367 Will use information from the model if not set (1000 cases by default)."""
369 EQUAL_DOMINATION_ALLOWED: ClassVar[bool] = False
370 """Should equal domination be allowed?
372 If this is set to True, the pareto front will allow solutions with the same
373 x and y values.
374 """
376 LOG_LEVEL: ClassVar[str] = "DEBUG"
377 """The log level to use for the application."""
379 LOG_FILE: ClassVar[Optional[str]] = None
380 """The log file to use for the application."""
382 ARCHIVE_TENSORBOARD_LOGS: ClassVar[bool] = True
383 """Should the tensorboard logs be archived?
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 """
389 ARCHIVE_SOLUTIONS: ClassVar[bool] = False
390 """Should the solutions' evaluations and states be archived?
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 """
397 DELETE_LOADED_SOLUTION_ARCHIVES: ClassVar[bool] = True
398 """If an archived solution is loaded, should it be deleted?
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 """
404 OVERWRITE_EXISTING_SOLUTION_ARCHIVES: ClassVar[bool] = True
405 """If an archived solution is loaded, should it be overwritten?
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 """
411 CHECK_FOR_TIMETABLE_EQUALITY: ClassVar[bool] = False
412 """Should the equality of timetables be checked when comparing solutions?
414 This includes unordered comparison of firing rules.
416 This should be enabled for analysis, but might be to slow for actual
417 optimization runs.
418 """
420 DISABLE_REMOVE_ACTION_RULE: ClassVar[bool] = True
421 """Should the remove action rule be disabled?
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.
427 But in practice it might make sense to disable this, as it might help the
428 optimizer to find a better solution.
429 """
431 NUMBER_OF_SIMULATION_FOR_MEDIAN: ClassVar[int] = 5
432 """The number of simulations to run for each new state.
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 """
438 USE_MEDIAN_SIMULATION_FOR_EVALUATION: ClassVar[bool] = True
439 """Should the median simulation be used for evaluation?
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 """
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}")
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"