scirs2_optimize/multi_objective/indicators/
mod.rs1use crate::multi_objective::solutions::Solution;
6
7pub fn hypervolume(pareto_front: &[Solution], reference_point: &[f64]) -> f64 {
9 if pareto_front.is_empty() {
10 return 0.0;
11 }
12
13 let n_objectives = pareto_front[0].objectives.len();
14
15 if n_objectives == 2 {
17 hypervolume_2d(pareto_front, reference_point)
18 } else {
19 hypervolume_monte_carlo(pareto_front, reference_point, 10000)
21 }
22}
23
24fn hypervolume_2d(pareto_front: &[Solution], reference_point: &[f64]) -> f64 {
26 let mut sorted_front = pareto_front.to_vec();
27 sorted_front.sort_by(|a, b| {
28 a.objectives[0]
29 .partial_cmp(&b.objectives[0])
30 .expect("Operation failed")
31 });
32
33 let mut volume = 0.0;
34 let mut prev_x = 0.0;
35
36 for solution in &sorted_front {
37 let x = solution.objectives[0];
38 let y = solution.objectives[1];
39
40 if x < reference_point[0] && y < reference_point[1] {
41 let width = x - prev_x;
42 let height = reference_point[1] - y;
43 volume += width * height;
44 prev_x = x;
45 }
46 }
47
48 if prev_x < reference_point[0] {
50 let last_y = sorted_front.last().map(|s| s.objectives[1]).unwrap_or(0.0);
51 if last_y < reference_point[1] {
52 volume += (reference_point[0] - prev_x) * (reference_point[1] - last_y);
53 }
54 }
55
56 volume
57}
58
59fn hypervolume_monte_carlo(
61 pareto_front: &[Solution],
62 reference_point: &[f64],
63 n_samples: usize,
64) -> f64 {
65 use scirs2_core::random::Rng;
66 let mut rng = scirs2_core::random::rng();
67 let n_objectives = reference_point.len();
68
69 let mut count = 0;
70
71 for _ in 0..n_samples {
72 let point: Vec<f64> = (0..n_objectives)
73 .map(|i| rng.random::<f64>() * reference_point[i])
74 .collect();
75
76 if is_dominated_by_front(&point, pareto_front) {
77 count += 1;
78 }
79 }
80
81 let total_volume: f64 = reference_point.iter().product();
82 total_volume * (count as f64 / n_samples as f64)
83}
84
85fn is_dominated_by_front(point: &[f64], pareto_front: &[Solution]) -> bool {
86 for solution in pareto_front {
87 if dominates(
88 solution.objectives.as_slice().expect("Operation failed"),
89 point,
90 ) {
91 return true;
92 }
93 }
94 false
95}
96
97fn dominates(a: &[f64], b: &[f64]) -> bool {
98 let mut at_least_one_better = false;
99
100 for i in 0..a.len() {
101 if a[i] > b[i] {
102 return false;
103 }
104 if a[i] < b[i] {
105 at_least_one_better = true;
106 }
107 }
108
109 at_least_one_better
110}
111
112pub fn igd(pareto_front: &[Solution], true_pareto_front: &[Vec<f64>]) -> f64 {
114 if true_pareto_front.is_empty() {
115 return f64::INFINITY;
116 }
117
118 let sum: f64 = true_pareto_front
119 .iter()
120 .map(|true_point| {
121 pareto_front
122 .iter()
123 .map(|solution| {
124 euclidean_distance(
125 solution.objectives.as_slice().expect("Operation failed"),
126 true_point,
127 )
128 })
129 .fold(f64::INFINITY, |a, b| a.min(b))
130 })
131 .sum();
132
133 sum / true_pareto_front.len() as f64
134}
135
136pub fn generational_distance(pareto_front: &[Solution], true_pareto_front: &[Vec<f64>]) -> f64 {
138 if pareto_front.is_empty() {
139 return f64::INFINITY;
140 }
141
142 let sum: f64 = pareto_front
143 .iter()
144 .map(|solution| {
145 true_pareto_front
146 .iter()
147 .map(|true_point| {
148 euclidean_distance(
149 solution.objectives.as_slice().expect("Operation failed"),
150 true_point,
151 )
152 })
153 .fold(f64::INFINITY, |a, b| a.min(b))
154 })
155 .sum();
156
157 sum / pareto_front.len() as f64
158}
159
160pub fn spacing(pareto_front: &[Solution]) -> f64 {
162 if pareto_front.len() < 2 {
163 return 0.0;
164 }
165
166 let distances: Vec<f64> = pareto_front
167 .iter()
168 .map(|sol| {
169 pareto_front
170 .iter()
171 .filter(|other| !std::ptr::eq(*other, sol))
172 .map(|other| {
173 euclidean_distance(
174 sol.objectives.as_slice().expect("Operation failed"),
175 other.objectives.as_slice().expect("Operation failed"),
176 )
177 })
178 .fold(f64::INFINITY, |a, b| a.min(b))
179 })
180 .collect();
181
182 let mean_distance = distances.iter().sum::<f64>() / distances.len() as f64;
183
184 let variance = distances
185 .iter()
186 .map(|d| (d - mean_distance).powi(2))
187 .sum::<f64>()
188 / distances.len() as f64;
189
190 variance.sqrt()
191}
192
193fn euclidean_distance(a: &[f64], b: &[f64]) -> f64 {
194 a.iter()
195 .zip(b.iter())
196 .map(|(x, y)| (x - y).powi(2))
197 .sum::<f64>()
198 .sqrt()
199}