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 crate::diffusion::NoiseLevel;
use crate::eval::ClusterBootstrap;

use super::*;

fn level(gamma: f32) -> NoiseLevel {
    NoiseLevel::from_signal_variance(gamma).unwrap()
}

fn bootstrap() -> ClusterBootstrap {
    ClusterBootstrap::new(NonZeroUsize::new(256).unwrap(), 0.95, 7).unwrap()
}

/// A clean-target loss at `gamma` whose value is `loss` (a one-element
/// estimate/target pair with squared difference `loss`). Pass perfect squares for
/// an exact `loss`, since the estimate is `sqrt(loss)` and `f32` rounds otherwise.
fn loss_at(gamma: f32, loss: f32) -> CleanTargetLoss {
    let target = Tensor::vector(vec![0.0]);
    let estimate = Tensor::vector(vec![loss.sqrt()]);
    CleanTargetLoss::from_estimate(level(gamma), &estimate, &target).unwrap()
}

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

#[test]
fn estimate_equal_to_target_has_zero_loss() {
    let t = Tensor::new([2, 2], vec![1.0, -2.0, 3.0, 0.5]).unwrap();
    let l = CleanTargetLoss::from_estimate(level(0.5), &t, &t).unwrap();
    assert_eq!(l.loss(), 0.0);
    assert_eq!(l.level(), level(0.5));
}

#[test]
fn estimate_loss_matches_hand_computed_mse() {
    let estimate = Tensor::new([1, 2], vec![1.0, 4.0]).unwrap();
    let target = Tensor::new([1, 2], vec![0.0, 0.0]).unwrap();
    // mean of (1, 16) = 8.5
    let l = CleanTargetLoss::from_estimate(level(0.3), &estimate, &target).unwrap();
    assert!((l.loss() - 8.5).abs() < 1e-9);
}

#[test]
fn from_velocity_recovers_the_true_target_exactly() {
    // x_t = diffuse(x0, eps); v = the true velocity target. The recovery is exact,
    // so the clean-target loss is ~0 regardless of the noise level.
    let x0 = Tensor::new([2, 2], vec![0.7, -0.3, 0.4, 0.1]).unwrap();
    let eps = Tensor::new([2, 2], vec![-1.0, 0.5, 0.2, -0.8]).unwrap();
    for gamma in [0.05, 0.5, 0.95] {
        let lvl = level(gamma);
        let x_t = lvl.diffuse(&x0, &eps).unwrap();
        let v = lvl.velocity_target(&x0, &eps).unwrap();
        let l = CleanTargetLoss::from_velocity(lvl, &x_t, &v, &x0).unwrap();
        assert!(l.loss() < 1e-9, "gamma {gamma}: loss {}", l.loss());
    }
}

#[test]
fn high_snr_recovery_is_target_regardless_of_velocity() {
    // At gamma = 1 the noisy sample *is* the target, so any v_hat recovers it: the
    // structural reason the high-SNR bins are cheap and the unweighted objective
    // overspends there.
    let x0 = Tensor::new([1, 3], vec![0.2, -0.5, 0.9]).unwrap();
    let eps = Tensor::zeros([1, 3]).unwrap();
    let lvl = NoiseLevel::pure_signal();
    let x_t = lvl.diffuse(&x0, &eps).unwrap();
    let arbitrary_v = Tensor::new([1, 3], vec![5.0, -7.0, 3.0]).unwrap();
    let l = CleanTargetLoss::from_velocity(lvl, &x_t, &arbitrary_v, &x0).unwrap();
    assert!(l.loss() < 1e-12, "loss {}", l.loss());
}

#[test]
fn from_estimate_rejects_shape_mismatch() {
    let a = Tensor::new([1, 2], vec![0.0, 0.0]).unwrap();
    let b = Tensor::new([2, 1], vec![0.0, 0.0]).unwrap();
    assert!(matches!(
        CleanTargetLoss::from_estimate(level(0.5), &a, &b),
        Err(Error::Validation(_))
    ));
}

