Coverage for o2/pareto_front.py: 100%

115 statements  

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

1from enum import Enum 

2from typing import TYPE_CHECKING 

3 

4from o2.models.evaluation import Evaluation 

5 

6if TYPE_CHECKING: 

7 from o2.models.solution import Solution 

8 

9 

10class FRONT_STATUS(Enum): # noqa: N801 

11 """The status of a solution compared to a Pareto Front.""" 

12 

13 IS_DOMINATED = 1 

14 """If the front is dominated by the evaluation""" 

15 DOMINATES = 2 

16 """If the front dominates the evaluation""" 

17 IN_FRONT = 3 

18 """If the evaluation is in the front""" 

19 INVALID = 4 

20 """If the evaluation is invalid""" 

21 

22 

23class ParetoFront: 

24 """A set of solutions, where no solution is dominated by another solution.""" 

25 

26 def __init__(self) -> None: 

27 self.solutions: list[Solution] = [] 

28 """A list of solutions in the front. They follow the same order as the evaluations.""" 

29 

30 @property 

31 def size(self) -> int: 

32 """Return the number of solutions in the front.""" 

33 return len(self.solutions) 

34 

35 @property 

36 def avg_y(self) -> float: 

37 """Return the average y of the front.""" 

38 if not self.solutions: 

39 return 0 

40 return sum(s.pareto_y for s in self.solutions) / self.size 

41 

42 @property 

43 def avg_x(self) -> float: 

44 """Return the average x of the front.""" 

45 if not self.solutions: 

46 return 0 

47 return sum(s.pareto_x for s in self.solutions) / self.size 

48 

49 @property 

50 def median_y(self) -> float: 

51 """Return the median y of the front.""" 

52 if not self.solutions: 

53 return 0 

54 return sorted(s.pareto_y for s in self.solutions)[self.size // 2] 

55 

56 @property 

57 def median_x(self) -> float: 

58 """Return the median x of the front.""" 

59 if not self.solutions: 

60 return 0 

61 return sorted(s.pareto_x for s in self.solutions)[self.size // 2] 

62 

63 @property 

64 def min_y(self) -> float: 

65 """Return the minimum y of the front.""" 

66 if not self.solutions: 

67 return 0 

68 return min(s.pareto_y for s in self.solutions) 

69 

70 @property 

71 def min_x(self) -> float: 

72 """Return the minimum x of the front.""" 

73 if not self.solutions: 

74 return 0 

75 return min(s.pareto_x for s in self.solutions) 

76 

77 @property 

78 def max_y(self) -> float: 

79 """Return the maximum y of the front.""" 

80 if not self.solutions: 

81 return 0 

82 return max(s.pareto_y for s in self.solutions) 

83 

84 @property 

85 def max_x(self) -> float: 

86 """Return the maximum x of the front.""" 

87 if not self.solutions: 

88 return 0 

89 return max(s.pareto_x for s in self.solutions) 

90 

91 @property 

92 def avg_per_case_cost(self) -> float: 

93 """Return the average cost of the front.""" 

94 if not self.solutions: 

95 return 0 

96 return sum(s.evaluation.avg_cost_by_case for s in self.solutions) / self.size 

97 

98 @property 

99 def avg_total_cost(self) -> float: 

100 """Return the average total cost of the front.""" 

101 if not self.solutions: 

102 return 0 

103 return sum(s.evaluation.total_cost for s in self.solutions) / self.size 

104 

105 @property 

106 def avg_cycle_time(self) -> float: 

107 """Return the average cycle time of the front.""" 

108 if not self.solutions: 

109 return 0 

110 return sum(s.evaluation.total_cycle_time for s in self.solutions) / self.size 

111 

112 @property 

113 def min_cycle_time(self) -> float: 

114 """Return the minimum cycle time of the front.""" 

115 if not self.solutions: 

116 return 0 

117 return min(s.evaluation.total_cycle_time for s in self.solutions) 

118 

119 @property 

120 def avg_point(self) -> tuple[float, float]: 

121 """Return the average point of the front.""" 

122 if not self.solutions: 

123 return 0, 0 

124 return self.avg_x, self.avg_y 

125 

126 def avg_distance_to(self, solution: "Solution") -> float: 

127 """Return the average distance to the given evaluation.""" 

128 if not self.solutions: 

129 return 0 

130 return sum(s.distance_to(solution) for s in self.solutions) / self.size 

131 

132 def add(self, solution: "Solution") -> None: 

133 """Add a new solution to the front. 

134 

135 Note, that this does not check if the solution is dominated by any 

136 other solution. This should be done before calling this method. 

137 """ 

138 # Remove all solutions dominated by the new solution 

139 self.solutions = [s for s in self.solutions if not s.is_dominated_by(solution)] 

140 self.solutions.append(solution) 

141 

142 def is_in_front(self, solution: "Solution") -> FRONT_STATUS: 

143 """Check whether the evaluation is in front of the current front. 

144 

145 Returns IS_DOMINATED if the front is dominated by the evaluation 

146 Returns DOMINATES if the front dominates the evaluation 

147 Returns IN_FRONT if the evaluation is in the front 

148 """ 

149 if not self.solutions: 

150 return FRONT_STATUS.IN_FRONT 

151 

152 if not solution.is_valid: 

153 return FRONT_STATUS.INVALID 

154 

155 self_is_always_dominated = True 

156 for s in self.solutions: 

157 if not s.is_dominated_by(solution): 

158 self_is_always_dominated = False 

159 if solution.is_dominated_by(s): 

160 return FRONT_STATUS.DOMINATES 

161 if self_is_always_dominated: 

162 return FRONT_STATUS.IS_DOMINATED 

163 return FRONT_STATUS.IN_FRONT 

164 

165 def is_dominated_by(self, solution: "Solution") -> bool: 

166 """Check whether the evaluation is dominated by the current front.""" 

167 return all(s.is_dominated_by(solution) for s in self.solutions) 

168 

169 def is_dominated_by_evaluation(self, evaluation: "Evaluation") -> bool: 

170 """Check whether the evaluation is dominated by the current front.""" 

171 return all(s.evaluation.is_dominated_by(evaluation) for s in self.solutions) 

172 

173 def get_bounding_rect(self) -> tuple[float, float, float, float]: 

174 """Get the bounding rectangle of the front. 

175 

176 Note, that this is of course only a very broad estimate, 

177 because the front is more a polygon than a rectangle. 

178 """ 

179 min_x = min(s.pareto_x for s in self.solutions) 

180 max_x = max(s.pareto_x for s in self.solutions) 

181 min_y = min(s.pareto_y for s in self.solutions) 

182 max_y = max(s.pareto_y for s in self.solutions) 

183 return min_x, min_y, max_x, max_y