Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
+613
View File
@@ -0,0 +1,613 @@
//! Differential privacy mechanisms for federated learning.
//!
//! This module implements privacy-preserving mechanisms including gradient
//! clipping, noise addition, and privacy budget accounting.
use rand::Rng;
use fedmed_shared::{PrivacyAccountantType, PrivacyConfig};
/// Differential privacy mechanism for adding noise to gradients.
#[derive(Debug, Clone)]
pub struct DifferentialPrivacy {
/// Privacy configuration.
config: PrivacyConfig,
/// Random number generator seed (reserved for reproducibility).
#[allow(dead_code)]
seed: u64,
}
impl DifferentialPrivacy {
/// Create a new differential privacy mechanism.
#[must_use]
pub fn new(config: PrivacyConfig) -> Self {
Self { config, seed: 42 }
}
/// Add Gaussian noise to gradients for differential privacy.
#[must_use]
pub fn add_noise(&self, gradients: &[f32]) -> Vec<f32> {
let mut rng = rand::thread_rng();
let sigma = self.config.noise_multiplier as f32 * self.config.clip_norm as f32;
gradients
.iter()
.map(|g| {
// Box-Muller transform for Gaussian noise
let u1: f32 = rng.r#gen::<f32>();
let u2: f32 = rng.r#gen::<f32>();
let noise = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos();
g + noise * sigma
})
.collect()
}
/// Add noise using local differential privacy (for client-side).
#[must_use]
pub fn add_local_noise(&self, gradients: &[f32]) -> Vec<f32> {
if !self.config.local_dp {
return gradients.to_vec();
}
let mut rng = rand::thread_rng();
// Local DP typically uses higher noise
let sigma = self.config.noise_multiplier as f32 * self.config.clip_norm as f32 * 2.0;
gradients
.iter()
.map(|g| {
let u1: f32 = rng.r#gen::<f32>();
let u2: f32 = rng.r#gen::<f32>();
let noise = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos();
g + noise * sigma
})
.collect()
}
/// Get the noise standard deviation.
#[must_use]
pub fn noise_std(&self) -> f64 {
self.config.noise_multiplier * self.config.clip_norm
}
/// Get the privacy configuration.
#[must_use]
pub fn config(&self) -> &PrivacyConfig {
&self.config
}
/// Update the noise multiplier.
pub fn set_noise_multiplier(&mut self, multiplier: f64) {
self.config.noise_multiplier = multiplier;
}
}
/// Gradient clipper for bounding sensitivity.
#[derive(Debug, Clone)]
pub struct GradientClipper {
/// Maximum L2 norm for gradients.
clip_norm: f64,
/// Whether to use per-sample clipping (reserved for advanced use).
#[allow(dead_code)]
per_sample: bool,
}
impl Default for GradientClipper {
fn default() -> Self {
Self::new(1.0)
}
}
impl GradientClipper {
/// Create a new gradient clipper.
#[must_use]
pub fn new(clip_norm: f64) -> Self {
Self {
clip_norm,
per_sample: false,
}
}
/// Create a per-sample gradient clipper.
#[must_use]
pub fn per_sample(clip_norm: f64) -> Self {
Self {
clip_norm,
per_sample: true,
}
}
/// Clip gradients to bound their L2 norm.
#[must_use]
pub fn clip(&self, gradients: &[f32]) -> Vec<f32> {
let norm = self.l2_norm(gradients);
if norm <= self.clip_norm as f32 {
return gradients.to_vec();
}
let scale = self.clip_norm as f32 / norm;
gradients.iter().map(|g| g * scale).collect()
}
/// Clip gradients with adaptive clipping.
#[must_use]
pub fn adaptive_clip(&self, gradients: &[f32], quantile: f32) -> Vec<f32> {
// Compute per-coordinate absolute values
let mut abs_grads: Vec<f32> = gradients.iter().map(|g| g.abs()).collect();
abs_grads.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
// Find the quantile threshold
let idx = ((abs_grads.len() as f32 * quantile) as usize).min(abs_grads.len() - 1);
let threshold = abs_grads[idx];
// Clip to threshold
gradients
.iter()
.map(|g| g.clamp(-threshold, threshold))
.collect()
}
/// Compute L2 norm of gradients.
#[must_use]
pub fn l2_norm(&self, gradients: &[f32]) -> f32 {
gradients.iter().map(|g| g * g).sum::<f32>().sqrt()
}
/// Compute L1 norm of gradients.
#[must_use]
pub fn l1_norm(&self, gradients: &[f32]) -> f32 {
gradients.iter().map(|g| g.abs()).sum()
}
/// Compute infinity norm of gradients.
#[must_use]
pub fn linf_norm(&self, gradients: &[f32]) -> f32 {
gradients
.iter()
.map(|g| g.abs())
.fold(0.0_f32, f32::max)
}
/// Get the clip norm.
#[must_use]
pub fn clip_norm(&self) -> f64 {
self.clip_norm
}
/// Set the clip norm.
pub fn set_clip_norm(&mut self, clip_norm: f64) {
self.clip_norm = clip_norm;
}
}
/// Privacy accountant for tracking privacy budget.
#[derive(Debug, Clone)]
pub struct PrivacyAccountant {
/// Privacy configuration.
config: PrivacyConfig,
/// Total privacy spent (epsilon).
epsilon_spent: f64,
/// Number of composition steps.
num_steps: usize,
/// Sample rate for subsampling amplification.
sample_rate: f64,
/// Accountant type.
accountant_type: PrivacyAccountantType,
}
impl PrivacyAccountant {
/// Create a new privacy accountant.
#[must_use]
pub fn new(config: PrivacyConfig) -> Self {
Self {
accountant_type: config.accountant,
config,
epsilon_spent: 0.0,
num_steps: 0,
sample_rate: 1.0,
}
}
/// Create an accountant with a specific sample rate.
#[must_use]
pub fn with_sample_rate(config: PrivacyConfig, sample_rate: f64) -> Self {
Self {
accountant_type: config.accountant,
config,
epsilon_spent: 0.0,
num_steps: 0,
sample_rate: sample_rate.clamp(0.0, 1.0),
}
}
/// Record a privacy-consuming step and return the epsilon spent.
pub fn step(&mut self, num_clients: usize) -> f64 {
self.num_steps += 1;
// Compute epsilon for this step based on accountant type
let step_epsilon = match self.accountant_type {
PrivacyAccountantType::Moments => self.moments_epsilon(num_clients),
PrivacyAccountantType::RDP => self.rdp_epsilon(num_clients),
PrivacyAccountantType::GDP => self.gdp_epsilon(num_clients),
PrivacyAccountantType::PLD => self.pld_epsilon(num_clients),
};
self.epsilon_spent += step_epsilon;
step_epsilon
}
/// Compute epsilon using moments accountant.
fn moments_epsilon(&self, _num_clients: usize) -> f64 {
// Simplified moments accountant
let sigma = self.config.noise_multiplier;
let q = self.sample_rate;
// Basic composition: epsilon per step
if sigma > 0.0 {
q * (1.0 / sigma.powi(2))
} else {
f64::INFINITY
}
}
/// Compute epsilon using Renyi differential privacy.
fn rdp_epsilon(&self, _num_clients: usize) -> f64 {
// Simplified RDP accountant
let sigma = self.config.noise_multiplier;
let q = self.sample_rate;
let alpha = 2.0; // RDP order
if sigma > 0.0 {
// RDP bound with subsampling: q * alpha / (2 * sigma^2)
let rdp = q * alpha / (2.0 * sigma.powi(2));
// Convert to (epsilon, delta)-DP
let delta = self.config.delta;
rdp + (1.0 / delta).ln() / (alpha - 1.0)
} else {
f64::INFINITY
}
}
/// Compute epsilon using Gaussian differential privacy.
fn gdp_epsilon(&self, _num_clients: usize) -> f64 {
// Simplified GDP accountant
let sigma = self.config.noise_multiplier;
if sigma > 0.0 {
// Central limit theorem approximation
1.0 / sigma
} else {
f64::INFINITY
}
}
/// Compute epsilon using privacy loss distribution.
fn pld_epsilon(&self, _num_clients: usize) -> f64 {
// Simplified PLD accountant (falls back to RDP for simplicity)
self.rdp_epsilon(_num_clients)
}
/// Get total epsilon spent.
#[must_use]
pub fn epsilon_spent(&self) -> f64 {
self.epsilon_spent
}
/// Get number of steps.
#[must_use]
pub fn num_steps(&self) -> usize {
self.num_steps
}
/// Check if privacy budget is exhausted.
#[must_use]
pub fn is_budget_exhausted(&self) -> bool {
if let Some(target) = self.config.target_epsilon {
self.epsilon_spent >= target
} else {
false
}
}
/// Get remaining privacy budget.
#[must_use]
pub fn remaining_budget(&self) -> Option<f64> {
self.config
.target_epsilon
.map(|target| (target - self.epsilon_spent).max(0.0))
}
/// Estimate number of remaining steps.
#[must_use]
pub fn estimate_remaining_steps(&self) -> Option<usize> {
if self.num_steps == 0 || self.epsilon_spent <= 0.0 {
return None;
}
let epsilon_per_step = self.epsilon_spent / self.num_steps as f64;
self.remaining_budget()
.map(|remaining| (remaining / epsilon_per_step) as usize)
}
/// Reset the accountant.
pub fn reset(&mut self) {
self.epsilon_spent = 0.0;
self.num_steps = 0;
}
/// Get the target epsilon.
#[must_use]
pub fn target_epsilon(&self) -> Option<f64> {
self.config.target_epsilon
}
/// Get delta.
#[must_use]
pub fn delta(&self) -> f64 {
self.config.delta
}
}
/// Secure noise generation for distributed settings.
#[derive(Debug)]
pub struct SecureNoiseGenerator {
/// Seed for reproducibility.
seed: u64,
/// Number of parties.
num_parties: usize,
}
impl SecureNoiseGenerator {
/// Create a new secure noise generator.
#[must_use]
pub fn new(seed: u64, num_parties: usize) -> Self {
Self { seed, num_parties }
}
/// Generate correlated noise that sums to Gaussian noise.
/// Each party generates their share independently.
#[must_use]
pub fn generate_noise_share(&self, party_id: usize, size: usize, sigma: f32) -> Vec<f32> {
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(self.seed.wrapping_add(party_id as u64));
(0..size)
.map(|_| {
let u1: f32 = rng.r#gen::<f32>();
let u2: f32 = rng.r#gen::<f32>();
let noise = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos();
noise * sigma / (self.num_parties as f32).sqrt()
})
.collect()
}
/// Combine noise shares from all parties.
#[must_use]
pub fn combine_shares(&self, shares: &[Vec<f32>]) -> Vec<f32> {
if shares.is_empty() {
return vec![];
}
let size = shares[0].len();
let mut combined = vec![0.0_f32; size];
for share in shares {
for (i, &s) in share.iter().enumerate() {
if i < combined.len() {
combined[i] += s;
}
}
}
combined
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_config() -> PrivacyConfig {
PrivacyConfig {
epsilon: 8.0,
delta: 1e-5,
clip_norm: 1.0,
noise_multiplier: 1.1,
local_dp: false,
accountant: PrivacyAccountantType::RDP,
target_epsilon: Some(10.0),
}
}
#[test]
fn test_differential_privacy() {
let config = sample_config();
let dp = DifferentialPrivacy::new(config);
let gradients = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let noisy = dp.add_noise(&gradients);
assert_eq!(noisy.len(), gradients.len());
// Noisy gradients should be different from original
assert!(
noisy
.iter()
.zip(gradients.iter())
.any(|(n, g)| (n - g).abs() > 0.0)
);
}
#[test]
fn test_local_dp() {
let mut config = sample_config();
config.local_dp = true;
let dp = DifferentialPrivacy::new(config);
let gradients = vec![0.1, 0.2, 0.3];
let noisy = dp.add_local_noise(&gradients);
assert_eq!(noisy.len(), gradients.len());
}
#[test]
fn test_gradient_clipping() {
let clipper = GradientClipper::new(1.0);
// Large gradients should be clipped
let large_grads = vec![10.0, 10.0, 10.0];
let clipped = clipper.clip(&large_grads);
let clipped_norm = clipper.l2_norm(&clipped);
assert!(clipped_norm <= 1.01); // Allow small floating point error
// Small gradients should not be affected
let small_grads = vec![0.1, 0.1, 0.1];
let not_clipped = clipper.clip(&small_grads);
assert_eq!(not_clipped, small_grads);
}
#[test]
fn test_adaptive_clipping() {
let clipper = GradientClipper::new(1.0);
let gradients = vec![0.1, 0.5, 1.0, 2.0, 5.0];
let clipped = clipper.adaptive_clip(&gradients, 0.5);
// Median value should be the threshold
assert!(clipped.iter().all(|&g| g.abs() <= 1.01));
}
#[test]
fn test_norms() {
let clipper = GradientClipper::new(1.0);
let gradients = vec![3.0, 4.0];
assert!((clipper.l2_norm(&gradients) - 5.0).abs() < 0.001);
assert!((clipper.l1_norm(&gradients) - 7.0).abs() < 0.001);
assert!((clipper.linf_norm(&gradients) - 4.0).abs() < 0.001);
}
#[test]
fn test_privacy_accountant() {
let config = sample_config();
let mut accountant = PrivacyAccountant::new(config);
assert_eq!(accountant.epsilon_spent(), 0.0);
assert_eq!(accountant.num_steps(), 0);
let epsilon = accountant.step(5);
assert!(epsilon > 0.0);
assert_eq!(accountant.num_steps(), 1);
assert!(accountant.epsilon_spent() > 0.0);
}
#[test]
fn test_budget_exhaustion() {
let mut config = sample_config();
config.target_epsilon = Some(0.1);
config.noise_multiplier = 0.1; // Low noise = high epsilon per step
let mut accountant = PrivacyAccountant::new(config);
// Take many steps until budget is exhausted
for _ in 0..1000 {
accountant.step(5);
if accountant.is_budget_exhausted() {
break;
}
}
assert!(accountant.is_budget_exhausted());
assert!(accountant.remaining_budget().unwrap() <= 0.0);
}
#[test]
fn test_remaining_steps_estimate() {
let config = sample_config();
let mut accountant = PrivacyAccountant::new(config);
// Take some steps
for _ in 0..10 {
accountant.step(5);
}
let remaining = accountant.estimate_remaining_steps();
assert!(remaining.is_some());
}
#[test]
fn test_accountant_reset() {
let config = sample_config();
let mut accountant = PrivacyAccountant::new(config);
accountant.step(5);
assert!(accountant.epsilon_spent() > 0.0);
accountant.reset();
assert_eq!(accountant.epsilon_spent(), 0.0);
assert_eq!(accountant.num_steps(), 0);
}
#[test]
fn test_secure_noise_generator() {
let generator = SecureNoiseGenerator::new(42, 3);
let shares: Vec<_> = (0..3)
.map(|party| generator.generate_noise_share(party, 10, 1.0))
.collect();
let combined = generator.combine_shares(&shares);
assert_eq!(combined.len(), 10);
}
#[test]
fn test_different_accountant_types() {
for accountant_type in [
PrivacyAccountantType::Moments,
PrivacyAccountantType::RDP,
PrivacyAccountantType::GDP,
PrivacyAccountantType::PLD,
] {
let mut config = sample_config();
config.accountant = accountant_type;
let mut accountant = PrivacyAccountant::new(config);
let epsilon = accountant.step(5);
assert!(epsilon > 0.0);
assert!(epsilon < f64::INFINITY);
}
}
#[test]
fn test_sample_rate() {
let config = sample_config();
let mut accountant_full = PrivacyAccountant::new(config.clone());
let mut accountant_sub = PrivacyAccountant::with_sample_rate(config, 0.1);
let eps_full = accountant_full.step(5);
let eps_sub = accountant_sub.step(5);
// Subsampled should have lower epsilon (privacy amplification)
assert!(eps_sub < eps_full);
}
#[test]
fn test_clipper_set_norm() {
let mut clipper = GradientClipper::new(1.0);
assert_eq!(clipper.clip_norm(), 1.0);
clipper.set_clip_norm(2.0);
assert_eq!(clipper.clip_norm(), 2.0);
}
#[test]
fn test_dp_set_noise_multiplier() {
let config = sample_config();
let mut dp = DifferentialPrivacy::new(config);
dp.set_noise_multiplier(2.0);
assert_eq!(dp.config().noise_multiplier, 2.0);
}
}