use proptest::prelude::*;
use super::*;
fn arb_samples() -> impl Strategy<Value = Vec<Latent<Continuous>>> {
(1usize..4, 1usize..5).prop_flat_map(|(elems, count)| {
proptest::collection::vec(proptest::collection::vec(-10.0f32..10.0, elems), count)
.prop_map(move |rows| rows.into_iter().map(|r| latent([elems, 1], r)).collect())
})
}
proptest! {
#[test]
fn dispersion_is_nonnegative(samples in arb_samples()) {
prop_assert!(sample_dispersion(&samples).unwrap() >= 0.0);
}
#[test]
fn dispersion_is_shift_invariant(samples in arb_samples(), shift in -5.0f32..5.0) {
let base = sample_dispersion(&samples).unwrap();
let shifted: Vec<Latent<Continuous>> = samples
.iter()
.map(|s| {
let t = s.repr();
latent(
[t.values().len(), 1],
t.values().iter().map(|v| v + shift).collect(),
)
})
.collect();
let after = sample_dispersion(&shifted).unwrap();
prop_assert!((base - after).abs() <= 1e-3 * base.max(1.0), "{base} vs {after}");
}
#[test]
fn dispersion_scales_quadratically(samples in arb_samples(), scale in 0.0f32..4.0) {
let base = sample_dispersion(&samples).unwrap();
let scaled: Vec<Latent<Continuous>> = samples
.iter()
.map(|s| {
let t = s.repr();
latent([t.values().len(), 1], t.values().iter().map(|v| v * scale).collect())
})
.collect();
let after = sample_dispersion(&scaled).unwrap();
let want = base * scale * scale;
prop_assert!((want - after).abs() <= 1e-2 * want.max(1.0), "{want} vs {after}");
}
#[test]
fn within_plus_between_equals_pooled(
elems in 1usize..4,
conds in 1usize..4,
seeds in 1usize..4,
values in proptest::collection::vec(-6.0f32..6.0, 1..48),
) {
let per_cond = seeds * elems;
let need = conds * per_cond;
prop_assume!(values.len() >= need);
let mut grid: Vec<Vec<Latent<Continuous>>> = Vec::with_capacity(conds);
let mut k = 0;
let mut pooled: Vec<Latent<Continuous>> = Vec::with_capacity(conds * seeds);
for _ in 0..conds {
let mut row = Vec::with_capacity(seeds);
for _ in 0..seeds {
let chunk = values[k..k + elems].to_vec();
k += elems;
let l = latent([elems, 1], chunk);
row.push(l.clone());
pooled.push(l);
}
grid.push(row);
}
let d = GenerationDiagnostics::measure(&grid).unwrap();
let pooled_disp = sample_dispersion(&pooled).unwrap();
let sum = d.within_condition() + d.between_condition();
prop_assert!(
(sum - pooled_disp).abs() <= 1e-3 * pooled_disp.max(1.0),
"within+between {sum} vs pooled {pooled_disp}"
);
}
#[test]
fn response_curve_invariants(values in proptest::collection::vec(-6.0f32..6.0, 2..16)) {
let r = ResponseCurve::measure(&scalar_sweep(&values)).unwrap();
prop_assert!(r.net_response() >= 0.0);
prop_assert!((0.0..=1.0).contains(&r.monotonicity()), "{}", r.monotonicity());
}
#[test]
fn monotone_sweep_is_maximally_straight(start in -5.0f32..5.0, steps in 2usize..12, gap in 0.1f32..3.0) {
let values: Vec<f32> = (0..steps).map(|k| start + k as f32 * gap).collect();
let r = ResponseCurve::measure(&scalar_sweep(&values)).unwrap();
let want = gap * (steps - 1) as f32;
prop_assert!((r.net_response() - want).abs() <= 1e-3 * want.max(1.0));
prop_assert!((r.monotonicity() - 1.0).abs() <= 1e-4, "{}", r.monotonicity());
}
}