1use super::functions::*;
5
6#[allow(dead_code)]
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum NoiseType {
9 Laplace,
10 Exponential,
11 Gumbel,
12}
13pub struct LaplaceMechanism {
17 pub sensitivity: f64,
19 pub epsilon: f64,
21 pub scale: f64,
23}
24impl LaplaceMechanism {
25 pub fn new(sensitivity: f64, epsilon: f64) -> Self {
29 assert!(sensitivity > 0.0, "sensitivity must be positive");
30 assert!(epsilon > 0.0, "epsilon must be positive");
31 let scale = sensitivity / epsilon;
32 LaplaceMechanism {
33 sensitivity,
34 epsilon,
35 scale,
36 }
37 }
38 pub fn apply(&self, true_answer: f64, u: f64) -> f64 {
41 assert!(u > 0.0 && u < 1.0, "u must be in (0, 1)");
42 let v = u - 0.5;
43 let noise = -self.scale * v.signum() * (1.0 - 2.0 * v.abs()).ln();
44 true_answer + noise
45 }
46 pub fn privacy_loss(&self) -> f64 {
48 self.sensitivity / self.scale
49 }
50}
51#[allow(dead_code)]
53#[derive(Debug, Clone)]
54pub struct RenyiDp {
55 pub alpha: f64,
56 pub epsilon: f64,
57}
58impl RenyiDp {
59 #[allow(dead_code)]
60 pub fn new(alpha: f64, epsilon: f64) -> Self {
61 assert!(alpha > 1.0, "RDP order alpha must be > 1");
62 assert!(epsilon >= 0.0, "epsilon must be >= 0");
63 Self { alpha, epsilon }
64 }
65 #[allow(dead_code)]
66 pub fn to_pure_dp(&self) -> (f64, f64) {
67 let delta: f64 = 1e-5;
68 let eps_prime = self.epsilon + (1.0_f64 / delta).ln() / (self.alpha - 1.0);
69 (eps_prime, delta)
70 }
71 #[allow(dead_code)]
72 pub fn compose(&self, other: &RenyiDp) -> Option<RenyiDp> {
73 if (self.alpha - other.alpha).abs() < 1e-10 {
74 Some(RenyiDp::new(self.alpha, self.epsilon + other.epsilon))
75 } else {
76 None
77 }
78 }
79 #[allow(dead_code)]
80 pub fn gaussian_mechanism_epsilon(alpha: f64, sigma: f64, sensitivity: f64) -> Self {
81 let eps = alpha * sensitivity * sensitivity / (2.0 * sigma * sigma);
82 Self::new(alpha, eps)
83 }
84}
85pub struct RenyiAccountant {
87 pub ledger: Vec<(f64, f64)>,
90}
91impl RenyiAccountant {
92 pub fn new() -> Self {
94 RenyiAccountant { ledger: vec![] }
95 }
96 pub fn compose(&mut self, alpha: f64, eps_rdp: f64) {
98 if let Some(entry) = self
99 .ledger
100 .iter_mut()
101 .find(|(a, _)| (*a - alpha).abs() < 1e-10)
102 {
103 entry.1 += eps_rdp;
104 } else {
105 self.ledger.push((alpha, eps_rdp));
106 }
107 }
108 pub fn to_approx_dp(&self, alpha: f64, eps_rdp: f64, delta: f64) -> f64 {
112 assert!(alpha > 1.0, "α must be > 1 for RDP to DP conversion");
113 assert!(delta > 0.0 && delta < 1.0);
114 eps_rdp + (1.0 / delta).ln() / (alpha - 1.0)
115 }
116 pub fn optimal_eps(&self, delta: f64) -> f64 {
118 self.ledger
119 .iter()
120 .filter(|(alpha, _)| *alpha > 1.0)
121 .map(|(alpha, eps_rdp)| self.to_approx_dp(*alpha, *eps_rdp, delta))
122 .fold(f64::INFINITY, f64::min)
123 }
124}
125#[allow(dead_code)]
127#[derive(Debug, Clone)]
128pub struct ExponentialMechanismExt {
129 pub epsilon: f64,
130 pub sensitivity: f64,
131 pub output_range_name: String,
132}
133impl ExponentialMechanismExt {
134 #[allow(dead_code)]
135 pub fn new(eps: f64, sens: f64, range: &str) -> Self {
136 Self {
137 epsilon: eps,
138 sensitivity: sens,
139 output_range_name: range.to_string(),
140 }
141 }
142 #[allow(dead_code)]
143 pub fn sampling_probability_description(&self) -> String {
144 format!(
145 "Pr[output = r] proportional to exp(epsilon * u(D, r) / (2 * sensitivity)), range={}",
146 self.output_range_name
147 )
148 }
149 #[allow(dead_code)]
150 pub fn utility_guarantee(&self, opt_utility: f64, range_size: usize) -> f64 {
151 self.sensitivity * (range_size as f64).ln() / self.epsilon + opt_utility
152 }
153 #[allow(dead_code)]
154 pub fn is_epsilon_dp(&self) -> bool {
155 true
156 }
157}
158pub struct LaplaceNoise {
163 pub scale: f64,
165}
166impl LaplaceNoise {
167 pub fn new(scale: f64) -> Self {
169 assert!(scale > 0.0, "Laplace scale must be positive");
170 LaplaceNoise { scale }
171 }
172 pub fn sample_from_uniform(&self, u: f64) -> f64 {
176 assert!(u > 0.0 && u < 1.0, "u must be in (0, 1)");
177 let v = u - 0.5;
178 -self.scale * v.signum() * (1.0 - 2.0 * v.abs()).ln()
179 }
180 pub fn scale_for_pure_dp(sensitivity: f64, eps: f64) -> f64 {
182 assert!(eps > 0.0, "ε must be positive");
183 sensitivity / eps
184 }
185}
186#[allow(dead_code)]
188#[derive(Debug, Clone)]
189pub struct DpSyntheticData {
190 pub epsilon: f64,
191 pub delta: f64,
192 pub num_attributes: usize,
193 pub method: SyntheticDataMethod,
194}
195impl DpSyntheticData {
196 #[allow(dead_code)]
197 pub fn new(eps: f64, delta: f64, attrs: usize, method: SyntheticDataMethod) -> Self {
198 Self {
199 epsilon: eps,
200 delta,
201 num_attributes: attrs,
202 method,
203 }
204 }
205 #[allow(dead_code)]
206 pub fn marginal_error_bound(&self) -> f64 {
207 (self.num_attributes as f64).sqrt() / (self.epsilon * 100.0)
208 }
209}
210pub struct GaussianNoise {
212 pub sigma: f64,
214}
215impl GaussianNoise {
216 pub fn new(sigma: f64) -> Self {
218 assert!(sigma > 0.0, "Gaussian sigma must be positive");
219 GaussianNoise { sigma }
220 }
221 pub fn sigma_for_approx_dp(l2_sensitivity: f64, eps: f64, delta: f64) -> f64 {
225 assert!(eps > 0.0 && delta > 0.0 && delta < 1.0);
226 l2_sensitivity * (2.0 * (1.25f64 / delta).ln()).sqrt() / eps
227 }
228 pub fn box_muller(u1: f64, u2: f64) -> f64 {
231 assert!(u1 > 0.0 && u2 > 0.0);
232 let r = (-2.0 * u1.ln()).sqrt();
233 let theta = 2.0 * std::f64::consts::PI * u2;
234 r * theta.cos()
235 }
236 pub fn scale_sample(&self, z: f64) -> f64 {
238 self.sigma * z
239 }
240}
241#[allow(dead_code)]
243#[derive(Debug, Clone)]
244pub struct ZcdpBound {
245 pub rho: f64,
246}
247impl ZcdpBound {
248 #[allow(dead_code)]
249 pub fn new(rho: f64) -> Self {
250 assert!(rho >= 0.0);
251 Self { rho }
252 }
253 #[allow(dead_code)]
254 pub fn to_approximate_dp(&self, delta: f64) -> f64 {
255 self.rho + 2.0 * (self.rho * (1.0 / delta).ln()).sqrt()
256 }
257 #[allow(dead_code)]
258 pub fn gaussian_mechanism_rho(sigma: f64, sensitivity: f64) -> Self {
259 Self::new(sensitivity * sensitivity / (2.0 * sigma * sigma))
260 }
261 #[allow(dead_code)]
262 pub fn compose(&self, other: &ZcdpBound) -> ZcdpBound {
263 ZcdpBound::new(self.rho + other.rho)
264 }
265}
266#[allow(dead_code)]
268#[derive(Debug, Clone)]
269pub struct DpHistogram {
270 pub bins: usize,
271 pub epsilon: f64,
272 pub noise_mechanism: NoiseType,
273}
274impl DpHistogram {
275 #[allow(dead_code)]
276 pub fn laplace(bins: usize, eps: f64) -> Self {
277 Self {
278 bins,
279 epsilon: eps,
280 noise_mechanism: NoiseType::Laplace,
281 }
282 }
283 #[allow(dead_code)]
284 pub fn l1_sensitivity(&self) -> f64 {
285 2.0
286 }
287 #[allow(dead_code)]
288 pub fn noise_scale(&self) -> f64 {
289 self.l1_sensitivity() / self.epsilon
290 }
291 #[allow(dead_code)]
292 pub fn expected_absolute_error(&self) -> f64 {
293 self.noise_scale()
294 }
295}
296pub struct GaussianMechanism {
300 pub l2_sensitivity: f64,
302 pub epsilon: f64,
304 pub delta: f64,
306 pub sigma: f64,
308}
309impl GaussianMechanism {
310 pub fn new(l2_sensitivity: f64, epsilon: f64, delta: f64) -> Self {
312 assert!(l2_sensitivity > 0.0, "l2_sensitivity must be positive");
313 assert!(epsilon > 0.0, "epsilon must be positive");
314 assert!(delta > 0.0 && delta < 1.0, "delta must be in (0, 1)");
315 let sigma = l2_sensitivity * (2.0 * (1.25f64 / delta).ln()).sqrt() / epsilon;
316 GaussianMechanism {
317 l2_sensitivity,
318 epsilon,
319 delta,
320 sigma,
321 }
322 }
323 pub fn apply(&self, true_answer: f64, u1: f64, u2: f64) -> f64 {
325 let z = GaussianNoise::box_muller(u1, u2);
326 true_answer + self.sigma * z
327 }
328 pub fn rdp_guarantee(&self, alpha: f64) -> f64 {
330 assert!(alpha > 1.0, "α must be > 1");
331 alpha * self.l2_sensitivity * self.l2_sensitivity / (2.0 * self.sigma * self.sigma)
332 }
333}
334pub struct ExponentialMechanism {
338 pub epsilon: f64,
340 pub utility_sensitivity: f64,
342}
343impl ExponentialMechanism {
344 pub fn new(epsilon: f64, utility_sensitivity: f64) -> Self {
346 assert!(epsilon > 0.0, "epsilon must be positive");
347 assert!(
348 utility_sensitivity > 0.0,
349 "utility_sensitivity must be positive"
350 );
351 ExponentialMechanism {
352 epsilon,
353 utility_sensitivity,
354 }
355 }
356 pub fn probabilities(&self, utility_scores: &[f64]) -> Vec<f64> {
358 assert!(!utility_scores.is_empty(), "need at least one candidate");
359 let scale = self.epsilon / (2.0 * self.utility_sensitivity);
360 let weights: Vec<f64> = utility_scores.iter().map(|&u| (scale * u).exp()).collect();
361 let total: f64 = weights.iter().sum();
362 weights.iter().map(|&w| w / total).collect()
363 }
364 pub fn sample_index(&self, probs: &[f64], u: f64) -> usize {
366 assert!(u >= 0.0 && u < 1.0, "u must be in [0, 1)");
367 let mut cumulative = 0.0;
368 for (i, &p) in probs.iter().enumerate() {
369 cumulative += p;
370 if u < cumulative {
371 return i;
372 }
373 }
374 probs.len() - 1
375 }
376}
377#[allow(dead_code)]
379#[derive(Debug, Clone)]
380pub struct LocalDpMechanism {
381 pub epsilon: f64,
382 pub mechanism_type: LocalMechanismType,
383 pub domain_size: usize,
384}
385impl LocalDpMechanism {
386 #[allow(dead_code)]
387 pub fn randomized_response(eps: f64) -> Self {
388 Self {
389 epsilon: eps,
390 mechanism_type: LocalMechanismType::RandomizedResponse,
391 domain_size: 2,
392 }
393 }
394 #[allow(dead_code)]
395 pub fn unary_encoding(eps: f64, d: usize) -> Self {
396 Self {
397 epsilon: eps,
398 mechanism_type: LocalMechanismType::UnaryEncoding,
399 domain_size: d,
400 }
401 }
402 #[allow(dead_code)]
403 pub fn variance_estimate(&self) -> f64 {
404 let e = self.epsilon.exp();
405 let d = self.domain_size as f64;
406 match self.mechanism_type {
407 LocalMechanismType::RandomizedResponse => 4.0 * e / ((e - 1.0) * (e - 1.0)),
408 LocalMechanismType::UnaryEncoding => (e + 1.0) / (e - 1.0) * (e + 1.0) / (e - 1.0) / d,
409 _ => 1.0 / (d * self.epsilon * self.epsilon),
410 }
411 }
412 #[allow(dead_code)]
413 pub fn is_locally_private(&self) -> bool {
414 true
415 }
416}
417#[allow(dead_code)]
418#[derive(Debug, Clone, PartialEq, Eq)]
419pub enum SyntheticDataMethod {
420 PrivBayes,
421 Mst,
422 Aim,
423 Gem,
424}
425#[allow(dead_code)]
426#[derive(Debug, Clone, PartialEq, Eq)]
427pub enum LocalMechanismType {
428 RandomizedResponse,
429 UnaryEncoding,
430 OptimizedUnaryEncoding,
431 HadamardResponse,
432 SampledHistogram,
433}
434#[allow(dead_code)]
436#[derive(Debug, Clone, Default)]
437pub struct PrivacyLedger {
438 pub entries: Vec<PrivacyEntry>,
439}
440impl PrivacyLedger {
441 #[allow(dead_code)]
442 pub fn new() -> Self {
443 Self::default()
444 }
445 #[allow(dead_code)]
446 pub fn add_entry(&mut self, name: &str, eps: f64, delta: f64, comp: CompositionType) {
447 self.entries.push(PrivacyEntry {
448 mechanism_name: name.to_string(),
449 epsilon: eps,
450 delta,
451 composition: comp,
452 });
453 }
454 #[allow(dead_code)]
455 pub fn total_sequential_epsilon(&self) -> f64 {
456 self.entries
457 .iter()
458 .filter(|e| e.composition == CompositionType::Sequential)
459 .map(|e| e.epsilon)
460 .sum()
461 }
462 #[allow(dead_code)]
463 pub fn total_sequential_delta(&self) -> f64 {
464 self.entries
465 .iter()
466 .filter(|e| e.composition == CompositionType::Sequential)
467 .map(|e| e.delta)
468 .sum()
469 }
470 #[allow(dead_code)]
471 pub fn parallel_max_epsilon(&self) -> f64 {
472 self.entries
473 .iter()
474 .filter(|e| e.composition == CompositionType::Parallel)
475 .map(|e| e.epsilon)
476 .fold(0.0_f64, f64::max)
477 }
478}
479#[allow(dead_code)]
481#[derive(Debug, Clone)]
482pub struct ReportNoisyMax {
483 pub epsilon: f64,
484 pub noise_type: NoiseType,
485}
486impl ReportNoisyMax {
487 #[allow(dead_code)]
488 pub fn with_laplace(epsilon: f64) -> Self {
489 Self {
490 epsilon,
491 noise_type: NoiseType::Laplace,
492 }
493 }
494 #[allow(dead_code)]
495 pub fn is_pure_dp(&self) -> bool {
496 true
497 }
498 #[allow(dead_code)]
499 pub fn scale(&self) -> f64 {
500 1.0 / self.epsilon
501 }
502}
503pub struct PrivacyBudget {
507 pub total_epsilon: f64,
509 pub total_delta: f64,
511 pub spent_epsilon: f64,
513 pub spent_delta: f64,
515}
516impl PrivacyBudget {
517 pub fn new(total_epsilon: f64, total_delta: f64) -> Self {
519 assert!(total_epsilon > 0.0, "total_epsilon must be positive");
520 assert!(total_delta >= 0.0, "total_delta must be non-negative");
521 PrivacyBudget {
522 total_epsilon,
523 total_delta,
524 spent_epsilon: 0.0,
525 spent_delta: 0.0,
526 }
527 }
528 pub fn spend(&mut self, eps: f64, delta: f64) -> Result<(), String> {
532 let new_eps = self.spent_epsilon + eps;
533 let new_delta = self.spent_delta + delta;
534 if new_eps > self.total_epsilon + 1e-12 {
535 return Err(format!(
536 "Epsilon budget exceeded: need {:.4}, have {:.4}",
537 new_eps, self.total_epsilon
538 ));
539 }
540 if new_delta > self.total_delta + 1e-12 {
541 return Err(format!(
542 "Delta budget exceeded: need {:.4}, have {:.4}",
543 new_delta, self.total_delta
544 ));
545 }
546 self.spent_epsilon = new_eps;
547 self.spent_delta = new_delta;
548 Ok(())
549 }
550 pub fn remaining_epsilon(&self) -> f64 {
552 (self.total_epsilon - self.spent_epsilon).max(0.0)
553 }
554 pub fn remaining_delta(&self) -> f64 {
556 (self.total_delta - self.spent_delta).max(0.0)
557 }
558 pub fn is_valid(&self) -> bool {
560 self.spent_epsilon <= self.total_epsilon + 1e-12
561 && self.spent_delta <= self.total_delta + 1e-12
562 }
563}
564#[allow(dead_code)]
566#[derive(Debug, Clone)]
567pub struct DpMeanEstimator {
568 pub epsilon: f64,
569 pub delta: f64,
570 pub range: (f64, f64),
571 pub n: usize,
572}
573impl DpMeanEstimator {
574 #[allow(dead_code)]
575 pub fn new(eps: f64, delta: f64, lo: f64, hi: f64, n: usize) -> Self {
576 Self {
577 epsilon: eps,
578 delta,
579 range: (lo, hi),
580 n,
581 }
582 }
583 #[allow(dead_code)]
584 pub fn clipped_sensitivity(&self) -> f64 {
585 (self.range.1 - self.range.0) / self.n as f64
586 }
587 #[allow(dead_code)]
588 pub fn mse_gaussian_mechanism(&self) -> f64 {
589 let sigma =
590 self.clipped_sensitivity() * (2.0 * (1.25 / self.delta).ln()).sqrt() / self.epsilon;
591 sigma * sigma
592 }
593}
594#[allow(dead_code)]
596#[derive(Debug, Clone)]
597pub struct DpSgd {
598 pub learning_rate: f64,
599 pub noise_multiplier: f64,
600 pub max_grad_norm: f64,
601 pub batch_size: usize,
602 pub num_steps: usize,
603 pub dataset_size: usize,
604}
605impl DpSgd {
606 #[allow(dead_code)]
607 pub fn new(
608 lr: f64,
609 noise_mult: f64,
610 max_norm: f64,
611 batch: usize,
612 steps: usize,
613 n: usize,
614 ) -> Self {
615 Self {
616 learning_rate: lr,
617 noise_multiplier: noise_mult,
618 max_grad_norm: max_norm,
619 batch_size: batch,
620 num_steps: steps,
621 dataset_size: n,
622 }
623 }
624 #[allow(dead_code)]
625 pub fn sampling_rate(&self) -> f64 {
626 self.batch_size as f64 / self.dataset_size as f64
627 }
628 #[allow(dead_code)]
629 pub fn privacy_spent_rdp_alpha(&self, alpha: f64) -> f64 {
630 let q = self.sampling_rate();
631 alpha * q * q / (2.0 * self.noise_multiplier * self.noise_multiplier)
632 * self.num_steps as f64
633 }
634 #[allow(dead_code)]
635 pub fn gradient_clipping_description(&self) -> String {
636 format!("Clip grad to L2 norm <= {}", self.max_grad_norm)
637 }
638}
639#[allow(dead_code)]
641#[derive(Debug, Clone)]
642pub struct ShuffleAmplification {
643 pub local_epsilon: f64,
644 pub n: usize,
645}
646impl ShuffleAmplification {
647 #[allow(dead_code)]
648 pub fn new(local_eps: f64, n: usize) -> Self {
649 Self {
650 local_epsilon: local_eps,
651 n,
652 }
653 }
654 #[allow(dead_code)]
655 pub fn central_epsilon_approx(&self) -> f64 {
656 let e_eps = self.local_epsilon.exp();
657 e_eps * (((self.n as f64).ln()).sqrt()) / (self.n as f64).sqrt()
658 }
659 #[allow(dead_code)]
660 pub fn is_stronger_than_local_dp(&self) -> bool {
661 self.central_epsilon_approx() < self.local_epsilon
662 }
663}
664#[allow(dead_code)]
666#[derive(Debug, Clone)]
667pub struct DpMedianEstimator {
668 pub epsilon: f64,
669 pub domain_size: usize,
670}
671impl DpMedianEstimator {
672 #[allow(dead_code)]
673 pub fn new(eps: f64, d: usize) -> Self {
674 Self {
675 epsilon: eps,
676 domain_size: d,
677 }
678 }
679 #[allow(dead_code)]
680 pub fn exponential_mechanism_based(&self) -> bool {
681 true
682 }
683 #[allow(dead_code)]
684 pub fn sensitivity(&self) -> usize {
685 1
686 }
687}
688#[allow(dead_code)]
690#[derive(Debug, Clone)]
691pub struct InferenceAttackModel {
692 pub adversary_advantage: f64,
693 pub privacy_bound: f64,
694}
695impl InferenceAttackModel {
696 #[allow(dead_code)]
697 pub fn new(adv: f64, eps: f64) -> Self {
698 Self {
699 adversary_advantage: adv,
700 privacy_bound: eps,
701 }
702 }
703 #[allow(dead_code)]
704 pub fn advantage_bounded_by_dp(&self) -> bool {
705 let dp_bound = self.privacy_bound.exp() - 1.0;
706 self.adversary_advantage <= dp_bound + 1e-10
707 }
708}
709#[allow(dead_code)]
711#[derive(Debug, Clone)]
712pub struct PrivacyEntry {
713 pub mechanism_name: String,
714 pub epsilon: f64,
715 pub delta: f64,
716 pub composition: CompositionType,
717}
718#[allow(dead_code)]
719#[derive(Debug, Clone, PartialEq, Eq)]
720pub enum CompositionType {
721 Sequential,
722 Parallel,
723 PostProcessing,
724}