helena 0.1.0

Core types and component interfaces for helena, a latent data-to-waveform generation platform.
Documentation
use proptest::prelude::*;

use super::*;

#[test]
fn streaming_fit_matches_iterator_fit() {
    let a = seq(2, 2, vec![0.0, 10.0, 2.0, 20.0]);
    let b = seq(1, 2, vec![4.0, 30.0]);

    let buffered = LatentNorm::fit([&a, &b], LatentNorm::DEFAULT_EPS).unwrap();

    let mut fit = NormFitter::new();
    fit.push(&a).unwrap();
    fit.push(&b).unwrap();
    // `finish` floors at `DEFAULT_EPS`, matching the `fit` call above.
    let streamed = fit.finish().unwrap();

    assert_eq!(streamed, buffered);
}

#[test]
fn dim_and_is_empty_track_state() {
    let mut fit = NormFitter::new();
    assert!(fit.is_empty());
    assert_eq!(fit.dim(), None);

    let latent = seq(2, 3, vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
    fit.push(&latent).unwrap();
    assert!(!fit.is_empty());
    assert_eq!(fit.dim(), Some(3));
}

#[test]
fn push_fixes_dimension_then_rejects_mismatch() {
    let a = seq(1, 2, vec![0.0, 0.0]);
    let b = seq(1, 3, vec![0.0, 0.0, 0.0]);
    let mut fit = NormFitter::new();
    fit.push(&a).unwrap();
    assert!(
        matches!(fit.push(&b), Err(Error::Shape { expected, actual })
        if expected == vec![1, 2] && actual == vec![1, 3])
    );
}

// The old `push_rejects_tokens_and_nonfinite` test is deleted: a token latent
// can never reach `push` (it takes a `FrameSeq`, which is continuous by type),
// and a non-finite value can never reach it either (finiteness is the
// `FrameSeq` invariant). Both rejections are unrepresentable, not tested-for.

#[test]
fn failed_push_leaves_builder_unchanged() {
    // The only way a `push` can fail is a dimension mismatch, and it is caught
    // before any accumulator is touched — so an errored push is a no-op and
    // `finish` cannot emit a corrupt artifact.
    let a2 = seq(1, 2, vec![0.0, 0.0]);
    let mismatched = seq(1, 3, vec![0.0, 0.0, 0.0]);
    let mut fit = NormFitter::new();
    assert!(fit.push(&mismatched).is_ok()); // first push fixes dim = 3

    // A later mismatched push leaves the earlier stats intact.
    fit.push(&mismatched).unwrap();
    let snapshot = fit.clone().finish().unwrap();
    assert!(fit.push(&a2).is_err());
    let after = fit.finish().unwrap();
    assert_eq!(after, snapshot);
}

#[test]
fn finish_rejects_empty_or_frameless_builder() {
    assert!(matches!(
        NormFitter::new().finish(),
        Err(Error::Validation(_))
    ));
    // Zero frames is a valid `FrameSeq` (emptiness is structural), so a builder
    // can see a sequence yet contribute no frames — `finish` must still reject.
    let empty = seq(0, 2, vec![]);
    let mut fit = NormFitter::new();
    fit.push(&empty).unwrap();
    assert!(matches!(fit.finish(), Err(Error::Validation(_))));
}

proptest! {
    /// Folding latents through the builder one at a time is bit-for-bit the same
    /// as the buffered iterator [`LatentNorm::fit`]: both drive the same Welford
    /// accumulators in the same order.
    #[test]
    fn streaming_equals_buffered_prop(
        flat in prop::collection::vec(-1.0e3f32..1.0e3, 2..64),
        split in 1usize..8,
    ) {
        let dim = 2usize;
        let rows = flat.len() / dim;
        prop_assume!(rows >= 1);
        let data = flat[..rows * dim].to_vec();

        // Chop the frames into up to `split` latents of the same width.
        let chunk = rows.div_ceil(split).max(1);
        let mut latents = Vec::new();
        let mut f = 0;
        while f < rows {
            let take = chunk.min(rows - f);
            let slice = data[f * dim..(f + take) * dim].to_vec();
            latents.push(seq(take, dim, slice));
            f += take;
        }

        let buffered = LatentNorm::fit(latents.iter(), LatentNorm::DEFAULT_EPS).unwrap();
        let mut fit = NormFitter::new();
        for latent in &latents {
            fit.push(latent).unwrap();
        }
        let streamed = fit.finish().unwrap();

        prop_assert_eq!(streamed, buffered);
    }
}