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::Rng;
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::Rng>(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.remaining_epsilon.lock().unwrap();
180 *remaining >= epsilon && self.total_delta >= delta
181 }
182
183 pub fn consume(&self, epsilon: f32, delta: f32) -> Result<()> {
185 if !self.can_afford(epsilon, delta) {
186 return Err(Error::InvalidInput("Insufficient privacy budget".into()));
187 }
188
189 let mut remaining = self.remaining_epsilon.lock().unwrap();
190 *remaining -= epsilon;
191
192 let mut queries = self.queries.lock().unwrap();
193 queries.push(QueryRecord {
194 epsilon,
195 delta,
196 timestamp: std::time::SystemTime::now(),
197 });
198
199 Ok(())
200 }
201
202 pub fn remaining(&self) -> f32 {
204 *self.remaining_epsilon.lock().unwrap()
205 }
206
207 pub fn stats(&self) -> PrivacyBudgetStats {
209 let remaining = *self.remaining_epsilon.lock().unwrap();
210 let queries = self.queries.lock().unwrap();
211
212 PrivacyBudgetStats {
213 total_epsilon: self.total_epsilon,
214 remaining_epsilon: remaining,
215 consumed_epsilon: self.total_epsilon - remaining,
216 total_delta: self.total_delta,
217 num_queries: queries.len(),
218 }
219 }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct PrivacyBudgetStats {
225 pub total_epsilon: f32,
227 pub remaining_epsilon: f32,
229 pub consumed_epsilon: f32,
231 pub total_delta: f32,
233 pub num_queries: usize,
235}
236
237pub struct PrivateEmbedding {
239 #[allow(dead_code)]
241 original: Vec<f32>,
242 pub noisy: Vec<f32>,
244 mechanism: PrivacyMechanism,
246}
247
248impl PrivateEmbedding {
249 pub fn new(embedding: Vec<f32>, mechanism: PrivacyMechanism) -> Self {
251 let noisy = mechanism.add_noise(&embedding);
252
253 Self {
254 original: embedding,
255 noisy,
256 mechanism,
257 }
258 }
259
260 pub fn public_embedding(&self) -> &[f32] {
262 &self.noisy
263 }
264
265 pub fn privacy_params(&self) -> (f32, f32) {
267 (self.mechanism.epsilon(), self.mechanism.delta())
268 }
269
270 pub fn utility_loss(&self) -> f32 {
272 self.mechanism.expected_utility_loss(self.noisy.len())
273 }
274}
275
276pub struct TradeoffAnalyzer {
278 epsilons: Vec<f32>,
280 sensitivity: f32,
282}
283
284impl TradeoffAnalyzer {
285 pub fn new(sensitivity: f32) -> Self {
287 let epsilons = vec![0.1, 0.5, 1.0, 2.0, 5.0, 10.0];
289
290 Self {
291 epsilons,
292 sensitivity,
293 }
294 }
295
296 pub fn analyze(&self, dimension: usize) -> Vec<TradeoffPoint> {
298 self.epsilons
299 .iter()
300 .map(|&epsilon| {
301 let mechanism = PrivacyMechanism::laplacian(epsilon, self.sensitivity).unwrap();
302 let utility_loss = mechanism.expected_utility_loss(dimension);
303
304 TradeoffPoint {
305 epsilon,
306 delta: 0.0,
307 utility_loss,
308 }
309 })
310 .collect()
311 }
312
313 pub fn find_epsilon_for_utility(&self, dimension: usize, max_utility_loss: f32) -> Option<f32> {
315 let points = self.analyze(dimension);
316
317 points
318 .into_iter()
319 .filter(|p| p.utility_loss <= max_utility_loss)
320 .map(|p| p.epsilon)
321 .min_by(|a, b| a.partial_cmp(b).unwrap())
322 }
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct TradeoffPoint {
328 pub epsilon: f32,
330 pub delta: f32,
332 pub utility_loss: f32,
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_laplacian_mechanism() {
342 let mechanism = PrivacyMechanism::laplacian(1.0, 1.0).unwrap();
343 assert_eq!(mechanism.epsilon(), 1.0);
344 assert_eq!(mechanism.delta(), 0.0);
345
346 let embedding = vec![1.0, 2.0, 3.0];
347 let noisy = mechanism.add_noise(&embedding);
348
349 assert_eq!(noisy.len(), embedding.len());
350 assert_ne!(noisy, embedding);
352 }
353
354 #[test]
355 fn test_gaussian_mechanism() {
356 let mechanism = PrivacyMechanism::gaussian(1.0, 0.001, 1.0).unwrap();
357 assert_eq!(mechanism.epsilon(), 1.0);
358 assert!(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 }
365
366 #[test]
367 fn test_privacy_budget() {
368 let budget = PrivacyBudget::new(10.0, 0.001).unwrap();
369
370 assert!(budget.can_afford(1.0, 0.0001));
371 assert_eq!(budget.remaining(), 10.0);
372
373 budget.consume(1.0, 0.0001).unwrap();
374 assert_eq!(budget.remaining(), 9.0);
375
376 let stats = budget.stats();
377 assert_eq!(stats.consumed_epsilon, 1.0);
378 assert_eq!(stats.num_queries, 1);
379 }
380
381 #[test]
382 fn test_budget_exhaustion() {
383 let budget = PrivacyBudget::new(1.0, 0.001).unwrap();
384
385 budget.consume(0.5, 0.0001).unwrap();
386 budget.consume(0.5, 0.0001).unwrap();
387
388 assert!(budget.consume(0.1, 0.0001).is_err());
390 }
391
392 #[test]
393 fn test_private_embedding() {
394 let embedding = vec![1.0, 2.0, 3.0];
395 let mechanism = PrivacyMechanism::laplacian(1.0, 1.0).unwrap();
396
397 let private_emb = PrivateEmbedding::new(embedding.clone(), mechanism);
398
399 assert_eq!(private_emb.public_embedding().len(), embedding.len());
400 assert_eq!(private_emb.privacy_params().0, 1.0);
401 assert!(private_emb.utility_loss() > 0.0);
402 }
403
404 #[test]
405 fn test_tradeoff_analyzer() {
406 let analyzer = TradeoffAnalyzer::new(1.0);
407 let points = analyzer.analyze(768);
408
409 assert!(!points.is_empty());
410 assert!(points[0].utility_loss > points.last().unwrap().utility_loss);
412 }
413
414 #[test]
415 fn test_find_epsilon_for_utility() {
416 let analyzer = TradeoffAnalyzer::new(1.0);
417 let epsilon = analyzer.find_epsilon_for_utility(768, 10.0);
418
419 assert!(epsilon.is_some());
420 assert!(epsilon.unwrap() > 0.0);
421 }
422
423 #[test]
424 fn test_utility_loss_estimation() {
425 let mechanism = PrivacyMechanism::laplacian(1.0, 1.0).unwrap();
426 let loss = mechanism.expected_utility_loss(768);
427
428 assert!(loss > 20.0 && loss < 30.0);
430 }
431}