Files
heuropt/tests/metric_properties.rs
T
swaits 8a8c32f125 test(proptest): massive property-test expansion for every algorithm and operator
Goes from 10 properties to 50+, organized into four files:

- tests/properties.rs (existing) — Pareto-utility invariants
- tests/algorithm_properties.rs (new) — every Optimizer impl gets:
  * determinism-with-seed property
  * no-panic-on-random-valid-input property
  * population-size-as-documented property where applicable
- tests/operator_properties.rs (new) — every Variation/Initializer/
  Repair impl gets the right size + in-bounds + no-panic properties
- tests/metric_properties.rs (new) — every metric gets monotonicity
  / non-negativity / dim-checking properties
- tests/numerical_stability.rs (new) — single-point populations,
  duplicate populations, near-zero bounds, very large bounds,
  algorithms-on-flat-fitness — none of which should panic.

Total: 226 unit tests + this much-larger property suite. Strategies
are factored into a small `prop_helpers` module shared across files
so the random-input generators stay consistent.
2026-05-05 11:01:36 -06:00

122 lines
4.1 KiB
Rust

//! Per-metric property tests for the Pareto-quality metrics.
use proptest::prelude::*;
use heuropt::core::candidate::Candidate;
use heuropt::core::evaluation::Evaluation;
use heuropt::core::objective::{Objective, ObjectiveSpace};
use heuropt::metrics::hypervolume::{hypervolume_2d, hypervolume_nd};
use heuropt::metrics::spacing::spacing;
fn space_2d() -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
}
fn space_3d() -> ObjectiveSpace {
ObjectiveSpace::new(vec![
Objective::minimize("f1"),
Objective::minimize("f2"),
Objective::minimize("f3"),
])
}
fn cand_2d(a: f64, b: f64) -> Candidate<()> {
Candidate::new((), Evaluation::new(vec![a, b]))
}
fn cand_3d(a: f64, b: f64, c: f64) -> Candidate<()> {
Candidate::new((), Evaluation::new(vec![a, b, c]))
}
proptest! {
/// hypervolume_2d is non-negative.
#[test]
fn hv2_non_negative(
front in prop::collection::vec((0.0_f64..10.0, 0.0_f64..10.0), 0..15),
) {
let s = space_2d();
let pop: Vec<Candidate<()>> = front.iter().map(|&(a, b)| cand_2d(a, b)).collect();
let hv = hypervolume_2d(&pop, &s, [11.0, 11.0]);
prop_assert!(hv >= 0.0);
prop_assert!(hv.is_finite());
}
/// hypervolume_2d is bounded above by the (reference - 0)² = 121 box.
#[test]
fn hv2_bounded_by_box(
front in prop::collection::vec((0.0_f64..10.0, 0.0_f64..10.0), 1..15),
) {
let s = space_2d();
let pop: Vec<Candidate<()>> = front.iter().map(|&(a, b)| cand_2d(a, b)).collect();
let hv = hypervolume_2d(&pop, &s, [11.0, 11.0]);
prop_assert!(hv <= 121.0_f64 + 1e-9);
}
/// Adding a dominated point doesn't change hypervolume_2d.
#[test]
fn hv2_dominated_invariant(
a in 0.0_f64..5.0,
b in 0.0_f64..5.0,
d_offset in 0.001_f64..3.0,
) {
let s = space_2d();
let base = vec![cand_2d(a, b)];
let mut with_dominated = base.clone();
// (a + offset, b + offset) is strictly worse than (a, b) on both
// axes, so it's dominated.
with_dominated.push(cand_2d(a + d_offset, b + d_offset));
let hv1 = hypervolume_2d(&base, &s, [11.0, 11.0]);
let hv2 = hypervolume_2d(&with_dominated, &s, [11.0, 11.0]);
prop_assert!((hv1 - hv2).abs() < 1e-9);
}
/// hypervolume_nd agrees with hypervolume_2d on 2-D inputs.
#[test]
fn hv_nd_matches_2d(
front in prop::collection::vec((0.0_f64..10.0, 0.0_f64..10.0), 1..10),
) {
let s = space_2d();
let pop: Vec<Candidate<()>> = front.iter().map(|&(a, b)| cand_2d(a, b)).collect();
let hv2 = hypervolume_2d(&pop, &s, [11.0, 11.0]);
let hvn = hypervolume_nd(&pop, &s, &[11.0, 11.0]);
prop_assert!((hv2 - hvn).abs() < 1e-9, "{hv2} vs {hvn}");
}
/// hypervolume_nd in 3-D is non-negative and bounded.
#[test]
fn hv3_non_negative_bounded(
pts in prop::collection::vec(
(0.0_f64..2.0, 0.0_f64..2.0, 0.0_f64..2.0),
0..10,
),
) {
let s = space_3d();
let pop: Vec<Candidate<()>> = pts.iter().map(|&(a, b, c)| cand_3d(a, b, c)).collect();
let hv = hypervolume_nd(&pop, &s, &[3.0, 3.0, 3.0]);
prop_assert!(hv >= 0.0);
prop_assert!(hv.is_finite());
// Reference box has volume 27.
prop_assert!(hv <= 27.0 + 1e-9);
}
/// spacing is non-negative and zero on a single point.
#[test]
fn spacing_non_negative(
front in prop::collection::vec((0.0_f64..10.0, 0.0_f64..10.0), 0..15),
) {
let s = space_2d();
let pop: Vec<Candidate<()>> = front.iter().map(|&(a, b)| cand_2d(a, b)).collect();
let sp = spacing(&pop, &s);
prop_assert!(sp >= 0.0);
prop_assert!(sp.is_finite());
}
/// spacing on a single-point front is exactly 0.
#[test]
fn spacing_single_point_is_zero(a in 0.0_f64..10.0, b in 0.0_f64..10.0) {
let s = space_2d();
let pop = vec![cand_2d(a, b)];
let sp = spacing(&pop, &s);
prop_assert_eq!(sp, 0.0);
}
}