1use ipfrs_core::{Error, Result};
10use serde::{Deserialize, Serialize};
11use std::sync::{Arc, Mutex};
12
13#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
15pub enum NoiseDistribution {
16 Laplacian { scale: f32 },
18 Gaussian { sigma: f32 },
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct PrivacyMechanism {
25 distribution: NoiseDistribution,
27 epsilon: f32,
29 delta: f32,
31 sensitivity: f32,
33}
34
35impl PrivacyMechanism {
36 pub fn laplacian(epsilon: f32, sensitivity: f32) -> Result<Self> {
38 if epsilon <= 0.0 {
39 return Err(Error::InvalidInput("Epsilon must be positive".into()));
40 }
41 if sensitivity <= 0.0 {
42 return Err(Error::InvalidInput("Sensitivity must be positive".into()));
43 }
44
45 let scale = sensitivity / epsilon;
46
47 Ok(Self {
48 distribution: NoiseDistribution::Laplacian { scale },
49 epsilon,
50 delta: 0.0,
51 sensitivity,
52 })
53 }
54
55 pub fn gaussian(epsilon: f32, delta: f32, sensitivity: f32) -> Result<Self> {
57 if epsilon <= 0.0 {
58 return Err(Error::InvalidInput("Epsilon must be positive".into()));
59 }
60 if delta <= 0.0 || delta >= 1.0 {
61 return Err(Error::InvalidInput("Delta must be in (0, 1)".into()));
62 }
63 if sensitivity <= 0.0 {
64 return Err(Error::InvalidInput("Sensitivity must be positive".into()));
65 }
66
67 let sigma = sensitivity * (2.0 * (1.25 / delta).ln()).sqrt() / epsilon;
70
71 Ok(Self {
72 distribution: NoiseDistribution::Gaussian { sigma },
73 epsilon,
74 delta,
75 sensitivity,
76 })
77 }
78
79 pub fn add_noise(&self, embedding: &[f32]) -> Vec<f32> {
81 use rand::RngExt;
82 let mut rng = rand::rng();
83
84 match self.distribution {
85 NoiseDistribution::Laplacian { scale } => embedding
86 .iter()
87 .map(|&x| x + sample_laplacian(&mut rng, scale))
88 .collect(),
89 NoiseDistribution::Gaussian { sigma } => {
90 embedding
91 .iter()
92 .map(|&x| {
93 let noise: f32 = rng.random_range(-1.0..1.0);
95 x + noise * sigma
96 })
97 .collect()
98 }
99 }
100 }
101
102 pub fn epsilon(&self) -> f32 {
104 self.epsilon
105 }
106
107 pub fn delta(&self) -> f32 {
109 self.delta
110 }
111
112 pub fn expected_utility_loss(&self, dimension: usize) -> f32 {
114 match self.distribution {
115 NoiseDistribution::Laplacian { scale } => {
116 scale * (dimension as f32).sqrt()
119 }
120 NoiseDistribution::Gaussian { sigma } => {
121 sigma * (dimension as f32).sqrt()
124 }
125 }
126 }
127}
128
129fn sample_laplacian<R: rand::RngExt>(rng: &mut R, scale: f32) -> f32 {
131 let u: f32 = rng.random_range(-0.5..0.5);
132 if u >= 0.0 {
133 -scale * (1.0 - 2.0 * u).ln()
134 } else {
135 scale * (1.0 + 2.0 * u).ln()
136 }
137}
138
139pub struct PrivacyBudget {
141 total_epsilon: f32,
143 remaining_epsilon: Arc<Mutex<f32>>,
145 total_delta: f32,
147 queries: Arc<Mutex<Vec<QueryRecord>>>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct QueryRecord {
154 pub epsilon: f32,
156 pub delta: f32,
158 pub timestamp: std::time::SystemTime,
160}
161
162impl PrivacyBudget {
163 pub fn new(total_epsilon: f32, total_delta: f32) -> Result<Self> {
165 if total_epsilon <= 0.0 {
166 return Err(Error::InvalidInput("Total epsilon must be positive".into()));
167 }
168
169 Ok(Self {
170 total_epsilon,
171 remaining_epsilon: Arc::new(Mutex::new(total_epsilon)),
172 total_delta,
173 queries: Arc::new(Mutex::new(Vec::new())),
174 })
175 }
176
177 pub fn can_afford(&self, epsilon: f32, delta: f32) -> bool {
179 let remaining = self
180 .remaining_epsilon
181 .lock()
182 .unwrap_or_else(|e| e.into_inner());
183 *remaining >= epsilon && self.total_delta >= delta
184 }
185
186 pub fn consume(&self, epsilon: f32, delta: f32) -> Result<()> {
188 if !self.can_afford(epsilon, delta) {
189 return Err(Error::InvalidInput("Insufficient privacy budget".into()));
190 }
191
192 let mut remaining = self
193 .remaining_epsilon
194 .lock()
195 .unwrap_or_else(|e| e.into_inner());
196 *remaining -= epsilon;
197
198 let mut queries = self.queries.lock().unwrap_or_else(|e| e.into_inner());
199 queries.push(QueryRecord {
200 epsilon,
201 delta,
202 timestamp: std::time::SystemTime::now(),
203 });
204
205 Ok(())
206 }
207
208 pub fn remaining(&self) -> f32 {
210 *self
211 .remaining_epsilon
212 .lock()
213 .unwrap_or_else(|e| e.into_inner())
214 }
215
216 pub fn stats(&self) -> PrivacyBudgetStats {
218 let remaining = *self
219 .remaining_epsilon
220 .lock()
221 .unwrap_or_else(|e| e.into_inner());
222 let queries = self.queries.lock().unwrap_or_else(|e| e.into_inner());
223
224 PrivacyBudgetStats {
225 total_epsilon: self.total_epsilon,
226 remaining_epsilon: remaining,
227 consumed_epsilon: self.total_epsilon - remaining,
228 total_delta: self.total_delta,
229 num_queries: queries.len(),
230 }
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct PrivacyBudgetStats {
237 pub total_epsilon: f32,
239 pub remaining_epsilon: f32,
241 pub consumed_epsilon: f32,
243 pub total_delta: f32,
245 pub num_queries: usize,
247}
248
249pub struct PrivateEmbedding {
251 #[allow(dead_code)]
253 original: Vec<f32>,
254 pub noisy: Vec<f32>,
256 mechanism: PrivacyMechanism,
258}
259
260impl PrivateEmbedding {
261 pub fn new(embedding: Vec<f32>, mechanism: PrivacyMechanism) -> Self {
263 let noisy = mechanism.add_noise(&embedding);
264
265 Self {
266 original: embedding,
267 noisy,
268 mechanism,
269 }
270 }
271
272 pub fn public_embedding(&self) -> &[f32] {
274 &self.noisy
275 }
276
277 pub fn privacy_params(&self) -> (f32, f32) {
279 (self.mechanism.epsilon(), self.mechanism.delta())
280 }
281
282 pub fn utility_loss(&self) -> f32 {
284 self.mechanism.expected_utility_loss(self.noisy.len())
285 }
286}
287
288pub struct TradeoffAnalyzer {
290 epsilons: Vec<f32>,
292 sensitivity: f32,
294}
295
296impl TradeoffAnalyzer {
297 pub fn new(sensitivity: f32) -> Self {
299 let epsilons = vec![0.1, 0.5, 1.0, 2.0, 5.0, 10.0];
301
302 Self {
303 epsilons,
304 sensitivity,
305 }
306 }
307
308 pub fn analyze(&self, dimension: usize) -> Vec<TradeoffPoint> {
310 self.epsilons
311 .iter()
312 .map(|&epsilon| {
313 let mechanism = PrivacyMechanism::laplacian(epsilon, self.sensitivity)
314 .expect("epsilons from preset list are all positive");
315 let utility_loss = mechanism.expected_utility_loss(dimension);
316
317 TradeoffPoint {
318 epsilon,
319 delta: 0.0,
320 utility_loss,
321 }
322 })
323 .collect()
324 }
325
326 pub fn find_epsilon_for_utility(&self, dimension: usize, max_utility_loss: f32) -> Option<f32> {
328 let points = self.analyze(dimension);
329
330 points
331 .into_iter()
332 .filter(|p| p.utility_loss <= max_utility_loss)
333 .map(|p| p.epsilon)
334 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
335 }
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct TradeoffPoint {
341 pub epsilon: f32,
343 pub delta: f32,
345 pub utility_loss: f32,
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn test_laplacian_mechanism() {
355 let mechanism =
356 PrivacyMechanism::laplacian(1.0, 1.0).expect("test: valid laplacian params");
357 assert_eq!(mechanism.epsilon(), 1.0);
358 assert_eq!(mechanism.delta(), 0.0);
359
360 let embedding = vec![1.0, 2.0, 3.0];
361 let noisy = mechanism.add_noise(&embedding);
362
363 assert_eq!(noisy.len(), embedding.len());
364 assert_ne!(noisy, embedding);
366 }
367
368 #[test]
369 fn test_gaussian_mechanism() {
370 let mechanism =
371 PrivacyMechanism::gaussian(1.0, 0.001, 1.0).expect("test: valid gaussian params");
372 assert_eq!(mechanism.epsilon(), 1.0);
373 assert!(mechanism.delta() > 0.0);
374
375 let embedding = vec![1.0, 2.0, 3.0];
376 let noisy = mechanism.add_noise(&embedding);
377
378 assert_eq!(noisy.len(), embedding.len());
379 }
380
381 #[test]
382 fn test_privacy_budget() {
383 let budget = PrivacyBudget::new(10.0, 0.001).expect("test: valid budget params");
384
385 assert!(budget.can_afford(1.0, 0.0001));
386 assert_eq!(budget.remaining(), 10.0);
387
388 budget
389 .consume(1.0, 0.0001)
390 .expect("test: consume within budget");
391 assert_eq!(budget.remaining(), 9.0);
392
393 let stats = budget.stats();
394 assert_eq!(stats.consumed_epsilon, 1.0);
395 assert_eq!(stats.num_queries, 1);
396 }
397
398 #[test]
399 fn test_budget_exhaustion() {
400 let budget = PrivacyBudget::new(1.0, 0.001).expect("test: valid budget params");
401
402 budget
403 .consume(0.5, 0.0001)
404 .expect("test: consume within budget");
405 budget
406 .consume(0.5, 0.0001)
407 .expect("test: consume within budget");
408
409 assert!(budget.consume(0.1, 0.0001).is_err());
411 }
412
413 #[test]
414 fn test_private_embedding() {
415 let embedding = vec![1.0, 2.0, 3.0];
416 let mechanism =
417 PrivacyMechanism::laplacian(1.0, 1.0).expect("test: valid laplacian params");
418
419 let private_emb = PrivateEmbedding::new(embedding.clone(), mechanism);
420
421 assert_eq!(private_emb.public_embedding().len(), embedding.len());
422 assert_eq!(private_emb.privacy_params().0, 1.0);
423 assert!(private_emb.utility_loss() > 0.0);
424 }
425
426 #[test]
427 fn test_tradeoff_analyzer() {
428 let analyzer = TradeoffAnalyzer::new(1.0);
429 let points = analyzer.analyze(768);
430
431 assert!(!points.is_empty());
432 assert!(
434 points[0].utility_loss
435 > points
436 .last()
437 .expect("test: non-empty points vec")
438 .utility_loss
439 );
440 }
441
442 #[test]
443 fn test_find_epsilon_for_utility() {
444 let analyzer = TradeoffAnalyzer::new(1.0);
445 let epsilon = analyzer.find_epsilon_for_utility(768, 10.0);
446
447 assert!(epsilon.is_some());
448 assert!(epsilon.expect("test: epsilon found for utility bound") > 0.0);
449 }
450
451 #[test]
452 fn test_utility_loss_estimation() {
453 let mechanism =
454 PrivacyMechanism::laplacian(1.0, 1.0).expect("test: valid laplacian params");
455 let loss = mechanism.expected_utility_loss(768);
456
457 assert!(loss > 20.0 && loss < 30.0);
459 }
460}