use std::num::NonZeroUsize;
use proptest::prelude::*;
use super::*;
fn steps(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).unwrap()
}
fn grid(conds: usize, seeds: usize, elems: usize, vals: &[f32]) -> Vec<Vec<Latent<Continuous>>> {
assert_eq!(vals.len(), conds * seeds * elems);
let mut k = 0;
(0..conds)
.map(|_| {
(0..seeds)
.map(|_| {
let cell = latent([elems, 1], vals[k..k + elems].to_vec());
k += elems;
cell
})
.collect()
})
.collect()
}
#[test]
fn identical_grids_have_zero_distance() {
let g = grid(2, 2, 2, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
assert_eq!(grid_distance(&g, &g).unwrap(), 0.0);
}
#[test]
fn distance_matches_hand_computed_mean() {
let reference = grid(1, 2, 1, &[1.0, 3.0]);
let candidate = grid(1, 2, 1, &[2.0, 7.0]);
assert!((grid_distance(&reference, &candidate).unwrap() - 2.5).abs() < 1e-6);
}
#[test]
fn rejects_empty_reference() {
let candidate = grid(1, 1, 1, &[0.0]);
assert!(matches!(
grid_distance(&[], &candidate),
Err(Error::Validation(_))
));
}
#[test]
fn rejects_condition_count_mismatch() {
let reference = grid(2, 1, 1, &[1.0, 2.0]);
let candidate = grid(1, 1, 1, &[1.0]);
assert!(matches!(
grid_distance(&reference, &candidate),
Err(Error::Validation(_))
));
}
#[test]
fn rejects_seed_budget_mismatch() {
let reference = grid(1, 2, 1, &[1.0, 2.0]);
let candidate = grid(1, 1, 1, &[1.0]);
assert!(matches!(
grid_distance(&reference, &candidate),
Err(Error::Validation(_))
));
}
#[test]
fn rejects_cell_shape_mismatch() {
let reference = grid(1, 1, 2, &[1.0, 2.0]);
let candidate = grid(1, 1, 1, &[1.0]);
assert!(matches!(
grid_distance(&reference, &candidate),
Err(Error::Validation(_))
));
}
#[test]
fn rejects_ragged_grids_even_when_pairwise_consistent() {
let reference = vec![
vec![latent([1, 1], vec![0.0])],
vec![latent([2, 1], vec![0.0, 0.0])],
];
let candidate = vec![
vec![latent([1, 1], vec![1.0])],
vec![latent([2, 1], vec![1.0, 1.0])],
];
assert!(matches!(
grid_distance(&reference, &candidate),
Err(Error::Validation(_))
));
}
#[test]
fn curve_orders_gaps_and_scores_each_budget() {
let reference = grid(1, 1, 1, &[0.0]);
let near = grid(1, 1, 1, &[1.0]);
let far = grid(1, 1, 1, &[4.0]);
let curve = StepGapCurve::measure(
steps(64),
&reference,
&[(steps(16), &near), (steps(4), &far)],
)
.unwrap();
assert_eq!(curve.reference_steps(), steps(64));
let g = curve.gaps();
assert_eq!(g[0].steps(), steps(4));
assert_eq!(g[0].gap(), 4.0);
assert_eq!(g[1].steps(), steps(16));
assert_eq!(g[1].gap(), 1.0);
assert!(curve.is_converging());
}
#[test]
fn curve_detects_non_convergence() {
let reference = grid(1, 1, 1, &[0.0]);
let far = grid(1, 1, 1, &[4.0]);
let near = grid(1, 1, 1, &[1.0]);
let curve = StepGapCurve::measure(
steps(64),
&reference,
&[(steps(4), &near), (steps(16), &far)],
)
.unwrap();
assert!(!curve.is_converging());
}
#[test]
fn curve_rejects_empty_and_duplicate_budgets() {
let reference = grid(1, 1, 1, &[0.0]);
assert!(matches!(
StepGapCurve::measure(steps(64), &reference, &[]),
Err(Error::Validation(_))
));
let a = grid(1, 1, 1, &[1.0]);
let b = grid(1, 1, 1, &[2.0]);
assert!(matches!(
StepGapCurve::measure(steps(64), &reference, &[(steps(4), &a), (steps(4), &b)]),
Err(Error::Validation(_))
));
}
#[test]
fn cell_distances_are_clustered_by_condition() {
let reference = grid(2, 2, 1, &[0.0, 0.0, 0.0, 0.0]);
let candidate = grid(2, 2, 1, &[1.0, 4.0, 2.0, 2.0]);
let clusters = grid_cell_distances(&reference, &candidate).unwrap();
assert_eq!(clusters.len(), 2, "one cluster per condition");
assert_eq!(clusters[0], vec![1.0, 4.0]);
assert_eq!(clusters[1], vec![2.0, 2.0]);
assert!((grid_distance(&reference, &candidate).unwrap() - 2.25).abs() < 1e-6);
}
#[test]
fn cell_distances_share_grid_distance_validation() {
let reference = grid(2, 1, 1, &[1.0, 2.0]);
let candidate = grid(1, 1, 1, &[1.0]);
assert!(matches!(
grid_cell_distances(&reference, &candidate),
Err(Error::Validation(_))
));
}
proptest! {
#[test]
fn self_distance_is_zero(
conds in 1usize..3,
seeds in 1usize..4,
elems in 1usize..4,
seed in -10.0f32..10.0,
) {
let vals: Vec<f32> = (0..conds * seeds * elems)
.map(|i| seed + i as f32)
.collect();
let g = grid(conds, seeds, elems, &vals);
prop_assert_eq!(grid_distance(&g, &g).unwrap(), 0.0);
}
#[test]
fn distance_is_symmetric_and_nonnegative(
conds in 1usize..3,
seeds in 1usize..3,
elems in 1usize..3,
a in proptest::collection::vec(-8.0f32..8.0, 18),
b in proptest::collection::vec(-8.0f32..8.0, 18),
) {
let n = conds * seeds * elems;
let ga = grid(conds, seeds, elems, &a[..n]);
let gb = grid(conds, seeds, elems, &b[..n]);
let ab = grid_distance(&ga, &gb).unwrap();
let ba = grid_distance(&gb, &ga).unwrap();
prop_assert!(ab >= 0.0);
prop_assert!((ab - ba).abs() <= 1e-4 * ab.max(1.0), "{ab} vs {ba}");
}
}