scirs2_optimize/multi_objective/indicators/
mod.rs1use crate::multi_objective::solutions::Solution;
6
7pub fn hypervolume(pareto_front: &[Solution], reference_point: &[f64]) -> f64 {
16 if pareto_front.is_empty() {
17 return 0.0;
18 }
19
20 let n_objectives = pareto_front[0].objectives.len();
21
22 if n_objectives == 2 {
23 hypervolume_2d(pareto_front, reference_point)
24 } else if n_objectives < 8 {
25 let objectives: Vec<Vec<f64>> =
29 pareto_front.iter().map(|s| s.objectives.to_vec()).collect();
30 crate::multi_objective::pareto::hypervolume_from_objectives(&objectives, reference_point)
31 } else {
32 hypervolume_monte_carlo(pareto_front, reference_point, 10000)
34 }
35}
36
37fn hypervolume_2d(pareto_front: &[Solution], reference_point: &[f64]) -> f64 {
39 let mut sorted_front = pareto_front.to_vec();
40 sorted_front.sort_by(|a, b| {
41 a.objectives[0]
42 .partial_cmp(&b.objectives[0])
43 .expect("Operation failed")
44 });
45
46 let mut volume = 0.0;
47 let mut prev_x = 0.0;
48
49 for solution in &sorted_front {
50 let x = solution.objectives[0];
51 let y = solution.objectives[1];
52
53 if x < reference_point[0] && y < reference_point[1] {
54 let width = x - prev_x;
55 let height = reference_point[1] - y;
56 volume += width * height;
57 prev_x = x;
58 }
59 }
60
61 if prev_x < reference_point[0] {
63 let last_y = sorted_front.last().map(|s| s.objectives[1]).unwrap_or(0.0);
64 if last_y < reference_point[1] {
65 volume += (reference_point[0] - prev_x) * (reference_point[1] - last_y);
66 }
67 }
68
69 volume
70}
71
72fn hypervolume_monte_carlo(
74 pareto_front: &[Solution],
75 reference_point: &[f64],
76 n_samples: usize,
77) -> f64 {
78 use scirs2_core::random::{Rng, RngExt};
79 let mut rng = scirs2_core::random::rng();
80 let n_objectives = reference_point.len();
81
82 let mut count = 0;
83
84 for _ in 0..n_samples {
85 let point: Vec<f64> = (0..n_objectives)
86 .map(|i| rng.random::<f64>() * reference_point[i])
87 .collect();
88
89 if is_dominated_by_front(&point, pareto_front) {
90 count += 1;
91 }
92 }
93
94 let total_volume: f64 = reference_point.iter().product();
95 total_volume * (count as f64 / n_samples as f64)
96}
97
98fn is_dominated_by_front(point: &[f64], pareto_front: &[Solution]) -> bool {
99 for solution in pareto_front {
100 if dominates(
101 solution.objectives.as_slice().expect("Operation failed"),
102 point,
103 ) {
104 return true;
105 }
106 }
107 false
108}
109
110fn dominates(a: &[f64], b: &[f64]) -> bool {
111 let mut at_least_one_better = false;
112
113 for i in 0..a.len() {
114 if a[i] > b[i] {
115 return false;
116 }
117 if a[i] < b[i] {
118 at_least_one_better = true;
119 }
120 }
121
122 at_least_one_better
123}
124
125pub fn igd(pareto_front: &[Solution], true_pareto_front: &[Vec<f64>]) -> f64 {
127 if true_pareto_front.is_empty() {
128 return f64::INFINITY;
129 }
130
131 let sum: f64 = true_pareto_front
132 .iter()
133 .map(|true_point| {
134 pareto_front
135 .iter()
136 .map(|solution| {
137 euclidean_distance(
138 solution.objectives.as_slice().expect("Operation failed"),
139 true_point,
140 )
141 })
142 .fold(f64::INFINITY, |a, b| a.min(b))
143 })
144 .sum();
145
146 sum / true_pareto_front.len() as f64
147}
148
149pub fn generational_distance(pareto_front: &[Solution], true_pareto_front: &[Vec<f64>]) -> f64 {
151 if pareto_front.is_empty() {
152 return f64::INFINITY;
153 }
154
155 let sum: f64 = pareto_front
156 .iter()
157 .map(|solution| {
158 true_pareto_front
159 .iter()
160 .map(|true_point| {
161 euclidean_distance(
162 solution.objectives.as_slice().expect("Operation failed"),
163 true_point,
164 )
165 })
166 .fold(f64::INFINITY, |a, b| a.min(b))
167 })
168 .sum();
169
170 sum / pareto_front.len() as f64
171}
172
173pub fn spacing(pareto_front: &[Solution]) -> f64 {
175 if pareto_front.len() < 2 {
176 return 0.0;
177 }
178
179 let distances: Vec<f64> = pareto_front
180 .iter()
181 .map(|sol| {
182 pareto_front
183 .iter()
184 .filter(|other| !std::ptr::eq(*other, sol))
185 .map(|other| {
186 euclidean_distance(
187 sol.objectives.as_slice().expect("Operation failed"),
188 other.objectives.as_slice().expect("Operation failed"),
189 )
190 })
191 .fold(f64::INFINITY, |a, b| a.min(b))
192 })
193 .collect();
194
195 let mean_distance = distances.iter().sum::<f64>() / distances.len() as f64;
196
197 let variance = distances
198 .iter()
199 .map(|d| (d - mean_distance).powi(2))
200 .sum::<f64>()
201 / distances.len() as f64;
202
203 variance.sqrt()
204}
205
206fn euclidean_distance(a: &[f64], b: &[f64]) -> f64 {
207 a.iter()
208 .zip(b.iter())
209 .map(|(x, y)| (x - y).powi(2))
210 .sum::<f64>()
211 .sqrt()
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use scirs2_core::ndarray::array;
218
219 fn make_solution(objs: &[f64]) -> Solution {
220 Solution::new(
221 array![0.0],
222 scirs2_core::ndarray::Array1::from_vec(objs.to_vec()),
223 )
224 }
225
226 #[test]
232 fn test_hypervolume_3d_single_point_at_origin() {
233 let front = vec![make_solution(&[0.0, 0.0, 0.0])];
234 let hv = hypervolume(&front, &[2.0, 2.0, 2.0]);
235 assert!(
236 (hv - 8.0).abs() < 1e-10,
237 "Expected 8.0 for single 3D point at origin, got {}",
238 hv
239 );
240 }
241
242 #[test]
245 fn test_hypervolume_3d_single_point_half() {
246 let front = vec![make_solution(&[0.5, 0.5, 0.5])];
247 let hv = hypervolume(&front, &[1.0, 1.0, 1.0]);
248 assert!(
249 (hv - 0.125).abs() < 1e-10,
250 "Expected 0.125 for 3D half-ref point, got {}",
251 hv
252 );
253 }
254
255 #[test]
262 fn test_hypervolume_3d_two_points() {
263 let front = vec![
264 make_solution(&[1.0, 3.0, 2.0]),
265 make_solution(&[3.0, 1.0, 4.0]),
266 ];
267 let hv = hypervolume(&front, &[5.0, 5.0, 5.0]);
268 assert!(
269 (hv - 28.0).abs() < 1e-10,
270 "Expected 28.0 for two non-dominated 3D points, got {}",
271 hv
272 );
273 }
274
275 #[test]
279 fn test_hypervolume_3d_wfg_vs_monte_carlo() {
280 let front = vec![
281 make_solution(&[1.0, 3.0, 2.0]),
282 make_solution(&[3.0, 1.0, 4.0]),
283 ];
284 let exact_hv = hypervolume(&front, &[5.0, 5.0, 5.0]);
285 let mc_hv = hypervolume_monte_carlo(&front, &[5.0, 5.0, 5.0], 100_000);
286 let tol = 0.05 * exact_hv; assert!(
288 (exact_hv - mc_hv).abs() < tol,
289 "WFG ({}) and Monte Carlo ({}) differ by more than 5%",
290 exact_hv,
291 mc_hv
292 );
293 }
294
295 #[test]
297 fn test_hypervolume_3d_empty() {
298 let front: Vec<Solution> = vec![];
299 let hv = hypervolume(&front, &[1.0, 1.0, 1.0]);
300 assert_eq!(hv, 0.0);
301 }
302
303 #[test]
305 fn test_hypervolume_3d_point_at_boundary() {
306 let front = vec![make_solution(&[1.0, 1.0, 1.0])];
307 let hv = hypervolume(&front, &[1.0, 1.0, 1.0]);
308 assert_eq!(hv, 0.0, "Point at boundary should contribute 0");
309 }
310}