helena 0.1.0

Core types and component interfaces for helena, a latent data-to-waveform generation platform.
Documentation
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());
    }
}