1use std::error::Error;
10use std::fmt;
11
12use crate::Rng;
13
14const DEFAULT_MONTE_CARLO_SAMPLES: usize = 100_000;
15const DEFAULT_MONTE_CARLO_SEED: u64 = 0x4856_2d4d_4f4e_5445;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum IndicatorError {
20 EmptySet,
22 EmptyPoint,
24 DimensionMismatch,
26 NonFiniteValue,
28 ReferencePointViolation,
30 InsufficientSamples,
32}
33
34impl fmt::Display for IndicatorError {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 let message = match self {
37 Self::EmptySet => "indicator sets must not be empty",
38 Self::EmptyPoint => "indicator points must have at least one objective",
39 Self::DimensionMismatch => "indicator point dimensions must match",
40 Self::NonFiniteValue => "indicator coordinates must be finite",
41 Self::ReferencePointViolation => {
42 "hypervolume points must be weakly better than the reference point"
43 }
44 Self::InsufficientSamples => "Monte Carlo hypervolume requires at least two samples",
45 };
46 formatter.write_str(message)
47 }
48}
49
50impl Error for IndicatorError {}
51
52#[derive(Clone, Debug, PartialEq)]
54pub struct ReferencePoint(Vec<f64>);
55
56impl ReferencePoint {
57 pub fn new(coordinates: Vec<f64>) -> Result<Self, IndicatorError> {
64 if coordinates.is_empty() {
65 return Err(IndicatorError::EmptyPoint);
66 }
67 if coordinates.iter().any(|value| !value.is_finite()) {
68 return Err(IndicatorError::NonFiniteValue);
69 }
70 Ok(Self(coordinates))
71 }
72
73 pub fn as_slice(&self) -> &[f64] {
75 &self.0
76 }
77
78 pub fn dimension(&self) -> usize {
80 self.0.len()
81 }
82}
83
84#[derive(Clone, Debug, PartialEq)]
86pub enum HypervolumeEstimate {
87 Exact(f64),
89 MonteCarlo {
91 value: f64,
93 standard_error: f64,
95 samples: usize,
97 seed: u64,
99 },
100}
101
102impl HypervolumeEstimate {
103 pub fn value(&self) -> f64 {
105 match self {
106 Self::Exact(value) | Self::MonteCarlo { value, .. } => *value,
107 }
108 }
109}
110
111#[derive(Clone, Debug, PartialEq)]
113pub struct HypervolumeReport {
114 pub estimate: HypervolumeEstimate,
116 pub input_points: usize,
118 pub retained_points: usize,
120 pub duplicates_collapsed: usize,
122 pub dominated_removed: usize,
124 pub outside_reference: usize,
127}
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq)]
131pub enum OutsidePolicy {
132 Strict,
134 Exclude,
136}
137
138fn same_point(left: &[f64], right: &[f64]) -> bool {
139 left.iter().zip(right).all(|(a, b)| a == b)
140}
141
142fn dominates(left: &[f64], right: &[f64]) -> bool {
143 let mut strict = false;
144 for (&a, &b) in left.iter().zip(right) {
145 if a > b {
146 return false;
147 }
148 strict |= a < b;
149 }
150 strict
151}
152
153struct CleanedHypervolumeFront {
154 points: Vec<Vec<f64>>,
155 duplicates: usize,
156 dominated: usize,
157 outside: usize,
158}
159
160fn clean_hypervolume_front(
161 front: &[Vec<f64>],
162 reference: &ReferencePoint,
163 policy: OutsidePolicy,
164) -> Result<CleanedHypervolumeFront, IndicatorError> {
165 validate_set(front, Some(reference.dimension()))?;
166 let outside = |point: &Vec<f64>| {
167 point
168 .iter()
169 .zip(reference.as_slice())
170 .any(|(&value, &limit)| value > limit)
171 };
172 let outside_reference = front.iter().filter(|point| outside(point)).count();
173 if policy == OutsidePolicy::Strict && outside_reference > 0 {
174 return Err(IndicatorError::ReferencePointViolation);
175 }
176
177 let mut unique: Vec<Vec<f64>> = front
178 .iter()
179 .filter(|point| !outside(point))
180 .cloned()
181 .collect();
182 unique.sort_by(|left, right| {
183 left.iter()
184 .zip(right)
185 .find_map(|(a, b)| (a != b).then(|| a.total_cmp(b)))
186 .unwrap_or(std::cmp::Ordering::Equal)
187 });
188 unique.dedup_by(|left, right| same_point(left, right));
189 let duplicates = front.len() - outside_reference - unique.len();
190 let retained: Vec<Vec<f64>> = unique
191 .iter()
192 .enumerate()
193 .filter(|(index, point)| {
194 !unique
195 .iter()
196 .enumerate()
197 .any(|(other, candidate)| other != *index && dominates(candidate, point))
198 })
199 .map(|(_, point)| point.clone())
200 .collect();
201 let dominated = unique.len() - retained.len();
202 Ok(CleanedHypervolumeFront {
203 points: retained,
204 duplicates,
205 dominated,
206 outside: outside_reference,
207 })
208}
209
210fn exact_union(points: &[&[f64]], reference: &[f64], dimensions: usize) -> f64 {
211 if points.is_empty() {
212 return 0.0;
213 }
214 if dimensions == 1 {
215 let minimum = points
216 .iter()
217 .map(|point| point[0])
218 .fold(reference[0], f64::min);
219 return (reference[0] - minimum).max(0.0);
220 }
221 let axis = dimensions - 1;
222 let mut cuts: Vec<f64> = points
223 .iter()
224 .map(|point| point[axis])
225 .filter(|value| *value < reference[axis])
226 .collect();
227 cuts.sort_by(f64::total_cmp);
228 cuts.dedup_by(|left, right| left == right);
229 let mut volume = 0.0;
230 for (index, &lower) in cuts.iter().enumerate() {
231 let upper = cuts.get(index + 1).copied().unwrap_or(reference[axis]);
232 if upper <= lower {
233 continue;
234 }
235 let active: Vec<&[f64]> = points
236 .iter()
237 .copied()
238 .filter(|point| point[axis] <= lower)
239 .collect();
240 volume += (upper - lower) * exact_union(&active, reference, axis);
241 }
242 volume
243}
244
245fn report(
246 input_points: usize,
247 retained: &[Vec<f64>],
248 duplicates_collapsed: usize,
249 dominated_removed: usize,
250 outside_reference: usize,
251 estimate: HypervolumeEstimate,
252) -> HypervolumeReport {
253 HypervolumeReport {
254 estimate,
255 input_points,
256 retained_points: retained.len(),
257 duplicates_collapsed,
258 dominated_removed,
259 outside_reference,
260 }
261}
262
263pub fn hypervolume(
274 front: &[Vec<f64>],
275 reference: &ReferencePoint,
276) -> Result<HypervolumeReport, IndicatorError> {
277 hypervolume_with(front, reference, OutsidePolicy::Strict)
278}
279
280pub fn hypervolume_with(
293 front: &[Vec<f64>],
294 reference: &ReferencePoint,
295 policy: OutsidePolicy,
296) -> Result<HypervolumeReport, IndicatorError> {
297 if reference.dimension() > 4 {
298 return hypervolume_monte_carlo_impl(
299 front,
300 reference,
301 DEFAULT_MONTE_CARLO_SAMPLES,
302 DEFAULT_MONTE_CARLO_SEED,
303 policy,
304 );
305 }
306 let cleaned = clean_hypervolume_front(front, reference, policy)?;
307 let borrowed: Vec<&[f64]> = cleaned.points.iter().map(Vec::as_slice).collect();
308 let value = exact_union(&borrowed, reference.as_slice(), reference.dimension());
309 Ok(report(
310 front.len(),
311 &cleaned.points,
312 cleaned.duplicates,
313 cleaned.dominated,
314 cleaned.outside,
315 HypervolumeEstimate::Exact(value),
316 ))
317}
318
319pub fn hypervolume_monte_carlo(
326 front: &[Vec<f64>],
327 reference: &ReferencePoint,
328 samples: usize,
329 seed: u64,
330) -> Result<HypervolumeReport, IndicatorError> {
331 hypervolume_monte_carlo_impl(front, reference, samples, seed, OutsidePolicy::Strict)
332}
333
334fn hypervolume_monte_carlo_impl(
335 front: &[Vec<f64>],
336 reference: &ReferencePoint,
337 samples: usize,
338 seed: u64,
339 policy: OutsidePolicy,
340) -> Result<HypervolumeReport, IndicatorError> {
341 if samples < 2 {
342 return Err(IndicatorError::InsufficientSamples);
343 }
344 let cleaned = clean_hypervolume_front(front, reference, policy)?;
345 let dimensions = reference.dimension();
346 let lower: Vec<f64> = (0..dimensions)
347 .map(|axis| {
348 cleaned
349 .points
350 .iter()
351 .map(|point| point[axis])
352 .fold(reference.as_slice()[axis], f64::min)
353 })
354 .collect();
355 let bounding_volume: f64 = lower
356 .iter()
357 .zip(reference.as_slice())
358 .map(|(&lo, &hi)| hi - lo)
359 .product();
360 let mut rng = Rng::new(seed);
361 let mut dominated_samples = 0usize;
362 for _ in 0..samples {
363 let sample: Vec<f64> = lower
364 .iter()
365 .zip(reference.as_slice())
366 .map(|(&lo, &hi)| lo + (hi - lo) * rng.uniform01())
367 .collect();
368 if cleaned.points.iter().any(|point| {
369 point
370 .iter()
371 .zip(&sample)
372 .all(|(&value, &draw)| value <= draw)
373 }) {
374 dominated_samples += 1;
375 }
376 }
377 let probability = dominated_samples as f64 / samples as f64;
378 let value = bounding_volume * probability;
379 let standard_error =
380 bounding_volume * (probability * (1.0 - probability) / samples as f64).sqrt();
381 Ok(report(
382 front.len(),
383 &cleaned.points,
384 cleaned.duplicates,
385 cleaned.dominated,
386 cleaned.outside,
387 HypervolumeEstimate::MonteCarlo {
388 value,
389 standard_error,
390 samples,
391 seed,
392 },
393 ))
394}
395
396fn validate_set(points: &[Vec<f64>], dimension: Option<usize>) -> Result<usize, IndicatorError> {
397 let Some(first) = points.first() else {
398 return Err(IndicatorError::EmptySet);
399 };
400 if first.is_empty() {
401 return Err(IndicatorError::EmptyPoint);
402 }
403 let dimension = dimension.unwrap_or(first.len());
404 if points.iter().any(|point| point.len() != dimension) {
405 return Err(IndicatorError::DimensionMismatch);
406 }
407 if points.iter().flatten().any(|value| !value.is_finite()) {
408 return Err(IndicatorError::NonFiniteValue);
409 }
410 Ok(dimension)
411}
412
413pub fn nondominated_sort(points: &[Vec<f64>]) -> Result<Vec<Vec<usize>>, IndicatorError> {
425 validate_set(points, None)?;
426 let count = points.len();
427 let mut dominates_indices = vec![Vec::new(); count];
428 let mut domination_count = vec![0usize; count];
429 for left in 0..count {
430 for right in left + 1..count {
431 if dominates(&points[left], &points[right]) {
432 dominates_indices[left].push(right);
433 domination_count[right] += 1;
434 } else if dominates(&points[right], &points[left]) {
435 dominates_indices[right].push(left);
436 domination_count[left] += 1;
437 }
438 }
439 }
440 let mut current: Vec<usize> = domination_count
441 .iter()
442 .enumerate()
443 .filter_map(|(index, &value)| (value == 0).then_some(index))
444 .collect();
445 let mut fronts = Vec::new();
446 while !current.is_empty() {
447 let mut next = Vec::new();
448 for &index in ¤t {
449 for &dominated in &dominates_indices[index] {
450 domination_count[dominated] -= 1;
451 if domination_count[dominated] == 0 {
452 next.push(dominated);
453 }
454 }
455 }
456 fronts.push(current);
457 current = next;
458 }
459 Ok(fronts)
460}
461
462pub fn crowding_distance(points: &[Vec<f64>]) -> Result<Vec<f64>, IndicatorError> {
474 validate_set(points, None)?;
475 let count = points.len();
476 if count <= 2 {
477 return Ok(vec![f64::INFINITY; count]);
478 }
479 let mut distance = vec![0.0; count];
480 for (objective, _) in points[0].iter().enumerate() {
481 let mut order: Vec<usize> = (0..count).collect();
482 order.sort_by(|&left, &right| points[left][objective].total_cmp(&points[right][objective]));
483 let minimum = points[order[0]][objective];
484 let maximum = points[order[count - 1]][objective];
485 let span = maximum - minimum;
486 if span <= 0.0 {
487 continue;
488 }
489 for &index in &order {
490 if points[index][objective] == minimum || points[index][objective] == maximum {
491 distance[index] = f64::INFINITY;
492 }
493 }
494 for position in 1..count - 1 {
495 let index = order[position];
496 if distance[index].is_finite() {
497 distance[index] += (points[order[position + 1]][objective]
498 - points[order[position - 1]][objective])
499 / span;
500 }
501 }
502 }
503 Ok(distance)
504}
505
506fn validate_pair(left: &[Vec<f64>], right: &[Vec<f64>]) -> Result<usize, IndicatorError> {
507 let dimension = validate_set(left, None)?;
508 validate_set(right, Some(dimension))?;
509 Ok(dimension)
510}
511
512fn euclidean(left: &[f64], right: &[f64]) -> f64 {
513 left.iter()
514 .zip(right)
515 .map(|(&a, &b)| (a - b).powi(2))
516 .sum::<f64>()
517 .sqrt()
518}
519
520fn plus_distance(approximation: &[f64], reference: &[f64]) -> f64 {
521 approximation
522 .iter()
523 .zip(reference)
524 .map(|(&a, &r)| (a - r).max(0.0).powi(2))
525 .sum::<f64>()
526 .sqrt()
527}
528
529fn mean_min_distance(
530 sources: &[Vec<f64>],
531 targets: &[Vec<f64>],
532 distance: impl Fn(&[f64], &[f64]) -> f64,
533) -> f64 {
534 sources
535 .iter()
536 .map(|source| {
537 targets
538 .iter()
539 .map(|target| distance(target, source))
540 .fold(f64::INFINITY, f64::min)
541 })
542 .sum::<f64>()
543 / sources.len() as f64
544}
545
546pub fn igd(front: &[Vec<f64>], reference_set: &[Vec<f64>]) -> Result<f64, IndicatorError> {
553 validate_pair(front, reference_set)?;
554 Ok(mean_min_distance(reference_set, front, euclidean))
555}
556
557pub fn igd_plus(front: &[Vec<f64>], reference_set: &[Vec<f64>]) -> Result<f64, IndicatorError> {
564 validate_pair(front, reference_set)?;
565 Ok(mean_min_distance(reference_set, front, plus_distance))
566}
567
568pub fn gd(front: &[Vec<f64>], reference_set: &[Vec<f64>]) -> Result<f64, IndicatorError> {
575 validate_pair(front, reference_set)?;
576 Ok(mean_min_distance(front, reference_set, euclidean))
577}
578
579pub fn gd_plus(front: &[Vec<f64>], reference_set: &[Vec<f64>]) -> Result<f64, IndicatorError> {
586 validate_pair(front, reference_set)?;
587 Ok(front
588 .iter()
589 .map(|approximation| {
590 reference_set
591 .iter()
592 .map(|reference| plus_distance(approximation, reference))
593 .fold(f64::INFINITY, f64::min)
594 })
595 .sum::<f64>()
596 / front.len() as f64)
597}
598
599pub fn additive_epsilon(
607 front: &[Vec<f64>],
608 reference_set: &[Vec<f64>],
609) -> Result<f64, IndicatorError> {
610 validate_pair(front, reference_set)?;
611 Ok(reference_set
612 .iter()
613 .map(|reference| {
614 front
615 .iter()
616 .map(|approximation| {
617 approximation
618 .iter()
619 .zip(reference)
620 .map(|(&a, &r)| a - r)
621 .fold(f64::NEG_INFINITY, f64::max)
622 })
623 .fold(f64::INFINITY, f64::min)
624 })
625 .fold(f64::NEG_INFINITY, f64::max))
626}
627
628pub fn spacing(front: &[Vec<f64>]) -> Result<f64, IndicatorError> {
635 validate_set(front, None)?;
636 if front.len() == 1 {
637 return Ok(0.0);
638 }
639 let nearest: Vec<f64> = front
640 .iter()
641 .enumerate()
642 .map(|(index, point)| {
643 front
644 .iter()
645 .enumerate()
646 .filter(|(other, _)| *other != index)
647 .map(|(_, candidate)| {
648 point
649 .iter()
650 .zip(candidate)
651 .map(|(&a, &b)| (a - b).abs())
652 .sum::<f64>()
653 })
654 .fold(f64::INFINITY, f64::min)
655 })
656 .collect();
657 let mean = nearest.iter().sum::<f64>() / nearest.len() as f64;
658 Ok((nearest
659 .iter()
660 .map(|distance| (distance - mean).powi(2))
661 .sum::<f64>()
662 / (nearest.len() - 1) as f64)
663 .sqrt())
664}
665
666pub fn spread(front: &[Vec<f64>], extremes: &[Vec<f64>]) -> Result<f64, IndicatorError> {
674 validate_pair(front, extremes)?;
675 if front.len() < 2 {
676 return Err(IndicatorError::EmptySet);
677 }
678 let nearest: Vec<f64> = front
679 .iter()
680 .enumerate()
681 .map(|(index, point)| {
682 front
683 .iter()
684 .enumerate()
685 .filter(|(other, _)| *other != index)
686 .map(|(_, candidate)| euclidean(point, candidate))
687 .fold(f64::INFINITY, f64::min)
688 })
689 .collect();
690 let mean = nearest.iter().sum::<f64>() / nearest.len() as f64;
691 let edge = extremes
692 .iter()
693 .map(|extreme| {
694 front
695 .iter()
696 .map(|point| euclidean(extreme, point))
697 .fold(f64::INFINITY, f64::min)
698 })
699 .sum::<f64>();
700 let deviation = nearest
701 .iter()
702 .map(|distance| (distance - mean).abs())
703 .sum::<f64>();
704 let denominator = edge + nearest.len() as f64 * mean;
705 Ok(if denominator == 0.0 {
706 0.0
707 } else {
708 (edge + deviation) / denominator
709 })
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715
716 fn reference(values: &[f64]) -> ReferencePoint {
717 ReferencePoint::new(values.to_vec()).unwrap()
718 }
719
720 #[test]
721 fn exact_hypervolume_has_auditable_cleanup() {
722 let front = vec![
723 vec![1.0, 4.0],
724 vec![2.0, 2.0],
725 vec![4.0, 1.0],
726 vec![2.0, 2.0],
727 vec![3.0, 3.0],
728 ];
729 let result = hypervolume(&front, &reference(&[5.0, 5.0])).unwrap();
730 assert_eq!(result.estimate, HypervolumeEstimate::Exact(11.0));
731 assert_eq!(result.input_points, 5);
732 assert_eq!(result.retained_points, 3);
733 assert_eq!(result.duplicates_collapsed, 1);
734 assert_eq!(result.dominated_removed, 1);
735 assert_eq!(result.outside_reference, 0);
736 }
737
738 #[test]
739 fn outside_reference_policy_is_explicit_and_audited() {
740 let front = vec![
741 vec![1.0, 4.0],
742 vec![2.0, 2.0],
743 vec![6.0, 1.0],
744 vec![7.0, 0.0],
745 ];
746 let reference = reference(&[5.0, 5.0]);
747 assert_eq!(
748 hypervolume(&front, &reference),
749 Err(IndicatorError::ReferencePointViolation)
750 );
751 let report = hypervolume_with(&front, &reference, OutsidePolicy::Exclude).unwrap();
752 assert_eq!(report.estimate, HypervolumeEstimate::Exact(10.0));
753 assert_eq!(report.input_points, 4);
754 assert_eq!(report.retained_points, 2);
755 assert_eq!(report.outside_reference, 2);
756 assert_eq!(report.duplicates_collapsed, 0);
757 assert_eq!(report.dominated_removed, 0);
758
759 let outside_only =
760 hypervolume_with(&[vec![6.0, 1.0]], &reference, OutsidePolicy::Exclude).unwrap();
761 assert_eq!(outside_only.estimate, HypervolumeEstimate::Exact(0.0));
762 assert_eq!(outside_only.retained_points, 0);
763 assert_eq!(outside_only.outside_reference, 1);
764 }
765
766 #[test]
767 fn exact_recursive_volume_handles_four_dimensions() {
768 let front = vec![vec![0.0; 4], vec![0.5; 4]];
769 let result = hypervolume(&front, &reference(&[1.0; 4])).unwrap();
770 assert_eq!(result.estimate, HypervolumeEstimate::Exact(1.0));
771 assert_eq!(result.dominated_removed, 1);
772 }
773
774 #[test]
775 fn exact_two_dimensional_volume_matches_an_independent_cell_union() {
776 let hv_reference = reference(&[10.0, 10.0]);
777 let mut rng = Rng::new(19);
778 for case in 0..1_000 {
779 let point_count = 1 + case % 7;
780 let front: Vec<Vec<f64>> = (0..point_count)
781 .map(|_| {
782 vec![
783 (9.0 * rng.uniform01()).floor(),
784 (9.0 * rng.uniform01()).floor(),
785 ]
786 })
787 .collect();
788 let brute_force_cells = (0..10)
789 .flat_map(|x| (0..10).map(move |y| (x, y)))
790 .filter(|&(x, y)| {
791 front
792 .iter()
793 .any(|point| point[0] <= x as f64 && point[1] <= y as f64)
794 })
795 .count() as f64;
796 assert_eq!(
797 hypervolume(&front, &hv_reference).unwrap().estimate.value(),
798 brute_force_cells
799 );
800 }
801 }
802
803 #[test]
804 fn sampled_volume_agrees_with_exact_within_reported_uncertainty() {
805 let front = vec![
806 vec![0.1, 0.8, 0.8, 0.8],
807 vec![0.8, 0.1, 0.8, 0.8],
808 vec![0.8, 0.8, 0.1, 0.8],
809 vec![0.8, 0.8, 0.8, 0.1],
810 vec![0.5, 0.5, 0.5, 0.5],
811 ];
812 let exact = hypervolume(&front, &reference(&[1.0; 4]))
813 .unwrap()
814 .estimate
815 .value();
816 let sampled = hypervolume_monte_carlo(&front, &reference(&[1.0; 4]), 500_000, 7).unwrap();
817 let HypervolumeEstimate::MonteCarlo {
818 value,
819 standard_error,
820 samples,
821 seed,
822 } = sampled.estimate
823 else {
824 panic!("sampled API returned exact hypervolume")
825 };
826 assert_eq!(samples, 500_000);
827 assert_eq!(seed, 7);
828 assert!((value - exact).abs() <= 3.0 * standard_error);
829 }
830
831 #[test]
832 fn high_dimensional_default_is_typed_as_monte_carlo() {
833 let result = hypervolume(&[vec![0.0; 5]], &reference(&[1.0; 5])).unwrap();
834 assert!(matches!(
835 result.estimate,
836 HypervolumeEstimate::MonteCarlo {
837 samples: 100_000,
838 ..
839 }
840 ));
841 }
842
843 #[test]
844 fn distances_and_epsilon_match_hand_computed_fixture() {
845 let front = vec![vec![1.0, 2.0], vec![2.0, 1.0]];
846 let ideal = vec![vec![1.0, 1.0]];
847 assert_eq!(igd(&front, &ideal).unwrap(), 1.0);
848 assert_eq!(igd_plus(&front, &ideal).unwrap(), 1.0);
849 assert_eq!(gd(&front, &ideal).unwrap(), 1.0);
850 assert_eq!(gd_plus(&front, &ideal).unwrap(), 1.0);
851 assert_eq!(additive_epsilon(&front, &ideal).unwrap(), 1.0);
852 assert_eq!(spacing(&front).unwrap(), 0.0);
853 }
854
855 #[test]
856 fn nondominated_sort_preserves_indices_and_duplicates() {
857 let points = vec![
858 vec![0.0, 2.0],
859 vec![1.0, 1.0],
860 vec![2.0, 0.0],
861 vec![2.0, 2.0],
862 vec![1.0, 1.0],
863 vec![3.0, 3.0],
864 ];
865 assert_eq!(
866 nondominated_sort(&points).unwrap(),
867 vec![vec![0, 1, 2, 4], vec![3], vec![5]]
868 );
869 }
870
871 #[test]
872 fn crowding_distance_matches_nsga_fixture() {
873 let distance =
874 crowding_distance(&[vec![0.0, 2.0], vec![1.0, 1.0], vec![2.0, 0.0]]).unwrap();
875 assert!(distance[0].is_infinite());
876 assert_eq!(distance[1], 2.0);
877 assert!(distance[2].is_infinite());
878 assert_eq!(
879 crowding_distance(&[vec![1.0, 1.0], vec![1.0, 1.0], vec![1.0, 1.0]]).unwrap(),
880 vec![0.0; 3]
881 );
882 }
883
884 #[test]
885 fn translation_and_positive_scaling_have_correct_factors() {
886 fn transform(points: &[Vec<f64>], scale: f64, offset: f64) -> Vec<Vec<f64>> {
887 points
888 .iter()
889 .map(|point| point.iter().map(|value| scale * value + offset).collect())
890 .collect()
891 }
892
893 fn close(actual: f64, expected: f64) {
894 assert!((actual - expected).abs() <= 1.0e-12 * expected.abs().max(1.0));
895 }
896
897 let front = vec![vec![1.0, 4.0], vec![2.0, 2.0], vec![4.5, 1.0]];
898 let reference_set = vec![vec![0.5, 4.5], vec![2.5, 2.5], vec![4.5, 0.5]];
899 let extremes = vec![reference_set[0].clone(), reference_set[2].clone()];
900 let translated = transform(&front, 1.0, 10.0);
901 let translated_reference = transform(&reference_set, 1.0, 10.0);
902 let translated_extremes = transform(&extremes, 1.0, 10.0);
903 let scaled = transform(&front, 3.0, 0.0);
904 let scaled_reference = transform(&reference_set, 3.0, 0.0);
905 let scaled_extremes = transform(&extremes, 3.0, 0.0);
906 let base = hypervolume(&front, &reference(&[5.0, 5.0]))
907 .unwrap()
908 .estimate
909 .value();
910 let shifted = hypervolume(&translated, &reference(&[15.0, 15.0]))
911 .unwrap()
912 .estimate
913 .value();
914 let expanded = hypervolume(&scaled, &reference(&[15.0, 15.0]))
915 .unwrap()
916 .estimate
917 .value();
918 close(shifted, base);
919 close(expanded, 9.0 * base);
920
921 let distances = [
922 igd(&front, &reference_set).unwrap(),
923 igd_plus(&front, &reference_set).unwrap(),
924 gd(&front, &reference_set).unwrap(),
925 gd_plus(&front, &reference_set).unwrap(),
926 additive_epsilon(&front, &reference_set).unwrap(),
927 spacing(&front).unwrap(),
928 ];
929 let translated_distances = [
930 igd(&translated, &translated_reference).unwrap(),
931 igd_plus(&translated, &translated_reference).unwrap(),
932 gd(&translated, &translated_reference).unwrap(),
933 gd_plus(&translated, &translated_reference).unwrap(),
934 additive_epsilon(&translated, &translated_reference).unwrap(),
935 spacing(&translated).unwrap(),
936 ];
937 let scaled_distances = [
938 igd(&scaled, &scaled_reference).unwrap(),
939 igd_plus(&scaled, &scaled_reference).unwrap(),
940 gd(&scaled, &scaled_reference).unwrap(),
941 gd_plus(&scaled, &scaled_reference).unwrap(),
942 additive_epsilon(&scaled, &scaled_reference).unwrap(),
943 spacing(&scaled).unwrap(),
944 ];
945 for ((original, shifted), expanded) in distances
946 .into_iter()
947 .zip(translated_distances)
948 .zip(scaled_distances)
949 {
950 close(shifted, original);
951 close(expanded, 3.0 * original);
952 }
953 let base_spread = spread(&front, &extremes).unwrap();
954 close(
955 spread(&translated, &translated_extremes).unwrap(),
956 base_spread,
957 );
958 close(spread(&scaled, &scaled_extremes).unwrap(), base_spread);
959 }
960
961 #[test]
962 fn strict_dominance_improves_compliant_indicators_for_ten_thousand_pairs() {
963 let ideal = vec![vec![0.0, 0.0]];
964 let hv_reference = reference(&[2.0, 2.0]);
965 let mut rng = Rng::new(42);
966 for _ in 0..10_000 {
967 let worse = vec![vec![0.2 + rng.uniform01(), 0.2 + rng.uniform01()]];
968 let better = vec![vec![worse[0][0] - 0.1, worse[0][1] - 0.1]];
969 assert!(
970 hypervolume(&better, &hv_reference)
971 .unwrap()
972 .estimate
973 .value()
974 > hypervolume(&worse, &hv_reference).unwrap().estimate.value()
975 );
976 assert!(igd_plus(&better, &ideal).unwrap() < igd_plus(&worse, &ideal).unwrap());
977 assert!(
978 additive_epsilon(&better, &ideal).unwrap()
979 < additive_epsilon(&worse, &ideal).unwrap()
980 );
981 }
982 }
983
984 #[test]
985 fn invalid_input_fails_closed() {
986 assert_eq!(ReferencePoint::new(vec![]), Err(IndicatorError::EmptyPoint));
987 assert_eq!(
988 ReferencePoint::new(vec![f64::NAN]),
989 Err(IndicatorError::NonFiniteValue)
990 );
991 assert_eq!(
992 hypervolume(&[], &reference(&[1.0])),
993 Err(IndicatorError::EmptySet)
994 );
995 assert_eq!(
996 hypervolume(&[vec![2.0]], &reference(&[1.0])),
997 Err(IndicatorError::ReferencePointViolation)
998 );
999 assert_eq!(
1000 igd(&[vec![0.0]], &[vec![0.0, 1.0]]),
1001 Err(IndicatorError::DimensionMismatch)
1002 );
1003 assert_eq!(nondominated_sort(&[]), Err(IndicatorError::EmptySet));
1004 assert_eq!(
1005 crowding_distance(&[vec![0.0], vec![f64::INFINITY]]),
1006 Err(IndicatorError::NonFiniteValue)
1007 );
1008 }
1009}