#[allow(dead_code)]
use crate::error::{OptimError, Result};
use scirs2_core::ndarray::{Array, ArrayBase, Data, DataMut, Dimension, ScalarOperand};
use scirs2_core::numeric::Float;
use scirs2_core::random::{thread_rng, Rng};
use scirs2_core::ScientificNumber;
use std::collections::VecDeque;
use std::fmt::Debug;
pub mod byzantine_tolerance;
pub mod differential_privacy; pub mod dp_sgd;
pub mod enhanced_audit;
pub mod federated; pub mod federated_privacy;
pub mod moment_accountant;
pub mod noise_mechanisms;
pub mod private_hyperparameter_optimization;
pub mod secure_multiparty;
pub mod utility_analysis;
use crate::optimizers::Optimizer;
pub use utility_analysis::{
AnalysisConfig, AnalysisMetadata, BudgetRecommendations, OptimalConfiguration, ParetoPoint,
PrivacyConfiguration, PrivacyParameterSpace, PrivacyRiskAssessment, PrivacyUtilityAnalyzer,
PrivacyUtilityResults, RobustnessResults, SensitivityResults, StatisticalTestResults,
UtilityMetric,
};
pub use federated::{
ByzantineRobustAggregator, ByzantineRobustConfig, ByzantineRobustMethod, ClientComposition,
CompositionStats, CrossDeviceConfig, CrossDevicePrivacyManager, DeviceProfile, DeviceType,
FederatedCompositionAnalyzer, FederatedCompositionMethod, OutlierDetectionResult,
ReputationSystemConfig, RoundComposition, SecureAggregationConfig, SecureAggregationPlan,
SecureAggregator, SeedSharingMethod, StatisticalTestConfig, StatisticalTestType, TemporalEvent,
TemporalEventType,
};
pub use differential_privacy::{
AmplificationConfig, AmplificationStats, PrivacyAmplificationAnalyzer, SubsamplingEvent,
};
#[derive(Debug, Clone)]
pub struct DifferentialPrivacyConfig {
pub target_epsilon: f64,
pub target_delta: f64,
pub noise_multiplier: f64,
pub l2_norm_clip: f64,
pub batch_size: usize,
pub dataset_size: usize,
pub max_steps: usize,
pub noise_mechanism: NoiseMechanism,
pub secure_aggregation: bool,
pub adaptive_clipping: bool,
pub adaptive_clip_init: f64,
pub adaptive_clip_lr: f64,
}
impl Default for DifferentialPrivacyConfig {
fn default() -> Self {
Self {
target_epsilon: 1.0,
target_delta: 1e-5,
noise_multiplier: 1.1,
l2_norm_clip: 1.0,
batch_size: 256,
dataset_size: 50000,
max_steps: 1000,
noise_mechanism: NoiseMechanism::Gaussian,
secure_aggregation: false,
adaptive_clipping: false,
adaptive_clip_init: 1.0,
adaptive_clip_lr: 0.2,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum NoiseMechanism {
Gaussian,
Laplace,
TreeAggregation,
ImprovedComposition,
}
#[derive(Debug, Clone)]
pub struct PrivacyBudget {
pub epsilon_consumed: f64,
pub delta_consumed: f64,
pub epsilon_remaining: f64,
pub delta_remaining: f64,
pub steps_taken: usize,
pub accounting_method: AccountingMethod,
pub estimated_steps_remaining: usize,
}
impl Default for PrivacyBudget {
fn default() -> Self {
Self {
epsilon_consumed: 0.0,
delta_consumed: 0.0,
epsilon_remaining: 1.0,
delta_remaining: 1e-5,
steps_taken: 0,
accounting_method: AccountingMethod::MomentsAccountant,
estimated_steps_remaining: 1000,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum AccountingMethod {
MomentsAccountant,
RenyiDP,
AdvancedComposition,
ZCDP,
}
pub struct DifferentiallyPrivateOptimizer<O, A, D>
where
A: Float + ScalarOperand + Debug + Send + Sync,
D: Dimension,
O: Optimizer<A, D>,
{
base_optimizer: O,
config: DifferentialPrivacyConfig,
accountant: MomentsAccountant,
rng: scirs2_core::random::CoreRandom,
adaptive_clip_state: Option<AdaptiveClippingState>,
gradient_history: VecDeque<GradientNorms>,
audit_trail: Vec<PrivacyEvent>,
step_count: usize,
_phantom: std::marker::PhantomData<(A, D)>,
}
#[derive(Debug, Clone)]
struct AdaptiveClippingState {
current_threshold: f64,
quantile_estimate: f64,
update_frequency: usize,
last_update_step: usize,
}
#[derive(Debug, Clone)]
struct GradientNorms {
step: usize,
pre_clip_norm: f64,
post_clip_norm: f64,
clipping_ratio: f64,
}
#[derive(Debug, Clone)]
pub struct PrivacyEvent {
step: usize,
event_type: PrivacyEventType,
epsilon_spent: f64,
delta_spent: f64,
noise_scale: f64,
}
#[derive(Debug, Clone)]
enum PrivacyEventType {
GradientRelease,
ModelUpdate,
ParameterQuery,
AdaptiveClipUpdate,
}
impl<O, A, D> DifferentiallyPrivateOptimizer<O, A, D>
where
A: Float
+ std::ops::AddAssign
+ std::ops::SubAssign
+ Send
+ Sync
+ scirs2_core::ndarray::ScalarOperand
+ std::fmt::Debug,
D: Dimension,
O: Optimizer<A, D>,
{
pub fn new(baseoptimizer: O, config: DifferentialPrivacyConfig) -> Result<Self> {
let accountant = MomentsAccountant::new(
config.noise_multiplier,
config.target_delta,
config.batch_size,
config.dataset_size,
);
let rng = thread_rng();
let adaptive_clip_state = if config.adaptive_clipping {
Some(AdaptiveClippingState {
current_threshold: config.adaptive_clip_init,
quantile_estimate: config.l2_norm_clip,
update_frequency: 50, last_update_step: 0,
})
} else {
None
};
Ok(Self {
base_optimizer: baseoptimizer,
config,
accountant,
rng,
adaptive_clip_state,
gradient_history: VecDeque::with_capacity(1000),
audit_trail: Vec::new(),
step_count: 0,
_phantom: std::marker::PhantomData,
})
}
pub fn dp_step(
&mut self,
params: &Array<A, D>,
gradients: &mut Array<A, D>,
) -> Result<Array<A, D>> {
self.step_count += 1;
if !self.has_privacy_budget()? {
return Err(OptimError::PrivacyBudgetExhausted {
consumed_epsilon: self.get_privacy_budget().epsilon_consumed,
target_epsilon: self.config.target_epsilon,
});
}
let pre_clip_norm = self.compute_l2_norm(gradients);
let clip_threshold = self.get_clipping_threshold();
let clipping_ratio = if pre_clip_norm > clip_threshold {
let scale = clip_threshold / pre_clip_norm;
gradients.mapv_inplace(|g| g * A::from(scale).expect("unwrap failed"));
scale
} else {
1.0
};
let post_clip_norm = self.compute_l2_norm(gradients);
self.add_calibrated_noise(gradients, clip_threshold)?;
let (epsilon_spent, delta_spent) = self.accountant.get_privacy_spent(self.step_count)?;
self.gradient_history.push_back(GradientNorms {
step: self.step_count,
pre_clip_norm,
post_clip_norm,
clipping_ratio,
});
if self.gradient_history.len() > 1000 {
self.gradient_history.pop_front();
}
self.audit_trail.push(PrivacyEvent {
step: self.step_count,
event_type: PrivacyEventType::GradientRelease,
epsilon_spent,
delta_spent,
noise_scale: self.config.noise_multiplier * clip_threshold,
});
if let Some(ref mut state) = self.adaptive_clip_state {
if self.step_count - state.last_update_step >= state.update_frequency {
state.last_update_step = self.step_count;
let target_ratio = 0.8; let new_threshold = pre_clip_norm * target_ratio;
state.current_threshold = new_threshold;
}
}
let updated_params = self.base_optimizer.step(params, gradients)?;
Ok(updated_params)
}
pub fn has_privacy_budget(&self) -> Result<bool> {
let budget = self.get_privacy_budget();
Ok(budget.epsilon_remaining > 0.0 && budget.delta_remaining > 0.0)
}
pub fn get_privacy_budget(&self) -> PrivacyBudget {
let (epsilon_consumed, delta_consumed) = self
.accountant
.get_privacy_spent(self.step_count)
.unwrap_or((0.0, 0.0));
let epsilon_remaining = (self.config.target_epsilon - epsilon_consumed).max(0.0);
let delta_remaining = (self.config.target_delta - delta_consumed).max(0.0);
let epsilon_per_step = if self.step_count > 0 {
epsilon_consumed / self.step_count as f64
} else {
0.0
};
let estimated_steps_remaining = if epsilon_per_step > 0.0 {
(epsilon_remaining / epsilon_per_step) as usize
} else {
usize::MAX
};
PrivacyBudget {
epsilon_consumed,
delta_consumed,
epsilon_remaining,
delta_remaining,
steps_taken: self.step_count,
accounting_method: AccountingMethod::MomentsAccountant,
estimated_steps_remaining,
}
}
fn compute_l2_norm<S, DIM>(&self, array: &ArrayBase<S, DIM>) -> f64
where
S: Data<Elem = A>,
DIM: Dimension,
{
array
.iter()
.map(|&x| {
let val = x.to_f64().unwrap_or(0.0);
val * val
})
.sum::<f64>()
.sqrt()
}
fn get_clipping_threshold(&self) -> f64 {
if let Some(ref state) = self.adaptive_clip_state {
state.current_threshold
} else {
self.config.l2_norm_clip
}
}
fn add_calibrated_noise<S, DIM>(
&mut self,
gradients: &mut ArrayBase<S, DIM>,
clip_threshold: f64,
) -> Result<()>
where
S: DataMut<Elem = A>,
DIM: Dimension,
{
let noise_scale = self.config.noise_multiplier * clip_threshold;
match self.config.noise_mechanism {
NoiseMechanism::Gaussian => {
let sigma_f64 = noise_scale.to_f64().unwrap_or(1.0);
gradients.mapv_inplace(|g| {
let u1: f64 = self.rng.gen_range(0.0..1.0);
let u2: f64 = self.rng.gen_range(0.0..1.0);
let z0 = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
let noise = A::from(z0 * sigma_f64).expect("unwrap failed");
g + noise
});
}
NoiseMechanism::Laplace => {
let scale_f64 = noise_scale.to_f64().unwrap_or(1.0);
gradients.mapv_inplace(|g| {
let u: f64 = self.rng.gen_range(0.0..1.0);
let laplace_sample = if u < 0.5 {
scale_f64 * (2.0 * u).ln()
} else {
-scale_f64 * (2.0 * (1.0 - u)).ln()
};
let noise = A::from(laplace_sample).expect("unwrap failed");
g + noise
});
}
_ => {
let sigma_f64 = noise_scale.to_f64().unwrap_or(1.0);
gradients.mapv_inplace(|g| {
let u1: f64 = self.rng.gen_range(0.0..1.0);
let u2: f64 = self.rng.gen_range(0.0..1.0);
let z0 = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
let noise = A::from(z0 * sigma_f64).expect("unwrap failed");
g + noise
});
}
}
Ok(())
}
fn update_adaptive_clipping(&mut self, state: &mut AdaptiveClippingState, current_norm: f64) {
let alpha = self.config.adaptive_clip_lr;
let target_quantile = 0.5;
if current_norm > state.quantile_estimate {
state.quantile_estimate += alpha * target_quantile;
} else {
state.quantile_estimate -= alpha * (1.0 - target_quantile);
}
state.current_threshold = state.quantile_estimate;
state.last_update_step = self.step_count;
}
pub fn get_clipping_stats(&self) -> ClippingStats {
if self.gradient_history.is_empty() {
return ClippingStats::default();
}
let total_steps = self.gradient_history.len();
let clipped_steps = self
.gradient_history
.iter()
.filter(|stats| stats.clipping_ratio < 1.0)
.count();
let avg_clipping_ratio: f64 = self
.gradient_history
.iter()
.map(|stats| stats.clipping_ratio)
.sum::<f64>()
/ total_steps as f64;
let avg_pre_clip_norm: f64 = self
.gradient_history
.iter()
.map(|stats| stats.pre_clip_norm)
.sum::<f64>()
/ total_steps as f64;
ClippingStats {
total_steps,
clipped_steps,
clipping_frequency: clipped_steps as f64 / total_steps as f64,
avg_clipping_ratio,
avg_pre_clip_norm,
current_threshold: self.get_clipping_threshold(),
}
}
pub fn get_audit_trail(&self) -> &[PrivacyEvent] {
&self.audit_trail
}
pub fn validate_privacy(&self) -> PrivacyValidation {
let budget = self.get_privacy_budget();
let clipping_stats = self.get_clipping_stats();
let mut warnings = Vec::new();
let mut is_valid = true;
if budget.epsilon_consumed > self.config.target_epsilon {
warnings.push("Epsilon budget exceeded".to_string());
is_valid = false;
}
if budget.delta_consumed > self.config.target_delta {
warnings.push("Delta budget exceeded".to_string());
is_valid = false;
}
if clipping_stats.clipping_frequency < 0.1 {
warnings.push(
"Low clipping frequency may indicate sub-optimal privacy-utility tradeoff"
.to_string(),
);
}
if clipping_stats.clipping_frequency > 0.9 {
warnings.push("High clipping frequency may severely impact utility".to_string());
}
PrivacyValidation {
is_valid,
budget: budget.clone(),
clipping_stats: clipping_stats.clone(),
warnings,
recommendations: self.generate_recommendations(&budget, &clipping_stats),
}
}
fn generate_recommendations(
&self,
budget: &PrivacyBudget,
clipping: &ClippingStats,
) -> Vec<String> {
let mut recommendations = Vec::new();
if clipping.clipping_frequency > 0.8 {
recommendations.push("Consider increasing the clipping threshold".to_string());
}
if clipping.clipping_frequency < 0.2 {
recommendations.push("Consider decreasing the clipping threshold".to_string());
}
if budget.epsilon_remaining < budget.epsilon_consumed * 0.1 {
recommendations.push("Privacy budget nearly exhausted - consider reducing noise multiplier for remaining steps".to_string());
}
recommendations
}
}
#[derive(Debug, Clone)]
pub struct ClippingStats {
pub total_steps: usize,
pub clipped_steps: usize,
pub clipping_frequency: f64,
pub avg_clipping_ratio: f64,
pub avg_pre_clip_norm: f64,
pub current_threshold: f64,
}
impl Default for ClippingStats {
fn default() -> Self {
Self {
total_steps: 0,
clipped_steps: 0,
clipping_frequency: 0.0,
avg_clipping_ratio: 1.0,
avg_pre_clip_norm: 0.0,
current_threshold: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct PrivacyValidation {
pub is_valid: bool,
pub budget: PrivacyBudget,
pub clipping_stats: ClippingStats,
pub warnings: Vec<String>,
pub recommendations: Vec<String>,
}
pub struct MomentsAccountant {
noise_multiplier: f64,
target_delta: f64,
batch_size: usize,
dataset_size: usize,
sampling_probability: f64,
}
impl MomentsAccountant {
pub fn new(
noise_multiplier: f64,
target_delta: f64,
batch_size: usize,
dataset_size: usize,
) -> Self {
let sampling_probability = batch_size as f64 / dataset_size as f64;
Self {
noise_multiplier,
target_delta,
batch_size,
dataset_size,
sampling_probability,
}
}
pub fn get_privacy_spent(&self, steps: usize) -> Result<(f64, f64)> {
if steps == 0 {
return Ok((0.0, 0.0));
}
let sigma = self.noise_multiplier;
let q = self.sampling_probability;
let t = steps as f64;
let alpha_max = 32.0; let log_moments = self.compute_log_moments(sigma, q, t, alpha_max);
let epsilon = self.compute_epsilon_from_moments(&log_moments, self.target_delta);
let delta = self.target_delta;
Ok((epsilon, delta))
}
fn compute_log_moments(&self, sigma: f64, q: f64, t: f64, alphamax: f64) -> Vec<f64> {
let mut log_moments = Vec::new();
for alpha_int in 2..=(alphamax as usize) {
let alpha = alpha_int as f64;
let log_moment = t
* (q * q * alpha * (alpha - 1.0) / (2.0 * sigma * sigma))
.exp()
.ln();
log_moments.push(log_moment);
}
log_moments
}
fn compute_epsilon_from_moments(&self, logmoments: &[f64], delta: f64) -> f64 {
let mut min_epsilon = f64::INFINITY;
for (i, &log_moment) in logmoments.iter().enumerate() {
let alpha = (i + 2) as f64;
let epsilon = (log_moment - delta.ln()) / (alpha - 1.0);
if epsilon < min_epsilon {
min_epsilon = epsilon;
}
}
min_epsilon.max(0.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::optimizers::SGD;
#[test]
fn test_dp_config_default() {
let config = DifferentialPrivacyConfig::default();
assert_eq!(config.target_epsilon, 1.0);
assert_eq!(config.noise_multiplier, 1.1);
assert!(matches!(config.noise_mechanism, NoiseMechanism::Gaussian));
}
#[test]
fn test_moments_accountant() {
let accountant = MomentsAccountant::new(1.1, 1e-5, 256, 50000);
let (epsilon, delta) = accountant.get_privacy_spent(100).expect("unwrap failed");
assert!(epsilon > 0.0);
assert_eq!(delta, 1e-5);
let (epsilon2, _) = accountant.get_privacy_spent(200).expect("unwrap failed");
assert!(epsilon2 > epsilon); }
#[test]
fn test_dp_optimizer_creation() {
let sgd = SGD::new(0.01);
let dp_config = DifferentialPrivacyConfig::default();
let dp_optimizer = DifferentiallyPrivateOptimizer::<_, f64, scirs2_core::ndarray::Ix1>::new(
sgd, dp_config,
);
assert!(dp_optimizer.is_ok());
}
#[test]
fn test_privacy_budget_tracking() {
let sgd = SGD::new(0.01);
let dp_config = DifferentialPrivacyConfig {
target_epsilon: 1.0,
max_steps: 100,
..Default::default()
};
let dp_optimizer: DifferentiallyPrivateOptimizer<SGD<f64>, f64, scirs2_core::ndarray::Ix1> =
DifferentiallyPrivateOptimizer::new(sgd, dp_config).expect("unwrap failed");
let budget = dp_optimizer.get_privacy_budget();
assert_eq!(budget.epsilon_consumed, 0.0);
assert_eq!(budget.epsilon_remaining, 1.0);
assert_eq!(budget.steps_taken, 0);
}
}