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

1from dataclasses import dataclass 

2from typing import Optional 

3 

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 

11 

12 

13class AddResourceActionParamsType(ModifyResourceBaseActionParamsType): 

14 """Parameter for `AddResourceAction`.""" 

15 

16 

17@dataclass(frozen=True) 

18class AddResourceAction(ModifyResourceBaseAction, str=False): 

19 """`AddResourceAction` will add (clone) a resource. 

20 

21 This action is based on the original optimos implementation, 

22 see `resolve_add_resources_in_process` in that project. 

23 

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) 

38 

39 """ 

40 

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 ) 

115 

116 return 

117 

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. 

126 

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) 

134 

135 if resource is None: 

136 return None 

137 

138 task_executions = evaluation.get_task_execution_count_by_resource(resource_id) 

139 

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 ] 

145 

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