Coverage for o2/actions/legacy_optimos_actions/add_resource_action.py: 87%
55 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
1from dataclasses import dataclass
2from typing import Optional
4from o2.actions.base_actions.base_action import RateSelfReturnType
5from o2.actions.base_actions.modify_resource_base_action import (
6 ModifyResourceBaseAction,
7 ModifyResourceBaseActionParamsType,
8)
9from o2.models.solution import Solution
10from o2.store import Store
13class AddResourceActionParamsType(ModifyResourceBaseActionParamsType):
14 """Parameter for `AddResourceAction`."""
17@dataclass(frozen=True)
18class AddResourceAction(ModifyResourceBaseAction, str=False):
19 """`AddResourceAction` will add (clone) a resource.
21 This action is based on the original optimos implementation,
22 see `resolve_add_resources_in_process` in that project.
24 It first gets all tasks sorted by the number of task instances,
25 that have either a waiting or idle time (descending).
26 For each task, it gets the resource_profile for that task, and then:
27 - If the profile contains only one resource...
28 - ...which in turn has only one task assigned, it will clone that resource.
29 - ...which has more than one task assigned, it will instead try to remove the
30 least done task from the resource. (Only tasks, that are executed more than once
31 by it and are also done by other resources, are considered).
32 This should give the resource more time to do the "problem" task.
33 - Else if the profile contains more than one resource, iterate over those resources
34 sorted by number of times the task is done by the resource (desc).
35 - If the resource has only one task assigned, it will clone the resource.
36 - If the resource has more than one task assigned, it will try to remove the
37 least done (other) task from the resource. (see above)
39 """
41 @staticmethod
42 def rate_self(store: Store, input: "Solution") -> RateSelfReturnType:
43 """Generate a best set of parameters & self-evaluates this action."""
44 parent_evaluation = input.evaluation
45 timetable = store.solution.state.timetable
46 tasks = parent_evaluation.get_tasks_sorted_by_occurrences_of_wt_and_it()
47 for task in tasks:
48 resource_profile = timetable.get_resource_profile(task)
49 if resource_profile is None:
50 continue
51 if len(resource_profile.resource_list) == 1:
52 resource = resource_profile.resource_list[0]
53 if len(resource.assigned_tasks) == 0:
54 continue
55 if len(resource.assigned_tasks) == 1:
56 yield (
57 AddResourceAction.get_default_rating(store),
58 AddResourceAction(
59 AddResourceActionParamsType(
60 resource_id=resource.id,
61 task_id=task,
62 clone_resource=True,
63 )
64 ),
65 )
66 else:
67 least_done_task = AddResourceAction._find_least_done_task_to_remove(
68 store, input, resource.id, task
69 )
70 if least_done_task is not None:
71 yield (
72 AddResourceAction.get_default_rating(store),
73 AddResourceAction(
74 AddResourceActionParamsType(
75 resource_id=resource.id,
76 task_id=least_done_task,
77 remove_task_from_resource=True,
78 )
79 ),
80 )
81 else:
82 sorted_resources = parent_evaluation.get_resources_sorted_by_task_execution_count(task)
83 for resource_id in sorted_resources:
84 resource = timetable.get_resource(resource_id)
85 if resource is None:
86 continue
87 if len(resource.assigned_tasks) == 0:
88 continue
89 if len(resource.assigned_tasks) == 1:
90 yield (
91 AddResourceAction.get_default_rating(store),
92 AddResourceAction(
93 AddResourceActionParamsType(
94 resource_id=resource.id,
95 task_id=task,
96 clone_resource=True,
97 )
98 ),
99 )
100 else:
101 least_done_task = AddResourceAction._find_least_done_task_to_remove(
102 store, input, resource.id, task
103 )
104 if least_done_task is not None:
105 yield (
106 AddResourceAction.get_default_rating(store),
107 AddResourceAction(
108 AddResourceActionParamsType(
109 resource_id=resource.id,
110 task_id=least_done_task,
111 remove_task_from_resource=True,
112 )
113 ),
114 )
116 return
118 @staticmethod
119 def _find_least_done_task_to_remove(
120 store: Store,
121 input: "Solution",
122 resource_id: str,
123 protected_task: str,
124 ) -> Optional[str]:
125 """Find the least done task to remove from the resource.
127 Only tasks, that are executed more than once by the resource and
128 are also done by other resources, are considered.
129 Of course the task must differ from the protected task.
130 """
131 timetable = store.solution.state.timetable
132 evaluation = input.evaluation
133 resource = timetable.get_resource(resource_id)
135 if resource is None:
136 return None
138 task_executions = evaluation.get_task_execution_count_by_resource(resource_id)
140 task_candidates = [
141 (timetable.get_resource_profile(task), task_executions.get(task, 0))
142 for task in resource.assigned_tasks
143 if task != protected_task and task_executions.get(task, 0) > 1
144 ]
146 if not task_candidates:
147 return None
148 least_done_task, _ = min(task_candidates, key=lambda x: x[1])
149 if least_done_task is None:
150 return None
151 return least_done_task.id