helena 0.1.0

Core types and component interfaces for helena, a latent data-to-waveform generation platform.
Documentation
use std::num::NonZeroUsize;

use proptest::prelude::*;

use super::*;

fn steps(n: usize) -> NonZeroUsize {
    NonZeroUsize::new(n).unwrap()
}

/// A `conds x seeds` grid of `elems`-wide cells filled row-major from `vals`.
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() {
    // One condition, two seeds, one element. Cell distances: |1-2| and |3-7| = 1, 4.
    let reference = grid(1, 2, 1, &[1.0, 3.0]);
    let candidate = grid(1, 2, 1, &[2.0, 7.0]);
    // mean of (1, 4) = 2.5
    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() {
    // Cell (0,0) is [1,1] in both grids and cell (1,0) is [2,1] in both, so each pair
    // matches; but the grid-wide shape differs, which would average incomparable L2
    // scales. The grid-wide shape check rejects it.
    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(_))
    ));
}

// `rejects_tokens_and_non_finite` is gone: a grid of `Latent<Continuous>` cannot
// hold tokens, and a `FrameSeq` cell cannot hold a NaN, so both rejections are
// unrepresentable.

#[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]);
    // Passed out of order; the curve sorts ascending by step budget.
    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);
    // Fewer steps -> larger gap -> the curve converges as the budget grows.
    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]);
    // Larger budget is *further* from the reference: not converging.
    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() {
    // Two conditions, two seeds. Per-cell |ref - cand|: cond 0 -> (1, 4), cond 1 -> (2, 2).
    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]);
    // `grid_distance` is exactly the pooled mean of these cells: (1 + 4 + 2 + 2) / 4.
    assert!((grid_distance(&reference, &candidate).unwrap() - 2.25).abs() < 1e-6);
}

#[test]
fn cell_distances_share_grid_distance_validation() {
    // The two share one core, so a misalignment rejected by `grid_distance` is rejected
    // here too (condition-count mismatch stands in for the whole boundary).
    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}");
    }
}