#[test]
fn from_estimate_rejects_non_finite_and_empty() {
    let target = Tensor::new([1, 2], vec![0.0, 0.0]).unwrap();
    let nan = Tensor::new([1, 2], vec![f32::NAN, 1.0]).unwrap();
    assert!(matches!(
        CleanTargetLoss::from_estimate(level(0.5), &nan, &target),
        Err(Error::Validation(_))
    ));
    let empty = Tensor::new([0, 2], vec![]).unwrap();
    assert!(matches!(
        CleanTargetLoss::from_estimate(level(0.5), &empty, &empty),
        Err(Error::Validation(_))
    ));
}

#[test]
fn curve_rejects_empty_losses() {
    assert!(matches!(
        PerSnrLossCurve::measure(&[], bins(4)),
        Err(Error::Validation(_))
    ));
}

#[test]
fn curve_assigns_losses_to_expected_bins() {
    // Perfect-square losses keep the `sqrt` round-trip exact (see `loss_at`).
    let losses = [
        loss_at(0.1, 1.0),  // bin 0: [0.00, 0.25)
        loss_at(0.3, 4.0),  // bin 1: [0.25, 0.50)
        loss_at(0.6, 9.0),  // bin 2: [0.50, 0.75)
        loss_at(0.9, 16.0), // bin 3: [0.75, 1.00)
        loss_at(1.0, 4.0),  // gamma == 1 folds into the top bin
    ];
    let curve = PerSnrLossCurve::measure(&losses, bins(4)).unwrap();
    assert_eq!(curve.observation_count(), 5);
    let b = curve.bins();
    assert_eq!(b.len(), 4);
    assert_eq!(b[0].count(), 1);
    assert_eq!(b[0].mean_loss(), Some(1.0));
    assert_eq!(b[1].mean_loss(), Some(4.0));
    assert_eq!(b[2].mean_loss(), Some(9.0));
    assert_eq!(b[3].count(), 2);
    assert_eq!(b[3].mean_loss(), Some(10.0)); // (16 + 4) / 2
}

#[test]
fn empty_bins_report_no_mean() {
    let curve = PerSnrLossCurve::measure(&[loss_at(0.6, 4.0)], bins(4)).unwrap();
    let b = curve.bins();
    assert_eq!(b[0].count(), 0);
    assert_eq!(b[0].mean_loss(), None);
    assert_eq!(b[2].mean_loss(), Some(4.0));
}

#[test]
fn bin_ranges_partition_the_unit_interval() {
    let curve = PerSnrLossCurve::measure(&[loss_at(0.5, 1.0)], bins(5)).unwrap();
    let b = curve.bins();
    assert_eq!(b[0].range().0, 0.0);
    assert!((b[4].range().1 - 1.0).abs() < 1e-6);
    for w in b.windows(2) {
        assert_eq!(w[0].range().1, w[1].range().0);
    }
}

#[test]
fn bin_intervals_center_on_bin_means() {
    // Two items, each scored at one gamma in bin 0 ([0, 0.25)) and one in bin 2
    // ([0.5, 0.75)). The interval is the pooled-mean bootstrap, so its point is the
    // bin's pooled `mean_loss`; with one loss per item per bin the clusters are
    // singletons, so the pooled and equal-weight statistics coincide either way.
    let per_item = vec![
        vec![loss_at(0.1, 1.0), loss_at(0.6, 4.0)],
        vec![loss_at(0.1, 9.0), loss_at(0.6, 16.0)],
    ];
    let curve = PerSnrLossCurve::measure(&per_item.concat(), bins(4)).unwrap();
    let intervals = PerSnrLossCurve::bin_intervals(&per_item, bins(4), &bootstrap()).unwrap();
    assert_eq!(intervals.len(), curve.bins().len());
    for (bin, ci) in curve.bins().iter().zip(&intervals) {
        match (bin.mean_loss(), ci) {
            (Some(mean), Some(ci)) => {
                assert!(
                    (ci.point() - mean).abs() < 1e-12,
                    "{} vs {mean}",
                    ci.point()
                );
                // Bounds stay inside the bin's own observation range.
                assert!(ci.lo() <= ci.point() && ci.point() <= ci.hi());
            }
            (None, None) => {}
            other => panic!("bin/interval populated-ness disagree: {other:?}"),
        }
    }
    // Bin 0 pools 1 and 9; bin 2 pools 4 and 16.
    assert_eq!(intervals[0].unwrap().point(), 5.0);
    assert_eq!(intervals[2].unwrap().point(), 10.0);
    assert!(intervals[1].is_none() && intervals[3].is_none());
}

