1pub fn hypervolume(points: &[Vec<f64>], r: &[f64]) -> f64 {
38 if points.is_empty() {
39 return 0.0;
40 }
41 if r.len() == 2 {
42 return hypervolume_2d(points, r);
43 }
44 wfg(points, r)
45}
46
47pub fn hv_contributions(points: &[Vec<f64>], r: &[f64]) -> Vec<f64> {
53 if points.is_empty() {
54 return Vec::new();
55 }
56 if r.len() == 2 {
57 return contributions_2d(points, r);
58 }
59 let total = wfg(points, r);
60 (0..points.len())
61 .map(|i| {
62 let without: Vec<Vec<f64>> = points
63 .iter()
64 .enumerate()
65 .filter(|&(j, _)| j != i)
66 .map(|(_, p)| p.clone())
67 .collect();
68 (total - wfg(&without, r)).max(0.0)
69 })
70 .collect()
71}
72
73pub fn gd(front: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
77 mean_min_distance(front, reference, euclidean)
78}
79
80pub fn igd(front: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
86 mean_min_distance(reference, front, euclidean)
87}
88
89pub fn gd_plus(front: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
94 mean_min_distance(front, reference, d_plus)
95}
96
97pub fn igd_plus(front: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
102 mean_min_distance(reference, front, |z, a| d_plus(a, z))
103}
104
105fn mean_min_distance(
107 from: &[Vec<f64>],
108 to: &[Vec<f64>],
109 dist: impl Fn(&[f64], &[f64]) -> f64,
110) -> f64 {
111 if from.is_empty() || to.is_empty() {
112 return f64::NAN;
113 }
114 let total: f64 = from
115 .iter()
116 .map(|p| to.iter().map(|q| dist(p, q)).fold(f64::INFINITY, f64::min))
117 .sum();
118 total / from.len() as f64
119}
120
121fn euclidean(a: &[f64], b: &[f64]) -> f64 {
122 a.iter()
123 .zip(b)
124 .map(|(x, y)| (x - y) * (x - y))
125 .sum::<f64>()
126 .sqrt()
127}
128
129fn d_plus(a: &[f64], z: &[f64]) -> f64 {
132 a.iter()
133 .zip(z)
134 .map(|(ai, zi)| (ai - zi).max(0.0).powi(2))
135 .sum::<f64>()
136 .sqrt()
137}
138
139fn hypervolume_2d(points: &[Vec<f64>], r: &[f64]) -> f64 {
141 let mut sorted: Vec<&Vec<f64>> = points.iter().collect();
143 sorted.sort_by(|a, b| {
144 a[0].partial_cmp(&b[0])
145 .unwrap_or(std::cmp::Ordering::Equal)
146 .then(a[1].partial_cmp(&b[1]).unwrap_or(std::cmp::Ordering::Equal))
147 });
148 let mut vol = 0.0;
149 let mut best_f2 = r[1];
150 for p in sorted {
151 if p[1] < best_f2 && p[0] < r[0] {
152 vol += (r[0] - p[0]) * (best_f2 - p[1]);
153 best_f2 = p[1];
154 }
155 }
156 vol
157}
158
159fn contributions_2d(points: &[Vec<f64>], r: &[f64]) -> Vec<f64> {
161 let k = points.len();
162 let mut order: Vec<usize> = (0..k).collect();
163 order.sort_by(|&a, &b| {
164 points[a][0]
165 .partial_cmp(&points[b][0])
166 .unwrap_or(std::cmp::Ordering::Equal)
167 .then(
168 points[a][1]
169 .partial_cmp(&points[b][1])
170 .unwrap_or(std::cmp::Ordering::Equal),
171 )
172 });
173 let mut contr = vec![0.0; k];
176 let mut stair: Vec<usize> = Vec::with_capacity(k);
177 let mut best_f2 = f64::INFINITY;
178 for &i in &order {
179 if points[i][1] < best_f2 {
180 stair.push(i);
181 best_f2 = points[i][1];
182 }
183 }
184 for (pos, &i) in stair.iter().enumerate() {
185 let x_next = if pos + 1 < stair.len() {
186 points[stair[pos + 1]][0]
187 } else {
188 r[0]
189 };
190 let y_prev = if pos > 0 {
191 points[stair[pos - 1]][1]
192 } else {
193 r[1]
194 };
195 contr[i] = ((x_next - points[i][0]) * (y_prev - points[i][1])).max(0.0);
196 }
197 contr
198}
199
200fn wfg(points: &[Vec<f64>], r: &[f64]) -> f64 {
203 if points.is_empty() {
204 return 0.0;
205 }
206 let mut vol = 0.0;
207 for i in 0..points.len() {
208 let incl: f64 = r
209 .iter()
210 .zip(&points[i])
211 .map(|(rj, pj)| (rj - pj).max(0.0))
212 .product();
213 let limited: Vec<Vec<f64>> = points[i + 1..]
215 .iter()
216 .map(|q| (0..r.len()).map(|j| points[i][j].max(q[j])).collect())
217 .collect();
218 let nd = non_dominated(&limited);
219 vol += incl - wfg(&nd, r);
220 }
221 vol
222}
223
224fn non_dominated(set: &[Vec<f64>]) -> Vec<Vec<f64>> {
226 let mut out = Vec::new();
227 for (i, p) in set.iter().enumerate() {
228 let dominated = set.iter().enumerate().any(|(j, q)| {
229 i != j && q.iter().zip(p).all(|(a, b)| a <= b) && q.iter().zip(p).any(|(a, b)| a < b)
230 });
231 if !dominated {
232 out.push(p.clone());
233 }
234 }
235 out
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn hypervolume_known_2d_and_3d() {
244 let pts = vec![vec![1.0, 3.0], vec![2.0, 2.0]];
246 assert!((hypervolume(&pts, &[3.0, 4.0]) - 3.0).abs() < 1e-9);
247 let with_dominated = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![2.5, 3.5]];
249 assert!((hypervolume(&with_dominated, &[3.0, 4.0]) - 3.0).abs() < 1e-9);
250 let one = vec![vec![1.0, 1.0, 1.0]];
252 assert!((hypervolume(&one, &[2.0, 3.0, 4.0]) - 6.0).abs() < 1e-9);
253 }
254
255 #[test]
256 fn wfg_and_2d_sweep_agree() {
257 let pts = vec![
259 vec![0.1, 0.9],
260 vec![0.4, 0.5],
261 vec![0.4, 0.5], vec![0.8, 0.2],
263 vec![0.9, 0.8], ];
265 let r = [1.1, 1.1];
266 let sweep = hypervolume_2d(&pts, &r);
267 let recursive = wfg(&pts, &r);
268 assert!(
269 (sweep - recursive).abs() < 1e-12,
270 "sweep {sweep} vs wfg {recursive}"
271 );
272 }
273
274 #[test]
275 fn contributions_match_leave_one_out() {
276 let pts = vec![vec![0.1, 0.9], vec![0.4, 0.5], vec![0.8, 0.2]];
277 let r = [1.0, 1.0];
278 let fast = hv_contributions(&pts, &r);
279 let total = hypervolume(&pts, &r);
280 for (i, f) in fast.iter().enumerate() {
281 let without: Vec<Vec<f64>> = pts
282 .iter()
283 .enumerate()
284 .filter(|&(j, _)| j != i)
285 .map(|(_, p)| p.clone())
286 .collect();
287 let slow = total - hypervolume(&without, &r);
288 assert!(
289 (f - slow).abs() < 1e-12,
290 "point {i}: fast {f} vs slow {slow}"
291 );
292 }
293 let with_extra = vec![
295 vec![0.1, 0.9],
296 vec![0.1, 0.9],
297 vec![0.5, 0.95], ];
299 let c = hv_contributions(&with_extra, &r);
300 assert_eq!(c[1], 0.0);
301 assert_eq!(c[2], 0.0);
302 }
303
304 #[test]
305 fn igd_plus_ignores_dominating_deviation() {
306 let reference = vec![vec![0.5, 0.5]];
309 let dominating_front = vec![vec![0.4, 0.4]];
310 assert!(igd(&dominating_front, &reference) > 0.0);
311 assert_eq!(igd_plus(&dominating_front, &reference), 0.0);
312 let dominated_front = vec![vec![0.6, 0.6]];
314 let d_igd = igd(&dominated_front, &reference);
315 let d_plusv = igd_plus(&dominated_front, &reference);
316 assert!((d_igd - d_plusv).abs() < 1e-12);
317 }
318
319 #[test]
320 fn gd_igd_basics() {
321 let front = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
322 assert_eq!(gd(&front, &front), 0.0);
324 assert_eq!(igd(&front, &front), 0.0);
325 assert_eq!(gd_plus(&front, &front), 0.0);
326 assert_eq!(igd_plus(&front, &front), 0.0);
327 assert!(gd(&[], &front).is_nan());
329 assert!(igd(&front, &[]).is_nan());
330 }
331}