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
« 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
4from o2.models.evaluation import Evaluation
6if TYPE_CHECKING:
7 from o2.models.solution import Solution
10class FRONT_STATUS(Enum): # noqa: N801
11 """The status of a solution compared to a Pareto Front."""
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"""
23class ParetoFront:
24 """A set of solutions, where no solution is dominated by another solution."""
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."""
30 @property
31 def size(self) -> int:
32 """Return the number of solutions in the front."""
33 return len(self.solutions)
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
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
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]
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]
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)
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)
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)
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)
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
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
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
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)
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
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
132 def add(self, solution: "Solution") -> None:
133 """Add a new solution to the front.
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)
142 def is_in_front(self, solution: "Solution") -> FRONT_STATUS:
143 """Check whether the evaluation is in front of the current front.
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
152 if not solution.is_valid:
153 return FRONT_STATUS.INVALID
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
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)
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)
173 def get_bounding_rect(self) -> tuple[float, float, float, float]:
174 """Get the bounding rectangle of the front.
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