use super::config::{NoiseMechanism, PrivacyConfig};
use chrono::{DateTime, Utc};
use scirs2_core::ndarray_ext::Array2;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivacyEngine {
pub config: PrivacyConfig,
pub privacy_accountant: PrivacyAccountant,
pub noise_generator: NoiseGenerator,
pub clipping_mechanisms: ClippingMechanisms,
}
impl PrivacyEngine {
pub fn new(config: PrivacyConfig) -> Self {
Self {
privacy_accountant: PrivacyAccountant::new(config.epsilon, config.delta),
noise_generator: NoiseGenerator::new(config.noise_mechanism.clone()),
clipping_mechanisms: ClippingMechanisms::new(config.clipping_threshold),
config,
}
}
pub fn process_gradients(
&mut self,
gradients: &Array2<f32>,
participant_id: Uuid,
) -> anyhow::Result<Array2<f32>> {
let clipped_gradients = self.clipping_mechanisms.clip_gradients(gradients);
let noisy_gradients = if self.config.enable_differential_privacy {
self.noise_generator.add_noise(&clipped_gradients)
} else {
clipped_gradients
};
let privacy_cost = self.calculate_privacy_cost(&noisy_gradients);
self.privacy_accountant
.consume_budget(participant_id, privacy_cost)?;
Ok(noisy_gradients)
}
fn calculate_privacy_cost(&self, _gradients: &Array2<f32>) -> f64 {
self.config.local_epsilon / 100.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivacyAccountant {
pub total_epsilon: f64,
pub used_epsilon: f64,
pub delta: f64,
pub participant_budgets: HashMap<Uuid, f64>,
pub round_budgets: Vec<f64>,
pub budget_history: Vec<BudgetEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BudgetEntry {
pub timestamp: DateTime<Utc>,
pub participant_id: Option<Uuid>,
pub privacy_cost: f64,
pub operation: String,
pub remaining_budget: f64,
}
impl PrivacyAccountant {
pub fn new(total_epsilon: f64, delta: f64) -> Self {
Self {
total_epsilon,
used_epsilon: 0.0,
delta,
participant_budgets: HashMap::new(),
round_budgets: Vec::new(),
budget_history: Vec::new(),
}
}
pub fn consume_budget(&mut self, participant_id: Uuid, cost: f64) -> anyhow::Result<()> {
if self.used_epsilon + cost > self.total_epsilon {
return Err(anyhow::anyhow!("Privacy budget exceeded"));
}
self.used_epsilon += cost;
*self
.participant_budgets
.entry(participant_id)
.or_insert(0.0) += cost;
self.budget_history.push(BudgetEntry {
timestamp: Utc::now(),
participant_id: Some(participant_id),
privacy_cost: cost,
operation: "gradient_update".to_string(),
remaining_budget: self.total_epsilon - self.used_epsilon,
});
Ok(())
}
pub fn remaining_budget(&self) -> f64 {
self.total_epsilon - self.used_epsilon
}
pub fn is_budget_available(&self, required_budget: f64) -> bool {
self.remaining_budget() >= required_budget
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoiseGenerator {
pub mechanism: NoiseMechanism,
pub scale: f64,
pub seed: Option<u64>,
}
impl NoiseGenerator {
pub fn new(mechanism: NoiseMechanism) -> Self {
Self {
mechanism,
scale: 1.0,
seed: None,
}
}
pub fn with_scale(mut self, scale: f64) -> Self {
self.scale = scale;
self
}
pub fn add_noise(&self, parameters: &Array2<f32>) -> Array2<f32> {
match self.mechanism {
NoiseMechanism::Gaussian => self.add_gaussian_noise(parameters),
NoiseMechanism::Laplace => self.add_laplace_noise(parameters),
NoiseMechanism::Exponential => self.add_exponential_noise(parameters),
NoiseMechanism::SparseVector => self.add_sparse_vector_noise(parameters),
}
}
fn add_gaussian_noise(&self, parameters: &Array2<f32>) -> Array2<f32> {
let noise = Array2::from_shape_fn(parameters.raw_dim(), |_| {
let u1: f32 = {
use scirs2_core::random::{Random, RngExt};
let mut random = Random::default();
random.random::<f32>()
};
let u2: f32 = {
use scirs2_core::random::{Random, RngExt};
let mut random = Random::default();
random.random::<f32>()
};
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos();
z * self.scale as f32
});
parameters + &noise
}
fn add_laplace_noise(&self, parameters: &Array2<f32>) -> Array2<f32> {
let noise = Array2::from_shape_fn(parameters.raw_dim(), |_| {
let u: f32 = {
use scirs2_core::random::{Random, RngExt};
let mut random = Random::default();
random.random::<f32>() - 0.5
};
let sign = if u > 0.0 { 1.0 } else { -1.0 };
-sign * (1.0 - 2.0 * u.abs()).ln() * self.scale as f32
});
parameters + &noise
}
fn add_exponential_noise(&self, parameters: &Array2<f32>) -> Array2<f32> {
self.add_laplace_noise(parameters)
}
fn add_sparse_vector_noise(&self, parameters: &Array2<f32>) -> Array2<f32> {
let mut result = self.add_gaussian_noise(parameters);
result.mapv_inplace(|x| {
if x.abs() < self.scale as f32 * 0.1 {
0.0
} else {
x
}
});
result
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClippingMechanisms {
pub threshold: f64,
pub method: ClippingMethod,
pub adaptive_clipping: bool,
pub threshold_history: Vec<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClippingMethod {
L2Norm,
L1Norm,
ElementWise,
Adaptive,
}
impl ClippingMechanisms {
pub fn new(threshold: f64) -> Self {
Self {
threshold,
method: ClippingMethod::L2Norm,
adaptive_clipping: false,
threshold_history: Vec::new(),
}
}
pub fn with_method(mut self, method: ClippingMethod) -> Self {
self.method = method;
self
}
pub fn with_adaptive_clipping(mut self, adaptive: bool) -> Self {
self.adaptive_clipping = adaptive;
self
}
pub fn clip_gradients(&mut self, gradients: &Array2<f32>) -> Array2<f32> {
let result = match self.method {
ClippingMethod::L2Norm => self.clip_l2_norm(gradients),
ClippingMethod::L1Norm => self.clip_l1_norm(gradients),
ClippingMethod::ElementWise => self.clip_element_wise(gradients),
ClippingMethod::Adaptive => self.clip_adaptive(gradients),
};
if self.adaptive_clipping {
let current_norm = self.calculate_norm(gradients);
self.threshold_history.push(current_norm);
if self.threshold_history.len() > 10 {
let avg_norm: f64 = self.threshold_history.iter().sum::<f64>()
/ self.threshold_history.len() as f64;
self.threshold = avg_norm * 1.2; self.threshold_history.remove(0); }
}
result
}
fn calculate_norm(&self, gradients: &Array2<f32>) -> f64 {
match self.method {
ClippingMethod::L2Norm | ClippingMethod::Adaptive => gradients
.iter()
.map(|x| (*x as f64) * (*x as f64))
.sum::<f64>()
.sqrt(),
ClippingMethod::L1Norm => gradients.iter().map(|x| (*x as f64).abs()).sum::<f64>(),
ClippingMethod::ElementWise => gradients
.iter()
.map(|x| (*x as f64).abs())
.fold(0.0, f64::max),
}
}
fn clip_l2_norm(&self, gradients: &Array2<f32>) -> Array2<f32> {
let norm = gradients.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > self.threshold as f32 {
gradients * (self.threshold as f32 / norm)
} else {
gradients.clone()
}
}
fn clip_l1_norm(&self, gradients: &Array2<f32>) -> Array2<f32> {
let norm = gradients.iter().map(|x| x.abs()).sum::<f32>();
if norm > self.threshold as f32 {
gradients * (self.threshold as f32 / norm)
} else {
gradients.clone()
}
}
fn clip_element_wise(&self, gradients: &Array2<f32>) -> Array2<f32> {
gradients.mapv(|x| x.max(-self.threshold as f32).min(self.threshold as f32))
}
fn clip_adaptive(&self, gradients: &Array2<f32>) -> Array2<f32> {
self.clip_l2_norm(gradients)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivacyParams {
pub epsilon: f64,
pub delta: f64,
pub sensitivity: f64,
pub composition_method: CompositionMethod,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CompositionMethod {
Basic,
Advanced,
RenyiDP { alpha: f64 },
PLD,
GDP,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvancedPrivacyAccountant {
pub privacy_params: PrivacyParams,
pub compositions: Vec<CompositionEntry>,
pub current_guarantees: PrivacyGuarantees,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompositionEntry {
pub timestamp: DateTime<Utc>,
pub privacy_cost: (f64, f64), pub mechanism: String,
pub participant_id: Option<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivacyGuarantees {
pub total_epsilon: f64,
pub total_delta: f64,
pub worst_case_loss: f64,
pub expected_loss: f64,
pub confidence_intervals: HashMap<String, (f64, f64)>,
}