Skip to main content

ruvector_dither/
golden.rs

1//! Golden-ratio quasi-random dither sequence.
2//!
3//! State update: `state = frac(state + φ)` where φ = (√5−1)/2 ≈ 0.618…
4//!
5//! This is the 1-D Halton sequence in base φ — it has the best possible
6//! equidistribution for a 1-D low-discrepancy sequence.
7
8use crate::DitherSource;
9
10/// Additive golden-ratio dither with zero-mean output in `[-0.5, 0.5]`.
11///
12/// The sequence has period 1 (irrational) so it never exactly repeats.
13/// Two instances with different seeds stay decorrelated.
14#[derive(Clone, Debug)]
15pub struct GoldenRatioDither {
16    state: f32,
17}
18
19/// φ = (√5 − 1) / 2
20const PHI: f32 = 0.618_033_98_f32;
21
22impl GoldenRatioDither {
23    /// Create a new sequence seeded at `initial_state` ∈ [0, 1).
24    ///
25    /// For per-layer / per-channel decorrelation, seed with
26    /// `frac(layer_id × φ + channel_id × φ²)`.
27    #[inline]
28    pub fn new(initial_state: f32) -> Self {
29        Self { state: initial_state.abs().fract() }
30    }
31
32    /// Construct from a `(layer_id, channel_id)` pair for structural decorrelation.
33    #[inline]
34    pub fn from_ids(layer_id: u32, channel_id: u32) -> Self {
35        let s = ((layer_id as f32) * PHI + (channel_id as f32) * PHI * PHI).fract();
36        Self { state: s }
37    }
38
39    /// Current state (useful for serialisation / checkpointing).
40    #[inline]
41    pub fn state(&self) -> f32 {
42        self.state
43    }
44}
45
46impl DitherSource for GoldenRatioDither {
47    /// Advance and return next value in `[-0.5, 0.5]`.
48    #[inline]
49    fn next_unit(&mut self) -> f32 {
50        self.state = (self.state + PHI).fract();
51        self.state - 0.5
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::DitherSource;
59
60    #[test]
61    fn output_is_in_range() {
62        let mut d = GoldenRatioDither::new(0.0);
63        for _ in 0..10_000 {
64            let v = d.next_unit();
65            assert!(v >= -0.5 && v <= 0.5, "out of range: {v}");
66        }
67    }
68
69    #[test]
70    fn mean_is_near_zero() {
71        let mut d = GoldenRatioDither::new(0.0);
72        let n = 100_000;
73        let mean: f32 = (0..n).map(|_| d.next_unit()).sum::<f32>() / n as f32;
74        assert!(mean.abs() < 0.01, "mean too large: {mean}");
75    }
76
77    #[test]
78    fn from_ids_decorrelates() {
79        let mut d0 = GoldenRatioDither::from_ids(0, 0);
80        let mut d1 = GoldenRatioDither::from_ids(1, 7);
81        // Confirm they start at different states
82        let v0 = d0.next_unit();
83        let v1 = d1.next_unit();
84        assert!((v0 - v1).abs() > 1e-4, "distinct seeds should produce distinct first values");
85    }
86
87    #[test]
88    fn deterministic_across_calls() {
89        let mut d1 = GoldenRatioDither::new(0.123);
90        let mut d2 = GoldenRatioDither::new(0.123);
91        for _ in 0..1000 {
92            assert_eq!(d1.next_unit(), d2.next_unit());
93        }
94    }
95}