1use std::collections::VecDeque;
27use std::f64::consts::PI;
28use thiserror::Error;
29
30fn xorshift64(state: &mut u64) -> u64 {
34 let mut x = *state;
35 x ^= x << 13;
36 x ^= x >> 7;
37 x ^= x << 17;
38 *state = x;
39 x
40}
41
42#[derive(Debug, Error, Clone)]
46pub enum DpError {
47 #[error("privacy budget exhausted: remaining epsilon = {remaining:.6}")]
49 BudgetExhausted {
50 remaining: f64,
52 },
53
54 #[error("invalid parameters: {0}")]
56 InvalidParameters(String),
57
58 #[error("sensitivity must be positive (got zero or negative)")]
60 ZeroSensitivity,
61
62 #[error("epsilon must be strictly positive")]
64 InvalidEpsilon,
65}
66
67#[derive(Debug, Clone, PartialEq)]
71pub enum PrivacyMechanism {
72 Laplace {
75 sensitivity: f64,
77 epsilon: f64,
79 },
80
81 Gaussian {
84 sensitivity: f64,
86 epsilon: f64,
88 delta: f64,
90 },
91
92 Randomized {
94 epsilon: f64,
96 },
97}
98
99impl PrivacyMechanism {
100 pub fn epsilon(&self) -> f64 {
102 match self {
103 PrivacyMechanism::Laplace { epsilon, .. } => *epsilon,
104 PrivacyMechanism::Gaussian { epsilon, .. } => *epsilon,
105 PrivacyMechanism::Randomized { epsilon } => *epsilon,
106 }
107 }
108
109 pub fn delta(&self) -> f64 {
111 match self {
112 PrivacyMechanism::Gaussian { delta, .. } => *delta,
113 _ => 0.0,
114 }
115 }
116
117 pub fn sensitivity(&self) -> Option<f64> {
119 match self {
120 PrivacyMechanism::Laplace { sensitivity, .. } => Some(*sensitivity),
121 PrivacyMechanism::Gaussian { sensitivity, .. } => Some(*sensitivity),
122 PrivacyMechanism::Randomized { .. } => None,
123 }
124 }
125
126 pub fn validate(&self) -> Result<(), DpError> {
128 let eps = self.epsilon();
129 if eps <= 0.0 {
130 return Err(DpError::InvalidEpsilon);
131 }
132 if let Some(s) = self.sensitivity() {
133 if s <= 0.0 {
134 return Err(DpError::ZeroSensitivity);
135 }
136 }
137 if let PrivacyMechanism::Gaussian { delta, .. } = self {
138 if *delta <= 0.0 || *delta >= 1.0 {
139 return Err(DpError::InvalidParameters(format!(
140 "delta must be in (0,1), got {delta}"
141 )));
142 }
143 }
144 Ok(())
145 }
146}
147
148#[derive(Debug, Clone)]
156pub struct NoiseScale {
157 pub mechanism: PrivacyMechanism,
159 pub scale: f64,
161}
162
163#[derive(Debug, Clone)]
167pub struct PrivacyParameters {
168 pub epsilon: f64,
170 pub delta: f64,
172 pub sensitivity: f64,
174}
175
176impl PrivacyParameters {
177 pub fn new(epsilon: f64, delta: f64, sensitivity: f64) -> Result<Self, DpError> {
179 if epsilon <= 0.0 {
180 return Err(DpError::InvalidEpsilon);
181 }
182 if sensitivity <= 0.0 {
183 return Err(DpError::ZeroSensitivity);
184 }
185 if !(0.0..1.0).contains(&delta) {
186 return Err(DpError::InvalidParameters(format!(
187 "delta must be in [0,1), got {delta}"
188 )));
189 }
190 Ok(Self {
191 epsilon,
192 delta,
193 sensitivity,
194 })
195 }
196}
197
198#[derive(Debug, Clone)]
202pub struct DpQuery {
203 pub query_id: String,
205 pub sensitivity: f64,
207 pub mechanism: PrivacyMechanism,
209}
210
211#[derive(Debug, Clone)]
215pub struct DpResult {
216 pub query_id: String,
218 pub true_value: f64,
220 pub noisy_value: f64,
222 pub noise_added: f64,
224 pub privacy_cost: f64,
226}
227
228#[derive(Debug, Clone)]
232pub struct BudgetTracker {
233 pub epsilon_budget: f64,
235 pub epsilon_used: f64,
237 pub delta_budget: f64,
239 pub delta_used: f64,
241 pub queries_answered: u64,
243}
244
245impl BudgetTracker {
246 pub fn new(epsilon_budget: f64, delta_budget: f64) -> Self {
248 Self {
249 epsilon_budget,
250 epsilon_used: 0.0,
251 delta_budget,
252 delta_used: 0.0,
253 queries_answered: 0,
254 }
255 }
256
257 pub fn remaining_epsilon(&self) -> f64 {
259 (self.epsilon_budget - self.epsilon_used).max(0.0)
260 }
261
262 pub fn remaining_delta(&self) -> f64 {
264 (self.delta_budget - self.delta_used).max(0.0)
265 }
266
267 pub fn is_exhausted(&self) -> bool {
269 self.epsilon_used >= self.epsilon_budget
270 }
271
272 pub fn charge(&mut self, epsilon_cost: f64, delta_cost: f64) -> Result<(), DpError> {
275 if self.is_exhausted() || self.epsilon_used + epsilon_cost > self.epsilon_budget {
276 return Err(DpError::BudgetExhausted {
277 remaining: self.remaining_epsilon(),
278 });
279 }
280 self.epsilon_used += epsilon_cost;
281 self.delta_used += delta_cost;
282 self.queries_answered += 1;
283 Ok(())
284 }
285}
286
287pub struct DifferentialPrivacyEngine {
294 pub budget: BudgetTracker,
296 answered: VecDeque<DpResult>,
298 max_history: usize,
300 rng_state: u64,
302}
303
304impl DifferentialPrivacyEngine {
305 pub fn new(epsilon_budget: f64, delta_budget: f64, max_history: usize) -> Self {
309 Self {
310 budget: BudgetTracker::new(epsilon_budget, delta_budget),
311 answered: VecDeque::new(),
312 max_history,
313 rng_state: 0x00DE_ADBE_EF42_u64,
314 }
315 }
316
317 pub fn compute_noise_scale(mechanism: &PrivacyMechanism) -> NoiseScale {
325 let scale = match mechanism {
326 PrivacyMechanism::Laplace {
327 sensitivity,
328 epsilon,
329 } => sensitivity / epsilon,
330
331 PrivacyMechanism::Gaussian {
332 sensitivity,
333 epsilon,
334 delta,
335 } => {
336 let inner = 2.0_f64 * (1.25_f64 / delta).ln();
338 sensitivity * inner.sqrt() / epsilon
339 }
340
341 PrivacyMechanism::Randomized { epsilon } => 1.0 / (epsilon.exp() + 1.0),
342 };
343 NoiseScale {
344 mechanism: mechanism.clone(),
345 scale,
346 }
347 }
348
349 fn uniform_sample(&mut self) -> f64 {
353 let raw = xorshift64(&mut self.rng_state);
354 raw as f64 / u64::MAX as f64
355 }
356
357 pub fn sample_laplace(&mut self, scale: f64) -> f64 {
362 let u = self.uniform_sample();
363 let centered = u - 0.5;
364 let sign = if centered >= 0.0 { 1.0_f64 } else { -1.0_f64 };
365 let arg = (1.0 - 2.0 * centered.abs()).max(1e-10);
366 -scale * sign * arg.ln()
367 }
368
369 pub fn sample_gaussian(&mut self, scale: f64) -> f64 {
375 let u1 = self.uniform_sample().max(1e-10);
376 let u2 = self.uniform_sample();
377 let z = (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos();
378 z * scale
379 }
380
381 fn sample_noise(&mut self, mechanism: &PrivacyMechanism, true_value: f64) -> f64 {
384 let ns = Self::compute_noise_scale(mechanism);
385 match mechanism {
386 PrivacyMechanism::Laplace { .. } => self.sample_laplace(ns.scale),
387 PrivacyMechanism::Gaussian { .. } => self.sample_gaussian(ns.scale),
388 PrivacyMechanism::Randomized { epsilon } => {
389 let flip_prob = ns.scale; let u = self.uniform_sample();
393 if u < flip_prob {
394 let sign = if self.uniform_sample() < 0.5 {
396 1.0_f64
397 } else {
398 -1.0_f64
399 };
400 let _ = epsilon; sign * 1.0 - true_value + true_value } else {
403 0.0
404 }
405 }
406 }
407 }
408
409 pub fn apply_mechanism(
419 &mut self,
420 query: &DpQuery,
421 true_value: f64,
422 ) -> Result<DpResult, DpError> {
423 query.mechanism.validate()?;
425
426 if self.budget.is_exhausted() {
428 return Err(DpError::BudgetExhausted {
429 remaining: self.budget.remaining_epsilon(),
430 });
431 }
432
433 let noise = self.sample_noise(&query.mechanism, true_value);
435 let noisy_value = true_value + noise;
436
437 let epsilon_cost = query.mechanism.epsilon();
439 let delta_cost = query.mechanism.delta();
440
441 self.budget.charge(epsilon_cost, delta_cost)?;
443
444 let result = DpResult {
445 query_id: query.query_id.clone(),
446 true_value,
447 noisy_value,
448 noise_added: noise,
449 privacy_cost: epsilon_cost,
450 };
451
452 if self.answered.len() >= self.max_history && self.max_history > 0 {
454 self.answered.pop_front();
455 }
456 if self.max_history > 0 {
457 self.answered.push_back(result.clone());
458 }
459
460 Ok(result)
461 }
462
463 pub fn apply_batch(&mut self, queries: &[(DpQuery, f64)]) -> Vec<Result<DpResult, DpError>> {
469 queries
470 .iter()
471 .map(|(q, v)| self.apply_mechanism(q, *v))
472 .collect()
473 }
474
475 pub fn compose_sequential(results: &[DpResult]) -> f64 {
479 results.iter().map(|r| r.privacy_cost).sum()
480 }
481
482 pub fn compose_advanced(results: &[DpResult], delta: f64) -> f64 {
493 if results.is_empty() {
494 return 0.0;
495 }
496 let k = results.len() as f64;
497 let epsilon_per_query = results
498 .iter()
499 .map(|r| r.privacy_cost)
500 .fold(f64::NEG_INFINITY, f64::max);
501
502 let eps = epsilon_per_query;
503 let term1 = (2.0 * k * (1.0 / delta).ln()).sqrt() * eps;
504 let term2 = k * eps * (eps.exp() - 1.0);
505 term1 + term2
506 }
507
508 pub fn sensitivity_clip(values: &[f64], sensitivity: f64) -> Vec<f64> {
514 values
515 .iter()
516 .map(|&v| v.clamp(-sensitivity, sensitivity))
517 .collect()
518 }
519
520 pub fn budget_stats(&self) -> BudgetTracker {
524 self.budget.clone()
525 }
526
527 pub fn history(&self) -> &VecDeque<DpResult> {
529 &self.answered
530 }
531
532 pub fn budget_mut(&mut self) -> &mut BudgetTracker {
534 &mut self.budget
535 }
536
537 pub fn reseed(&mut self, seed: u64) {
539 self.rng_state = if seed == 0 { 1 } else { seed };
541 }
542
543 pub fn clear_history(&mut self) {
545 self.answered.clear();
546 }
547}
548
549#[cfg(test)]
552mod tests {
553 use crate::differential_privacy::{
554 xorshift64, BudgetTracker, DifferentialPrivacyEngine, DpError, DpQuery, DpResult,
555 NoiseScale, PrivacyMechanism, PrivacyParameters,
556 };
557
558 #[test]
561 fn test_xorshift64_non_zero() {
562 let mut state = 0x00DE_ADBE_EF42_u64;
563 let v = xorshift64(&mut state);
564 assert_ne!(v, 0);
565 assert_ne!(state, 0x00DE_ADBE_EF42_u64);
566 }
567
568 #[test]
569 fn test_xorshift64_deterministic() {
570 let mut s1 = 12345u64;
571 let mut s2 = 12345u64;
572 for _ in 0..100 {
573 assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
574 }
575 }
576
577 #[test]
578 fn test_xorshift64_different_outputs() {
579 let mut state = 1u64;
580 let a = xorshift64(&mut state);
581 let b = xorshift64(&mut state);
582 assert_ne!(a, b);
583 }
584
585 #[test]
588 fn test_laplace_mechanism_epsilon() {
589 let m = PrivacyMechanism::Laplace {
590 sensitivity: 1.0,
591 epsilon: 0.5,
592 };
593 assert_eq!(m.epsilon(), 0.5);
594 assert_eq!(m.delta(), 0.0);
595 assert_eq!(m.sensitivity(), Some(1.0));
596 }
597
598 #[test]
599 fn test_gaussian_mechanism_fields() {
600 let m = PrivacyMechanism::Gaussian {
601 sensitivity: 2.0,
602 epsilon: 1.0,
603 delta: 1e-5,
604 };
605 assert_eq!(m.epsilon(), 1.0);
606 assert_eq!(m.delta(), 1e-5);
607 assert_eq!(m.sensitivity(), Some(2.0));
608 }
609
610 #[test]
611 fn test_randomized_mechanism_fields() {
612 let m = PrivacyMechanism::Randomized { epsilon: 0.5 };
613 assert_eq!(m.epsilon(), 0.5);
614 assert_eq!(m.delta(), 0.0);
615 assert!(m.sensitivity().is_none());
616 }
617
618 #[test]
619 fn test_mechanism_validate_ok() {
620 let m = PrivacyMechanism::Laplace {
621 sensitivity: 1.0,
622 epsilon: 1.0,
623 };
624 assert!(m.validate().is_ok());
625 }
626
627 #[test]
628 fn test_mechanism_validate_invalid_epsilon() {
629 let m = PrivacyMechanism::Laplace {
630 sensitivity: 1.0,
631 epsilon: 0.0,
632 };
633 assert!(matches!(m.validate(), Err(DpError::InvalidEpsilon)));
634 }
635
636 #[test]
637 fn test_mechanism_validate_zero_sensitivity() {
638 let m = PrivacyMechanism::Laplace {
639 sensitivity: 0.0,
640 epsilon: 1.0,
641 };
642 assert!(matches!(m.validate(), Err(DpError::ZeroSensitivity)));
643 }
644
645 #[test]
646 fn test_mechanism_validate_gaussian_invalid_delta() {
647 let m = PrivacyMechanism::Gaussian {
648 sensitivity: 1.0,
649 epsilon: 1.0,
650 delta: 0.0,
651 };
652 assert!(matches!(m.validate(), Err(DpError::InvalidParameters(_))));
653 }
654
655 #[test]
658 fn test_laplace_noise_scale() {
659 let m = PrivacyMechanism::Laplace {
660 sensitivity: 1.0,
661 epsilon: 2.0,
662 };
663 let ns = DifferentialPrivacyEngine::compute_noise_scale(&m);
664 assert!((ns.scale - 0.5).abs() < 1e-12);
666 }
667
668 #[test]
669 fn test_gaussian_noise_scale() {
670 let delta = 1e-5;
671 let m = PrivacyMechanism::Gaussian {
672 sensitivity: 1.0,
673 epsilon: 1.0,
674 delta,
675 };
676 let ns = DifferentialPrivacyEngine::compute_noise_scale(&m);
677 let expected = (2.0 * (1.25 / delta).ln()).sqrt();
678 assert!((ns.scale - expected).abs() < 1e-10);
679 }
680
681 #[test]
682 fn test_gaussian_noise_scale_scales_with_sensitivity() {
683 let m1 = PrivacyMechanism::Gaussian {
684 sensitivity: 1.0,
685 epsilon: 1.0,
686 delta: 1e-5,
687 };
688 let m2 = PrivacyMechanism::Gaussian {
689 sensitivity: 2.0,
690 epsilon: 1.0,
691 delta: 1e-5,
692 };
693 let ns1 = DifferentialPrivacyEngine::compute_noise_scale(&m1);
694 let ns2 = DifferentialPrivacyEngine::compute_noise_scale(&m2);
695 assert!((ns2.scale - 2.0 * ns1.scale).abs() < 1e-10);
696 }
697
698 #[test]
699 fn test_randomized_noise_scale() {
700 let eps = 1.0_f64;
701 let m = PrivacyMechanism::Randomized { epsilon: eps };
702 let ns = DifferentialPrivacyEngine::compute_noise_scale(&m);
703 let expected = 1.0 / (eps.exp() + 1.0);
704 assert!((ns.scale - expected).abs() < 1e-12);
705 }
706
707 #[test]
710 fn test_privacy_parameters_valid() {
711 let p = PrivacyParameters::new(1.0, 1e-5, 1.0);
712 assert!(p.is_ok());
713 let p = p.expect("test: should succeed");
714 assert_eq!(p.epsilon, 1.0);
715 assert_eq!(p.delta, 1e-5);
716 assert_eq!(p.sensitivity, 1.0);
717 }
718
719 #[test]
720 fn test_privacy_parameters_invalid_epsilon() {
721 assert!(matches!(
722 PrivacyParameters::new(0.0, 1e-5, 1.0),
723 Err(DpError::InvalidEpsilon)
724 ));
725 }
726
727 #[test]
728 fn test_privacy_parameters_invalid_sensitivity() {
729 assert!(matches!(
730 PrivacyParameters::new(1.0, 1e-5, 0.0),
731 Err(DpError::ZeroSensitivity)
732 ));
733 }
734
735 #[test]
736 fn test_privacy_parameters_invalid_delta() {
737 assert!(matches!(
738 PrivacyParameters::new(1.0, -0.1, 1.0),
739 Err(DpError::InvalidParameters(_))
740 ));
741 }
742
743 #[test]
746 fn test_budget_tracker_initial_state() {
747 let bt = BudgetTracker::new(10.0, 1e-5);
748 assert_eq!(bt.epsilon_budget, 10.0);
749 assert_eq!(bt.epsilon_used, 0.0);
750 assert!(!bt.is_exhausted());
751 assert!((bt.remaining_epsilon() - 10.0).abs() < 1e-12);
752 }
753
754 #[test]
755 fn test_budget_tracker_charge_success() {
756 let mut bt = BudgetTracker::new(5.0, 1e-4);
757 bt.charge(2.0, 1e-5).expect("test: should succeed");
758 assert!((bt.remaining_epsilon() - 3.0).abs() < 1e-12);
759 assert_eq!(bt.queries_answered, 1);
760 assert!(!bt.is_exhausted());
761 }
762
763 #[test]
764 fn test_budget_tracker_exhaustion() {
765 let mut bt = BudgetTracker::new(1.0, 0.0);
766 bt.charge(1.0, 0.0).expect("test: should succeed");
767 assert!(bt.is_exhausted());
768 let err = bt.charge(0.5, 0.0);
770 assert!(matches!(err, Err(DpError::BudgetExhausted { .. })));
771 }
772
773 #[test]
774 fn test_budget_tracker_remaining_floored_at_zero() {
775 let mut bt = BudgetTracker::new(1.0, 0.0);
776 bt.charge(1.0, 0.0).expect("test: should succeed");
777 assert_eq!(bt.remaining_epsilon(), 0.0);
778 }
779
780 #[test]
783 fn test_engine_construction() {
784 let engine = DifferentialPrivacyEngine::new(10.0, 1e-5, 100);
785 assert_eq!(engine.budget.epsilon_budget, 10.0);
786 assert_eq!(engine.history().len(), 0);
787 }
788
789 #[test]
790 fn test_engine_laplace_query() {
791 let mut engine = DifferentialPrivacyEngine::new(10.0, 0.0, 100);
792 let query = DpQuery {
793 query_id: "test_laplace".to_string(),
794 sensitivity: 1.0,
795 mechanism: PrivacyMechanism::Laplace {
796 sensitivity: 1.0,
797 epsilon: 1.0,
798 },
799 };
800 let result = engine
801 .apply_mechanism(&query, 100.0)
802 .expect("test: should succeed");
803 assert_eq!(result.query_id, "test_laplace");
804 assert!(result.noisy_value.is_finite());
805 assert!((result.noise_added - (result.noisy_value - result.true_value)).abs() < 1e-10);
806 assert_eq!(result.privacy_cost, 1.0);
807 }
808
809 #[test]
810 fn test_engine_gaussian_query() {
811 let mut engine = DifferentialPrivacyEngine::new(10.0, 1.0, 100);
812 let query = DpQuery {
813 query_id: "test_gaussian".to_string(),
814 sensitivity: 1.0,
815 mechanism: PrivacyMechanism::Gaussian {
816 sensitivity: 1.0,
817 epsilon: 1.0,
818 delta: 1e-5,
819 },
820 };
821 let result = engine
822 .apply_mechanism(&query, 50.0)
823 .expect("test: should succeed");
824 assert_eq!(result.query_id, "test_gaussian");
825 assert!(result.noisy_value.is_finite());
826 }
827
828 #[test]
829 fn test_engine_budget_deduction() {
830 let mut engine = DifferentialPrivacyEngine::new(3.0, 0.0, 100);
831 let query = DpQuery {
832 query_id: "q".to_string(),
833 sensitivity: 1.0,
834 mechanism: PrivacyMechanism::Laplace {
835 sensitivity: 1.0,
836 epsilon: 1.0,
837 },
838 };
839 engine
840 .apply_mechanism(&query, 1.0)
841 .expect("test: should succeed");
842 engine
843 .apply_mechanism(&query, 2.0)
844 .expect("test: should succeed");
845 engine
846 .apply_mechanism(&query, 3.0)
847 .expect("test: should succeed");
848 assert!(engine.budget.is_exhausted());
849 let err = engine.apply_mechanism(&query, 4.0);
850 assert!(matches!(err, Err(DpError::BudgetExhausted { .. })));
851 }
852
853 #[test]
854 fn test_engine_history_bounded() {
855 let mut engine = DifferentialPrivacyEngine::new(1000.0, 0.0, 3);
856 let query = DpQuery {
857 query_id: "q".to_string(),
858 sensitivity: 1.0,
859 mechanism: PrivacyMechanism::Laplace {
860 sensitivity: 1.0,
861 epsilon: 0.1,
862 },
863 };
864 for _ in 0..10 {
865 engine
866 .apply_mechanism(&query, 0.0)
867 .expect("test: should succeed");
868 }
869 assert_eq!(engine.history().len(), 3);
870 }
871
872 #[test]
873 fn test_engine_invalid_mechanism_rejected() {
874 let mut engine = DifferentialPrivacyEngine::new(10.0, 0.0, 100);
875 let query = DpQuery {
876 query_id: "bad".to_string(),
877 sensitivity: 0.0,
878 mechanism: PrivacyMechanism::Laplace {
879 sensitivity: -1.0,
880 epsilon: 1.0,
881 },
882 };
883 let err = engine.apply_mechanism(&query, 0.0);
884 assert!(err.is_err());
885 }
886
887 #[test]
888 fn test_engine_batch_apply() {
889 let mut engine = DifferentialPrivacyEngine::new(100.0, 0.0, 100);
890 let queries: Vec<(DpQuery, f64)> = (0..5)
891 .map(|i| {
892 (
893 DpQuery {
894 query_id: format!("q{i}"),
895 sensitivity: 1.0,
896 mechanism: PrivacyMechanism::Laplace {
897 sensitivity: 1.0,
898 epsilon: 1.0,
899 },
900 },
901 i as f64,
902 )
903 })
904 .collect();
905 let results = engine.apply_batch(&queries);
906 assert_eq!(results.len(), 5);
907 for r in &results {
908 assert!(r.is_ok());
909 }
910 }
911
912 #[test]
913 fn test_engine_batch_stops_on_budget_exhaustion() {
914 let mut engine = DifferentialPrivacyEngine::new(2.0, 0.0, 100);
916 let queries: Vec<(DpQuery, f64)> = (0..5)
917 .map(|i| {
918 (
919 DpQuery {
920 query_id: format!("q{i}"),
921 sensitivity: 1.0,
922 mechanism: PrivacyMechanism::Laplace {
923 sensitivity: 1.0,
924 epsilon: 1.0,
925 },
926 },
927 i as f64,
928 )
929 })
930 .collect();
931 let results = engine.apply_batch(&queries);
932 let ok_count = results.iter().filter(|r| r.is_ok()).count();
933 let err_count = results.iter().filter(|r| r.is_err()).count();
934 assert_eq!(ok_count, 2);
935 assert_eq!(err_count, 3);
936 }
937
938 #[test]
941 fn test_compose_sequential_empty() {
942 assert_eq!(DifferentialPrivacyEngine::compose_sequential(&[]), 0.0);
943 }
944
945 #[test]
946 fn test_compose_sequential_sums_costs() {
947 let results = vec![
948 make_result("a", 1.0),
949 make_result("b", 0.5),
950 make_result("c", 2.0),
951 ];
952 let total = DifferentialPrivacyEngine::compose_sequential(&results);
953 assert!((total - 3.5).abs() < 1e-12);
954 }
955
956 #[test]
957 fn test_compose_advanced_empty() {
958 assert_eq!(DifferentialPrivacyEngine::compose_advanced(&[], 1e-5), 0.0);
959 }
960
961 #[test]
962 fn test_compose_advanced_single_query() {
963 let results = vec![make_result("a", 1.0)];
964 let eps_adv = DifferentialPrivacyEngine::compose_advanced(&results, 1e-5);
965 let delta = 1e-5_f64;
967 let eps = 1.0_f64;
968 let expected = (2.0 * (1.0 / delta).ln()).sqrt() * eps + eps * (eps.exp() - 1.0);
969 assert!((eps_adv - expected).abs() < 1e-10);
970 }
971
972 #[test]
973 fn test_compose_advanced_larger_than_sequential_for_many_queries() {
974 let results: Vec<DpResult> = (0..20)
977 .map(|i| make_result(&format!("q{i}"), 0.1))
978 .collect();
979 let eps_adv = DifferentialPrivacyEngine::compose_advanced(&results, 1e-5);
980 assert!(eps_adv > 0.0);
981 assert!(eps_adv.is_finite());
982 }
983
984 #[test]
987 fn test_sensitivity_clip_within_bounds() {
988 let values = vec![0.5, -0.3, 0.0];
989 let clipped = DifferentialPrivacyEngine::sensitivity_clip(&values, 1.0);
990 assert_eq!(clipped, values);
991 }
992
993 #[test]
994 fn test_sensitivity_clip_above_bound() {
995 let values = vec![5.0, -5.0, 2.0];
996 let clipped = DifferentialPrivacyEngine::sensitivity_clip(&values, 1.0);
997 assert_eq!(clipped, vec![1.0, -1.0, 1.0]);
998 }
999
1000 #[test]
1001 fn test_sensitivity_clip_empty() {
1002 let clipped = DifferentialPrivacyEngine::sensitivity_clip(&[], 1.0);
1003 assert!(clipped.is_empty());
1004 }
1005
1006 #[test]
1007 fn test_sensitivity_clip_preserves_sign() {
1008 let values = vec![-10.0, 10.0];
1009 let clipped = DifferentialPrivacyEngine::sensitivity_clip(&values, 3.0);
1010 assert_eq!(clipped, vec![-3.0, 3.0]);
1011 }
1012
1013 #[test]
1016 fn test_laplace_noise_finite() {
1017 let mut engine = DifferentialPrivacyEngine::new(1000.0, 0.0, 1000);
1018 for _ in 0..1000 {
1019 let noise = engine.sample_laplace(1.0);
1020 assert!(noise.is_finite(), "Laplace noise must be finite");
1021 }
1022 }
1023
1024 #[test]
1025 fn test_gaussian_noise_finite() {
1026 let mut engine = DifferentialPrivacyEngine::new(1000.0, 1000.0, 1000);
1027 for _ in 0..1000 {
1028 let noise = engine.sample_gaussian(1.0);
1029 assert!(noise.is_finite(), "Gaussian noise must be finite");
1030 }
1031 }
1032
1033 #[test]
1034 fn test_laplace_noise_mean_near_zero() {
1035 let mut engine = DifferentialPrivacyEngine::new(f64::MAX, 0.0, 0);
1037 let n = 10_000usize;
1038 let mean: f64 = (0..n).map(|_| engine.sample_laplace(1.0)).sum::<f64>() / n as f64;
1039 assert!(
1040 mean.abs() < 0.15,
1041 "Empirical mean of Laplace samples too large: {mean}"
1042 );
1043 }
1044
1045 #[test]
1046 fn test_gaussian_noise_mean_near_zero() {
1047 let mut engine = DifferentialPrivacyEngine::new(f64::MAX, 0.0, 0);
1048 let n = 10_000usize;
1049 let mean: f64 = (0..n).map(|_| engine.sample_gaussian(1.0)).sum::<f64>() / n as f64;
1050 assert!(
1051 mean.abs() < 0.15,
1052 "Empirical mean of Gaussian samples too large: {mean}"
1053 );
1054 }
1055
1056 #[test]
1057 fn test_laplace_noise_scale_affects_variance() {
1058 let mut e1 = DifferentialPrivacyEngine::new(f64::MAX, 0.0, 0);
1059 e1.reseed(0xCAFE_BABE);
1060 let mut e2 = DifferentialPrivacyEngine::new(f64::MAX, 0.0, 0);
1061 e2.reseed(0xCAFE_BABE);
1062 let n = 1000usize;
1063 let var1: f64 = (0..n).map(|_| e1.sample_laplace(1.0).powi(2)).sum::<f64>() / n as f64;
1064 let var2: f64 = (0..n).map(|_| e2.sample_laplace(2.0).powi(2)).sum::<f64>() / n as f64;
1065 assert!(var2 > var1 * 2.0, "Larger scale should increase variance");
1067 }
1068
1069 #[test]
1072 fn test_reseed_reproducibility() {
1073 let mut engine = DifferentialPrivacyEngine::new(f64::MAX, 0.0, 0);
1074 engine.reseed(42);
1075 let a = engine.sample_laplace(1.0);
1076 engine.reseed(42);
1077 let b = engine.sample_laplace(1.0);
1078 assert_eq!(a, b);
1079 }
1080
1081 #[test]
1082 fn test_clear_history() {
1083 let mut engine = DifferentialPrivacyEngine::new(100.0, 0.0, 100);
1084 let query = DpQuery {
1085 query_id: "q".to_string(),
1086 sensitivity: 1.0,
1087 mechanism: PrivacyMechanism::Laplace {
1088 sensitivity: 1.0,
1089 epsilon: 1.0,
1090 },
1091 };
1092 engine
1093 .apply_mechanism(&query, 0.0)
1094 .expect("test: should succeed");
1095 assert_eq!(engine.history().len(), 1);
1096 engine.clear_history();
1097 assert_eq!(engine.history().len(), 0);
1098 }
1099
1100 #[test]
1101 fn test_budget_stats_clones_current_state() {
1102 let mut engine = DifferentialPrivacyEngine::new(10.0, 0.0, 100);
1103 let query = DpQuery {
1104 query_id: "q".to_string(),
1105 sensitivity: 1.0,
1106 mechanism: PrivacyMechanism::Laplace {
1107 sensitivity: 1.0,
1108 epsilon: 2.0,
1109 },
1110 };
1111 engine
1112 .apply_mechanism(&query, 0.0)
1113 .expect("test: should succeed");
1114 let stats = engine.budget_stats();
1115 assert!((stats.epsilon_used - 2.0).abs() < 1e-12);
1116 assert!((stats.remaining_epsilon() - 8.0).abs() < 1e-12);
1117 }
1118
1119 #[test]
1120 fn test_noise_scale_struct_carries_mechanism() {
1121 let m = PrivacyMechanism::Laplace {
1122 sensitivity: 3.0,
1123 epsilon: 1.5,
1124 };
1125 let ns: NoiseScale = DifferentialPrivacyEngine::compute_noise_scale(&m);
1126 assert_eq!(ns.mechanism, m);
1127 assert!((ns.scale - 2.0).abs() < 1e-12);
1128 }
1129
1130 fn make_result(id: &str, cost: f64) -> DpResult {
1133 DpResult {
1134 query_id: id.to_string(),
1135 true_value: 0.0,
1136 noisy_value: 0.0,
1137 noise_added: 0.0,
1138 privacy_cost: cost,
1139 }
1140 }
1141}