#[test]
fn bin_intervals_center_on_pooled_mean_under_unequal_items() {
    // A conditioning item that segments into two windows contributes two losses to a
    // bin; another item contributes one. The interval must stay centered on the bin's
    // pooled `mean_loss` (observation-weighted), not the equal-weight mean of per-item
    // means, even though the clusters are now unequal in size. This is the centering
    // half of the EVAL-8 review fix: windows of one item cluster together, but the
    // headline weighting they bracket is pooled.
    // Perfect-square losses keep the `sqrt` round-trip exact (see `loss_at`).
    let per_item = vec![
        vec![loss_at(0.1, 1.0), loss_at(0.1, 1.0)], // one item, two windows -> bin 0
        vec![loss_at(0.1, 16.0)],                   // another item, one window -> bin 0
    ];
    let curve = PerSnrLossCurve::measure(&per_item.concat(), bins(4)).unwrap();
    let intervals = PerSnrLossCurve::bin_intervals(&per_item, bins(4), &bootstrap()).unwrap();
    // Pooled bin-0 mean is (1 + 1 + 16) / 3 = 6.0; the equal-weight mean of per-item
    // means would instead be ((1 + 1) / 2 + 16) / 2 = 8.5. The interval tracks the pooled.
    assert_eq!(curve.bins()[0].mean_loss(), Some(6.0));
    let ci = intervals[0].unwrap();
    assert!(
        (ci.point() - 6.0).abs() < 1e-12,
        "point {} should equal the pooled bin mean 6.0",
        ci.point()
    );
    assert!(ci.lo() <= ci.point() && ci.point() <= ci.hi());
    assert!(intervals[1].is_none() && intervals[2].is_none() && intervals[3].is_none());
}

#[test]
fn bin_intervals_reject_no_losses() {
    assert!(matches!(
        PerSnrLossCurve::bin_intervals(&[], bins(4), &bootstrap()),
        Err(Error::Validation(_))
    ));
    assert!(matches!(
        PerSnrLossCurve::bin_intervals(&[vec![], vec![]], bins(4), &bootstrap()),
        Err(Error::Validation(_))
    ));
}

proptest! {
    #[test]
    fn true_velocity_recovery_is_exact(
        x0 in proptest::collection::vec(-5.0f32..5.0, 4),
        eps in proptest::collection::vec(-5.0f32..5.0, 4),
        gamma in 0.0f32..=1.0,
    ) {
        let x0 = Tensor::new([2, 2], x0).unwrap();
        let eps = Tensor::new([2, 2], eps).unwrap();
        let lvl = level(gamma);
        let x_t = lvl.diffuse(&x0, &eps).unwrap();
        let v = lvl.velocity_target(&x0, &eps).unwrap();
        let l = CleanTargetLoss::from_velocity(lvl, &x_t, &v, &x0).unwrap();
        prop_assert!(l.loss() < 1e-4, "loss {} at gamma {gamma}", l.loss());
    }

    #[test]
    fn curve_conserves_observations(
        gammas in proptest::collection::vec(0.0f32..=1.0, 1..32),
        n in 1usize..8,
    ) {
        let losses: Vec<CleanTargetLoss> = gammas.iter().map(|&g| loss_at(g, 1.0)).collect();
        let curve = PerSnrLossCurve::measure(&losses, bins(n)).unwrap();
        prop_assert_eq!(curve.observation_count(), losses.len());
        prop_assert_eq!(curve.bins().len(), n);
    }

    #[test]
    fn bin_means_are_bounded_by_their_members(
        values in proptest::collection::vec((0.0f32..=1.0, 0.0f32..9.0), 1..24),
        n in 1usize..6,
    ) {
        let losses: Vec<CleanTargetLoss> =
            values.iter().map(|&(g, v)| loss_at(g, v)).collect();
        let curve = PerSnrLossCurve::measure(&losses, bins(n)).unwrap();
        for bin in curve.bins() {
            if let Some(mean) = bin.mean_loss() {
                // every observed loss is in [0, 9], so a bin mean must be too.
                prop_assert!((0.0..=9.0 + 1e-3).contains(&mean), "mean {mean}");
            } else {
                prop_assert_eq!(bin.count(), 0);
            }
        }
    }
}