ruvector_dither/
golden.rs1use crate::DitherSource;
9
10#[derive(Clone, Debug)]
15pub struct GoldenRatioDither {
16 state: f32,
17}
18
19const PHI: f32 = 0.618_033_98_f32;
21
22impl GoldenRatioDither {
23 #[inline]
28 pub fn new(initial_state: f32) -> Self {
29 Self { state: initial_state.abs().fract() }
30 }
31
32 #[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 #[inline]
41 pub fn state(&self) -> f32 {
42 self.state
43 }
44}
45
46impl DitherSource for GoldenRatioDither {
47 #[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 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}