1use std::f64::consts::PI;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum NoiseType {
38 Gaussian,
40 Uniform,
42 Laplacian,
44 ScheduledGaussian,
47}
48
49#[derive(Debug, Clone)]
51pub struct GradientNoiseConfig {
52 pub noise_type: NoiseType,
54 pub initial_scale: f64,
57 pub decay_rate: f64,
60 pub clip_value: Option<f64>,
62 pub seed: u64,
64}
65
66#[derive(Debug, Clone, Default)]
68pub struct NoiseStats {
69 pub total_injections: u64,
71 pub total_elements: u64,
73 pub avg_noise_magnitude: f64,
75 pub max_noise_applied: f64,
77 pub current_scale: f64,
79}
80
81#[derive(Debug, Clone)]
83pub struct NoiseSample {
84 pub values: Vec<f64>,
86 pub mean: f64,
88 pub std_dev: f64,
90 pub step: u64,
92}
93
94pub struct GradientNoiseInjector {
101 config: GradientNoiseConfig,
102 rng_state: u64,
103 step: u64,
104 stats: NoiseStats,
105}
106
107impl GradientNoiseInjector {
108 pub fn new(config: GradientNoiseConfig) -> Self {
113 let seed = if config.seed == 0 {
114 0xDEAD_BEEF_CAFE_BABE
115 } else {
116 config.seed
117 };
118 let current_scale = config.initial_scale;
119 Self {
120 config,
121 rng_state: seed,
122 step: 0,
123 stats: NoiseStats {
124 current_scale,
125 ..NoiseStats::default()
126 },
127 }
128 }
129
130 fn xorshift64(&mut self) -> u64 {
134 let mut s = self.rng_state;
135 s ^= s << 13;
136 s ^= s >> 7;
137 s ^= s << 17;
138 self.rng_state = s;
139 s
140 }
141
142 fn uniform_01(&mut self) -> f64 {
144 let bits = self.xorshift64() >> 11;
146 (bits as f64) / ((1u64 << 53) as f64)
147 }
148
149 pub fn next_gaussian(&mut self) -> f64 {
152 loop {
153 let u1 = self.uniform_01();
154 let u2 = self.uniform_01();
155 if u1 <= f64::EPSILON {
157 continue;
158 }
159 let r = (-2.0 * u1.ln()).sqrt();
160 return r * (2.0 * PI * u2).cos();
161 }
162 }
163
164 pub fn next_uniform(&mut self, low: f64, high: f64) -> f64 {
166 let u = self.uniform_01();
167 low + u * (high - low)
168 }
169
170 pub fn next_laplacian(&mut self, scale: f64) -> f64 {
173 let u = self.uniform_01() - 0.5;
174 let abs_u = u.abs().min(0.5 - f64::EPSILON);
176 -scale * (1.0 - 2.0 * abs_u).ln() * u.signum()
177 }
178
179 pub fn clip_noise(&self, value: f64) -> f64 {
182 match self.config.clip_value {
183 Some(clip) => value.clamp(-clip, clip),
184 None => value,
185 }
186 }
187
188 pub fn current_scale(&self) -> f64 {
193 match self.config.noise_type {
194 NoiseType::ScheduledGaussian => {
195 self.config.initial_scale / (1.0 + self.config.decay_rate * self.step as f64)
196 }
197 _ => self.config.initial_scale,
198 }
199 }
200
201 fn sample_one(&mut self) -> f64 {
204 let scale = self.current_scale();
205 let raw = match self.config.noise_type {
206 NoiseType::Gaussian => self.next_gaussian() * scale,
207 NoiseType::Uniform => self.next_uniform(-scale, scale),
208 NoiseType::Laplacian => self.next_laplacian(scale),
209 NoiseType::ScheduledGaussian => self.next_gaussian() * scale,
210 };
211 self.clip_noise(raw)
212 }
213
214 pub fn inject(&mut self, gradients: &mut [f64]) {
219 let n = gradients.len() as u64;
220 let mut sum_abs: f64 = 0.0;
221 let mut local_max: f64 = 0.0;
222
223 for g in gradients.iter_mut() {
224 let noise = self.sample_one();
225 *g += noise;
226 let abs_noise = noise.abs();
227 sum_abs += abs_noise;
228 if abs_noise > local_max {
229 local_max = abs_noise;
230 }
231 }
232
233 let prev_total = self.stats.total_elements;
235 self.stats.total_injections += 1;
236 self.stats.total_elements += n;
237 if self.stats.max_noise_applied < local_max {
238 self.stats.max_noise_applied = local_max;
239 }
240 if n > 0 {
242 let new_avg = sum_abs / n as f64;
243 let total = prev_total + n;
244 self.stats.avg_noise_magnitude = (self.stats.avg_noise_magnitude * prev_total as f64
245 + new_avg * n as f64)
246 / total as f64;
247 }
248 self.stats.current_scale = self.current_scale();
249 }
250
251 pub fn sample_noise(&mut self, count: usize) -> NoiseSample {
254 let mut values = Vec::with_capacity(count);
255 for _ in 0..count {
256 values.push(self.sample_one());
257 }
258
259 let (mean, std_dev) = compute_mean_std(&values);
260
261 NoiseSample {
262 values,
263 mean,
264 std_dev,
265 step: self.step,
266 }
267 }
268
269 pub fn step(&mut self) {
272 self.step += 1;
273 self.stats.current_scale = self.current_scale();
274 }
275
276 pub fn reset(&mut self) {
278 self.step = 0;
279 self.rng_state = if self.config.seed == 0 {
280 0xDEAD_BEEF_CAFE_BABE
281 } else {
282 self.config.seed
283 };
284 self.stats = NoiseStats {
285 current_scale: self.config.initial_scale,
286 ..NoiseStats::default()
287 };
288 }
289
290 pub fn stats(&self) -> &NoiseStats {
292 &self.stats
293 }
294
295 pub fn set_scale(&mut self, scale: f64) {
298 self.config.initial_scale = scale;
299 self.stats.current_scale = self.current_scale();
300 }
301}
302
303fn compute_mean_std(values: &[f64]) -> (f64, f64) {
309 if values.is_empty() {
310 return (0.0, 0.0);
311 }
312 let n = values.len() as f64;
313 let mean = values.iter().sum::<f64>() / n;
314 if values.len() == 1 {
315 return (mean, 0.0);
316 }
317 let var = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
318 (mean, var.sqrt())
319}
320
321#[cfg(test)]
326mod tests {
327 use super::*;
328
329 fn gaussian_config(seed: u64) -> GradientNoiseConfig {
330 GradientNoiseConfig {
331 noise_type: NoiseType::Gaussian,
332 initial_scale: 0.1,
333 decay_rate: 0.0,
334 clip_value: None,
335 seed,
336 }
337 }
338
339 fn uniform_config(seed: u64) -> GradientNoiseConfig {
340 GradientNoiseConfig {
341 noise_type: NoiseType::Uniform,
342 initial_scale: 1.0,
343 decay_rate: 0.0,
344 clip_value: None,
345 seed,
346 }
347 }
348
349 fn laplacian_config(seed: u64) -> GradientNoiseConfig {
350 GradientNoiseConfig {
351 noise_type: NoiseType::Laplacian,
352 initial_scale: 0.5,
353 decay_rate: 0.0,
354 clip_value: None,
355 seed,
356 }
357 }
358
359 fn scheduled_config(seed: u64) -> GradientNoiseConfig {
360 GradientNoiseConfig {
361 noise_type: NoiseType::ScheduledGaussian,
362 initial_scale: 1.0,
363 decay_rate: 0.1,
364 clip_value: None,
365 seed,
366 }
367 }
368
369 #[test]
372 fn gaussian_noise_has_zero_mean_approximately() {
373 let mut inj = GradientNoiseInjector::new(gaussian_config(123));
374 let sample = inj.sample_noise(10_000);
375 assert!(
377 sample.mean.abs() < 0.01,
378 "mean = {} is too far from 0",
379 sample.mean
380 );
381 }
382
383 #[test]
384 fn gaussian_noise_std_approximates_scale() {
385 let mut inj = GradientNoiseInjector::new(gaussian_config(456));
386 let sample = inj.sample_noise(10_000);
387 assert!(
389 (sample.std_dev - 0.1).abs() < 0.02,
390 "std_dev = {} not close to 0.1",
391 sample.std_dev
392 );
393 }
394
395 #[test]
396 fn gaussian_noise_values_are_finite() {
397 let mut inj = GradientNoiseInjector::new(gaussian_config(789));
398 let sample = inj.sample_noise(1000);
399 for v in &sample.values {
400 assert!(v.is_finite(), "non-finite value: {}", v);
401 }
402 }
403
404 #[test]
407 fn uniform_noise_within_bounds() {
408 let mut inj = GradientNoiseInjector::new(uniform_config(111));
409 let sample = inj.sample_noise(5000);
410 for v in &sample.values {
411 assert!(*v >= -1.0 && *v < 1.0, "value {} out of [-1, 1) range", v);
412 }
413 }
414
415 #[test]
416 fn uniform_noise_mean_near_zero() {
417 let mut inj = GradientNoiseInjector::new(uniform_config(222));
418 let sample = inj.sample_noise(10_000);
419 assert!(
420 sample.mean.abs() < 0.05,
421 "mean = {} is too far from 0",
422 sample.mean
423 );
424 }
425
426 #[test]
427 fn uniform_noise_spreads_across_range() {
428 let mut inj = GradientNoiseInjector::new(uniform_config(333));
429 let sample = inj.sample_noise(5000);
430 let min = sample.values.iter().cloned().fold(f64::INFINITY, f64::min);
431 let max = sample
432 .values
433 .iter()
434 .cloned()
435 .fold(f64::NEG_INFINITY, f64::max);
436 assert!(min < -0.8, "min {} not spread enough", min);
437 assert!(max > 0.8, "max {} not spread enough", max);
438 }
439
440 #[test]
443 fn laplacian_noise_mean_near_zero() {
444 let mut inj = GradientNoiseInjector::new(laplacian_config(444));
445 let sample = inj.sample_noise(10_000);
446 assert!(
447 sample.mean.abs() < 0.05,
448 "mean = {} too far from 0",
449 sample.mean
450 );
451 }
452
453 #[test]
454 fn laplacian_noise_values_are_finite() {
455 let mut inj = GradientNoiseInjector::new(laplacian_config(555));
456 let sample = inj.sample_noise(1000);
457 for v in &sample.values {
458 assert!(v.is_finite(), "non-finite Laplacian value: {}", v);
459 }
460 }
461
462 #[test]
463 fn laplacian_has_heavier_tails_than_gaussian() {
464 let mut g_inj = GradientNoiseInjector::new(GradientNoiseConfig {
466 noise_type: NoiseType::Gaussian,
467 initial_scale: 0.5,
468 decay_rate: 0.0,
469 clip_value: None,
470 seed: 666,
471 });
472 let mut l_inj = GradientNoiseInjector::new(laplacian_config(666));
473 let mut g_vals: Vec<f64> = g_inj.sample_noise(10_000).values;
474 let mut l_vals: Vec<f64> = l_inj.sample_noise(10_000).values;
475 g_vals.sort_by(|a, b| {
476 a.abs()
477 .partial_cmp(&b.abs())
478 .unwrap_or(std::cmp::Ordering::Equal)
479 });
480 l_vals.sort_by(|a, b| {
481 a.abs()
482 .partial_cmp(&b.abs())
483 .unwrap_or(std::cmp::Ordering::Equal)
484 });
485 let g99 = g_vals[9900].abs();
486 let l99 = l_vals[9900].abs();
487 assert!(
488 l99 > g99,
489 "Laplacian 99th pct {} should exceed Gaussian {}",
490 l99,
491 g99
492 );
493 }
494
495 #[test]
498 fn scheduled_gaussian_decays_over_steps() {
499 let mut inj = GradientNoiseInjector::new(scheduled_config(777));
500 let scale0 = inj.current_scale();
501 assert!((scale0 - 1.0).abs() < f64::EPSILON);
502
503 inj.step();
504 let scale1 = inj.current_scale();
505 assert!(scale1 < scale0, "scale should decay");
506
507 for _ in 0..10 {
508 inj.step();
509 }
510 let scale11 = inj.current_scale();
511 assert!(
512 scale11 < scale1,
513 "scale should keep decaying: {} vs {}",
514 scale11,
515 scale1
516 );
517 }
518
519 #[test]
520 fn scheduled_gaussian_scale_formula_correct() {
521 let config = scheduled_config(888);
522 let mut inj = GradientNoiseInjector::new(config);
523 for _ in 0..5 {
524 inj.step();
525 }
526 let expected = 1.0 / (1.0 + 0.1 * 5.0);
527 let actual = inj.current_scale();
528 assert!(
529 (actual - expected).abs() < 1e-12,
530 "expected {}, got {}",
531 expected,
532 actual
533 );
534 }
535
536 #[test]
537 fn scheduled_gaussian_noise_magnitude_decreases() {
538 let mut inj = GradientNoiseInjector::new(scheduled_config(999));
539 let s0 = inj.sample_noise(5000);
540 for _ in 0..50 {
541 inj.step();
542 }
543 let s50 = inj.sample_noise(5000);
544 assert!(
545 s50.std_dev < s0.std_dev,
546 "later std {} should be less than initial {}",
547 s50.std_dev,
548 s0.std_dev
549 );
550 }
551
552 #[test]
555 fn clipping_limits_noise_magnitude() {
556 let config = GradientNoiseConfig {
557 noise_type: NoiseType::Gaussian,
558 initial_scale: 10.0,
559 decay_rate: 0.0,
560 clip_value: Some(0.5),
561 seed: 1010,
562 };
563 let mut inj = GradientNoiseInjector::new(config);
564 let sample = inj.sample_noise(5000);
565 for v in &sample.values {
566 assert!(
567 v.abs() <= 0.5 + f64::EPSILON,
568 "clipped value {} exceeds 0.5",
569 v
570 );
571 }
572 }
573
574 #[test]
575 fn clip_noise_returns_unchanged_without_config() {
576 let config = GradientNoiseConfig {
577 noise_type: NoiseType::Gaussian,
578 initial_scale: 1.0,
579 decay_rate: 0.0,
580 clip_value: None,
581 seed: 1111,
582 };
583 let inj = GradientNoiseInjector::new(config);
584 assert!((inj.clip_noise(999.0) - 999.0).abs() < f64::EPSILON);
585 }
586
587 #[test]
588 fn clip_noise_clamps_symmetric() {
589 let config = GradientNoiseConfig {
590 noise_type: NoiseType::Gaussian,
591 initial_scale: 1.0,
592 decay_rate: 0.0,
593 clip_value: Some(2.0),
594 seed: 1212,
595 };
596 let inj = GradientNoiseInjector::new(config);
597 assert!((inj.clip_noise(5.0) - 2.0).abs() < f64::EPSILON);
598 assert!((inj.clip_noise(-5.0) - (-2.0)).abs() < f64::EPSILON);
599 assert!((inj.clip_noise(1.5) - 1.5).abs() < f64::EPSILON);
600 }
601
602 #[test]
605 fn step_increments_counter() {
606 let mut inj = GradientNoiseInjector::new(gaussian_config(1313));
607 assert_eq!(inj.step, 0);
608 inj.step();
609 assert_eq!(inj.step, 1);
610 inj.step();
611 assert_eq!(inj.step, 2);
612 }
613
614 #[test]
617 fn same_seed_produces_same_sequence() {
618 let mut a = GradientNoiseInjector::new(gaussian_config(4242));
619 let mut b = GradientNoiseInjector::new(gaussian_config(4242));
620 let sa = a.sample_noise(100);
621 let sb = b.sample_noise(100);
622 assert_eq!(sa.values, sb.values);
623 }
624
625 #[test]
626 fn different_seeds_produce_different_sequences() {
627 let mut a = GradientNoiseInjector::new(gaussian_config(1));
628 let mut b = GradientNoiseInjector::new(gaussian_config(2));
629 let sa = a.sample_noise(100);
630 let sb = b.sample_noise(100);
631 assert_ne!(sa.values, sb.values);
633 }
634
635 #[test]
638 fn stats_updated_after_inject() {
639 let mut inj = GradientNoiseInjector::new(gaussian_config(1414));
640 let mut grads = vec![0.0; 50];
641 inj.inject(&mut grads);
642 let s = inj.stats();
643 assert_eq!(s.total_injections, 1);
644 assert_eq!(s.total_elements, 50);
645 assert!(s.avg_noise_magnitude > 0.0);
646 }
647
648 #[test]
649 fn stats_accumulate_across_injections() {
650 let mut inj = GradientNoiseInjector::new(gaussian_config(1515));
651 let mut g1 = vec![0.0; 100];
652 let mut g2 = vec![0.0; 200];
653 inj.inject(&mut g1);
654 inj.inject(&mut g2);
655 let s = inj.stats();
656 assert_eq!(s.total_injections, 2);
657 assert_eq!(s.total_elements, 300);
658 }
659
660 #[test]
661 fn max_noise_tracked() {
662 let config = GradientNoiseConfig {
663 noise_type: NoiseType::Gaussian,
664 initial_scale: 5.0,
665 decay_rate: 0.0,
666 clip_value: None,
667 seed: 1616,
668 };
669 let mut inj = GradientNoiseInjector::new(config);
670 let mut grads = vec![0.0; 1000];
671 inj.inject(&mut grads);
672 assert!(inj.stats().max_noise_applied > 0.0);
673 }
674
675 #[test]
678 fn inject_modifies_gradients() {
679 let mut inj = GradientNoiseInjector::new(gaussian_config(1717));
680 let original = vec![1.0, 2.0, 3.0, 4.0, 5.0];
681 let mut grads = original.clone();
682 inj.inject(&mut grads);
683 assert_ne!(grads, original, "gradients should be modified by noise");
684 }
685
686 #[test]
687 fn inject_preserves_length() {
688 let mut inj = GradientNoiseInjector::new(gaussian_config(1818));
689 let mut grads = vec![0.5; 37];
690 inj.inject(&mut grads);
691 assert_eq!(grads.len(), 37);
692 }
693
694 #[test]
697 fn inject_large_array() {
698 let mut inj = GradientNoiseInjector::new(gaussian_config(1919));
699 let mut grads = vec![0.0; 100_000];
700 inj.inject(&mut grads);
701 let nonzero = grads.iter().filter(|v| v.abs() > f64::EPSILON).count();
702 assert!(nonzero > 99_000, "almost all elements should receive noise");
703 }
704
705 #[test]
708 fn zero_scale_produces_no_noise() {
709 let config = GradientNoiseConfig {
710 noise_type: NoiseType::Gaussian,
711 initial_scale: 0.0,
712 decay_rate: 0.0,
713 clip_value: None,
714 seed: 2020,
715 };
716 let mut inj = GradientNoiseInjector::new(config);
717 let mut grads = vec![1.0, 2.0, 3.0];
718 inj.inject(&mut grads);
719 assert!((grads[0] - 1.0).abs() < f64::EPSILON);
721 assert!((grads[1] - 2.0).abs() < f64::EPSILON);
722 assert!((grads[2] - 3.0).abs() < f64::EPSILON);
723 }
724
725 #[test]
728 fn reset_clears_stats_and_step() {
729 let mut inj = GradientNoiseInjector::new(gaussian_config(2121));
730 let mut grads = vec![0.0; 10];
731 inj.inject(&mut grads);
732 inj.step();
733 inj.step();
734 inj.reset();
735 assert_eq!(inj.step, 0);
736 assert_eq!(inj.stats().total_injections, 0);
737 assert_eq!(inj.stats().total_elements, 0);
738 }
739
740 #[test]
741 fn reset_reproduces_sequence() {
742 let config = gaussian_config(2222);
743 let mut inj = GradientNoiseInjector::new(config);
744 let first = inj.sample_noise(50);
745 inj.reset();
746 let second = inj.sample_noise(50);
747 assert_eq!(first.values, second.values);
748 }
749
750 #[test]
753 fn set_scale_changes_output_magnitude() {
754 let mut inj = GradientNoiseInjector::new(gaussian_config(2323));
755 inj.set_scale(10.0);
756 let sample = inj.sample_noise(5000);
757 assert!(
759 sample.std_dev > 5.0,
760 "std_dev {} should reflect new scale 10",
761 sample.std_dev
762 );
763 }
764
765 #[test]
768 fn sample_noise_reports_correct_step() {
769 let mut inj = GradientNoiseInjector::new(gaussian_config(2424));
770 inj.step();
771 inj.step();
772 inj.step();
773 let sample = inj.sample_noise(10);
774 assert_eq!(sample.step, 3);
775 }
776
777 #[test]
778 fn sample_noise_empty_returns_defaults() {
779 let mut inj = GradientNoiseInjector::new(gaussian_config(2525));
780 let sample = inj.sample_noise(0);
781 assert!(sample.values.is_empty());
782 assert!((sample.mean).abs() < f64::EPSILON);
783 assert!((sample.std_dev).abs() < f64::EPSILON);
784 }
785
786 #[test]
789 fn zero_seed_uses_fallback() {
790 let config = GradientNoiseConfig {
791 noise_type: NoiseType::Gaussian,
792 initial_scale: 0.1,
793 decay_rate: 0.0,
794 clip_value: None,
795 seed: 0,
796 };
797 let mut inj = GradientNoiseInjector::new(config);
798 let sample = inj.sample_noise(10);
800 assert_eq!(sample.values.len(), 10);
801 }
802
803 #[test]
806 fn current_scale_constant_for_non_scheduled() {
807 let mut inj = GradientNoiseInjector::new(gaussian_config(2626));
808 let s0 = inj.current_scale();
809 for _ in 0..100 {
810 inj.step();
811 }
812 let s100 = inj.current_scale();
813 assert!(
814 (s0 - s100).abs() < f64::EPSILON,
815 "non-scheduled scale should not change"
816 );
817 }
818
819 #[test]
822 fn inject_empty_slice_no_panic() {
823 let mut inj = GradientNoiseInjector::new(gaussian_config(2727));
824 let mut grads: Vec<f64> = vec![];
825 inj.inject(&mut grads);
826 assert_eq!(inj.stats().total_injections, 1);
827 assert_eq!(inj.stats().total_elements, 0);
828 }
829}