ruvector-dither 0.1.0

Deterministic low-discrepancy dithering for low-bit quantization: golden-ratio and π-digit sequences for blue-noise error shaping
Documentation
//! Golden-ratio quasi-random dither sequence.
//!
//! State update: `state = frac(state + φ)` where φ = (√5−1)/2 ≈ 0.618…
//!
//! This is the 1-D Halton sequence in base φ — it has the best possible
//! equidistribution for a 1-D low-discrepancy sequence.

use crate::DitherSource;

/// Additive golden-ratio dither with zero-mean output in `[-0.5, 0.5]`.
///
/// The sequence has period 1 (irrational) so it never exactly repeats.
/// Two instances with different seeds stay decorrelated.
#[derive(Clone, Debug)]
pub struct GoldenRatioDither {
    state: f32,
}

/// φ = (√5 − 1) / 2
const PHI: f32 = 0.618_033_98_f32;

impl GoldenRatioDither {
    /// Create a new sequence seeded at `initial_state` ∈ [0, 1).
    ///
    /// For per-layer / per-channel decorrelation, seed with
    /// `frac(layer_id × φ + channel_id × φ²)`.
    #[inline]
    pub fn new(initial_state: f32) -> Self {
        Self { state: initial_state.abs().fract() }
    }

    /// Construct from a `(layer_id, channel_id)` pair for structural decorrelation.
    #[inline]
    pub fn from_ids(layer_id: u32, channel_id: u32) -> Self {
        let s = ((layer_id as f32) * PHI + (channel_id as f32) * PHI * PHI).fract();
        Self { state: s }
    }

    /// Current state (useful for serialisation / checkpointing).
    #[inline]
    pub fn state(&self) -> f32 {
        self.state
    }
}

impl DitherSource for GoldenRatioDither {
    /// Advance and return next value in `[-0.5, 0.5]`.
    #[inline]
    fn next_unit(&mut self) -> f32 {
        self.state = (self.state + PHI).fract();
        self.state - 0.5
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::DitherSource;

    #[test]
    fn output_is_in_range() {
        let mut d = GoldenRatioDither::new(0.0);
        for _ in 0..10_000 {
            let v = d.next_unit();
            assert!(v >= -0.5 && v <= 0.5, "out of range: {v}");
        }
    }

    #[test]
    fn mean_is_near_zero() {
        let mut d = GoldenRatioDither::new(0.0);
        let n = 100_000;
        let mean: f32 = (0..n).map(|_| d.next_unit()).sum::<f32>() / n as f32;
        assert!(mean.abs() < 0.01, "mean too large: {mean}");
    }

    #[test]
    fn from_ids_decorrelates() {
        let mut d0 = GoldenRatioDither::from_ids(0, 0);
        let mut d1 = GoldenRatioDither::from_ids(1, 7);
        // Confirm they start at different states
        let v0 = d0.next_unit();
        let v1 = d1.next_unit();
        assert!((v0 - v1).abs() > 1e-4, "distinct seeds should produce distinct first values");
    }

    #[test]
    fn deterministic_across_calls() {
        let mut d1 = GoldenRatioDither::new(0.123);
        let mut d2 = GoldenRatioDither::new(0.123);
        for _ in 0..1000 {
            assert_eq!(d1.next_unit(), d2.next_unit());
        }
    }
}