Skip to main content

trustformers_optim/
federated.rs

1//! # Federated Learning Optimization
2//!
3//! This module implements algorithms for federated learning, enabling distributed
4//! training across multiple clients while preserving privacy and handling
5//! heterogeneous data distributions.
6//!
7//! ## Available Algorithms
8//!
9//! - **FedAvg**: Standard federated averaging algorithm
10//! - **FedProx**: Federated optimization with proximal regularization
11//! - **Secure Aggregation**: Privacy-preserving parameter aggregation
12//! - **Differential Privacy**: Add noise for enhanced privacy protection
13//! - **Client Selection**: Strategies for selecting participating clients
14
15// reason: research-stage module — reserved API/scaffolding fields and methods
16// retained intentionally for in-progress features; not yet on active call paths.
17#![allow(dead_code)]
18
19use anyhow::{anyhow, Result};
20use scirs2_core::random::StdRng; // Explicit import for type clarity
21use scirs2_core::random::*; // SciRS2 Integration Policy - Replaces rand
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24use trustformers_core::tensor::Tensor;
25
26/// Configuration for federated averaging (FedAvg).
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct FedAvgConfig {
29    /// Number of local epochs per client
30    pub local_epochs: usize,
31    /// Local learning rate for client updates
32    pub local_learning_rate: f32,
33    /// Fraction of clients participating per round
34    pub client_fraction: f32,
35    /// Minimum number of clients required per round
36    pub min_clients: usize,
37    /// Maximum number of clients per round
38    pub max_clients: usize,
39    /// Weight decay for regularization
40    pub weight_decay: f32,
41}
42
43impl Default for FedAvgConfig {
44    fn default() -> Self {
45        Self {
46            local_epochs: 5,
47            local_learning_rate: 1e-3,
48            client_fraction: 0.1,
49            min_clients: 2,
50            max_clients: 100,
51            weight_decay: 0.0,
52        }
53    }
54}
55
56/// Configuration for FedProx algorithm.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct FedProxConfig {
59    /// FedAvg configuration
60    pub fedavg_config: FedAvgConfig,
61    /// Proximal term coefficient (μ)
62    pub mu: f32,
63}
64
65impl Default for FedProxConfig {
66    fn default() -> Self {
67        Self {
68            fedavg_config: FedAvgConfig::default(),
69            mu: 0.01,
70        }
71    }
72}
73
74/// Configuration for differential privacy.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct DifferentialPrivacyConfig {
77    /// Privacy budget (epsilon)
78    pub epsilon: f32,
79    /// Delta parameter for (ε,δ)-differential privacy
80    pub delta: f32,
81    /// Sensitivity of the function (max change in output per unit change in input)
82    pub sensitivity: f32,
83    /// Noise mechanism to use
84    pub noise_mechanism: NoiseMechanism,
85}
86
87impl Default for DifferentialPrivacyConfig {
88    fn default() -> Self {
89        Self {
90            epsilon: 1.0,
91            delta: 1e-5,
92            sensitivity: 1.0,
93            noise_mechanism: NoiseMechanism::Gaussian,
94        }
95    }
96}
97
98/// Types of noise mechanisms for differential privacy.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub enum NoiseMechanism {
101    /// Gaussian noise
102    Gaussian,
103    /// Laplace noise
104    Laplace,
105}
106
107/// Client selection strategies.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub enum ClientSelectionStrategy {
110    /// Random selection
111    Random,
112    /// Selection based on data size
113    DataSize,
114    /// Selection based on computational capacity
115    ComputeCapacity,
116    /// Selection based on communication quality
117    CommunicationQuality,
118}
119
120/// Information about a federated client.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ClientInfo {
123    /// Client identifier
124    pub client_id: String,
125    /// Number of data samples
126    pub data_size: usize,
127    /// Computational capacity (relative metric)
128    pub compute_capacity: f32,
129    /// Communication quality (bandwidth, latency, etc.)
130    pub communication_quality: f32,
131    /// Client availability
132    pub available: bool,
133}
134
135/// Federated Averaging (FedAvg) optimizer.
136///
137/// Implements the standard federated learning algorithm where clients
138/// perform local updates and the server aggregates them via weighted averaging.
139#[derive(Debug)]
140pub struct FedAvg {
141    config: FedAvgConfig,
142    global_parameters: Vec<Tensor>,
143    client_weights: HashMap<String, f32>,
144    current_round: usize,
145    selected_clients: Vec<String>,
146    rng: StdRng,
147}
148
149impl FedAvg {
150    /// Create a new FedAvg optimizer.
151    pub fn new(config: FedAvgConfig) -> Self {
152        Self {
153            config,
154            global_parameters: Vec::new(),
155            client_weights: HashMap::new(),
156            current_round: 0,
157            selected_clients: Vec::new(),
158            rng: StdRng::seed_from_u64(42),
159        }
160    }
161
162    /// Initialize global parameters.
163    pub fn initialize_global_parameters(&mut self, parameters: Vec<Tensor>) {
164        self.global_parameters = parameters;
165    }
166
167    /// Select clients for the current round.
168    pub fn select_clients(
169        &mut self,
170        available_clients: &[ClientInfo],
171        strategy: ClientSelectionStrategy,
172    ) -> Result<Vec<String>> {
173        let available: Vec<&ClientInfo> =
174            available_clients.iter().filter(|c| c.available).collect();
175
176        if available.is_empty() {
177            return Err(anyhow!("No available clients"));
178        }
179
180        let num_clients = (available.len() as f32 * self.config.client_fraction).round() as usize;
181        let num_clients = num_clients
182            .max(self.config.min_clients)
183            .min(self.config.max_clients)
184            .min(available.len());
185
186        let selected = match strategy {
187            ClientSelectionStrategy::Random => {
188                let mut indices: Vec<usize> = (0..available.len()).collect();
189                for i in 0..num_clients {
190                    let j = self.rng.random_range(i..indices.len());
191                    indices.swap(i, j);
192                }
193                indices[..num_clients].iter().map(|&i| available[i].client_id.clone()).collect()
194            },
195            ClientSelectionStrategy::DataSize => {
196                let mut clients_with_size: Vec<_> =
197                    available.iter().map(|c| (c.client_id.clone(), c.data_size)).collect();
198                clients_with_size.sort_by_key(|(_, size)| std::cmp::Reverse(*size));
199                clients_with_size[..num_clients].iter().map(|(id, _)| id.clone()).collect()
200            },
201            ClientSelectionStrategy::ComputeCapacity => {
202                let mut clients_with_capacity: Vec<_> =
203                    available.iter().map(|c| (c.client_id.clone(), c.compute_capacity)).collect();
204                clients_with_capacity.sort_by(|(_, a), (_, b)| {
205                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
206                });
207                clients_with_capacity[..num_clients].iter().map(|(id, _)| id.clone()).collect()
208            },
209            ClientSelectionStrategy::CommunicationQuality => {
210                let mut clients_with_quality: Vec<_> = available
211                    .iter()
212                    .map(|c| (c.client_id.clone(), c.communication_quality))
213                    .collect();
214                clients_with_quality.sort_by(|(_, a), (_, b)| {
215                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
216                });
217                clients_with_quality[..num_clients].iter().map(|(id, _)| id.clone()).collect()
218            },
219        };
220
221        self.selected_clients = selected;
222        Ok(self.selected_clients.clone())
223    }
224
225    /// Aggregate client updates using weighted averaging.
226    pub fn aggregate_updates(
227        &mut self,
228        client_updates: HashMap<String, Vec<Tensor>>,
229    ) -> Result<Vec<Tensor>> {
230        if client_updates.is_empty() {
231            return Err(anyhow!("No client updates to aggregate"));
232        }
233
234        let total_weight: f32 = client_updates
235            .keys()
236            .map(|client_id| self.client_weights.get(client_id).unwrap_or(&1.0))
237            .sum();
238
239        if total_weight == 0.0 {
240            return Err(anyhow!("Total client weight is zero"));
241        }
242
243        // Initialize aggregated parameters with zeros
244        let param_count = client_updates
245            .values()
246            .next()
247            .ok_or_else(|| anyhow::anyhow!("client_updates must have at least one entry"))?
248            .len();
249        let mut aggregated = Vec::with_capacity(param_count);
250
251        for i in 0..param_count {
252            // Get shape from first client's parameter
253            let first_param = &client_updates
254                .values()
255                .next()
256                .ok_or_else(|| anyhow::anyhow!("client_updates must have at least one entry"))?[i];
257            aggregated.push(Tensor::zeros_like(first_param)?);
258        }
259
260        // Weighted aggregation
261        for (client_id, updates) in &client_updates {
262            let weight = self.client_weights.get(client_id).unwrap_or(&1.0) / total_weight;
263
264            for (i, update) in updates.iter().enumerate() {
265                let weighted_update = update.mul_scalar(weight)?;
266                aggregated[i] = aggregated[i].add(&weighted_update)?;
267            }
268        }
269
270        // Update global parameters
271        self.global_parameters = aggregated.clone();
272        self.current_round += 1;
273
274        Ok(aggregated)
275    }
276
277    /// Set client weights for aggregation.
278    pub fn set_client_weights(&mut self, weights: HashMap<String, f32>) {
279        self.client_weights = weights;
280    }
281
282    /// Get current global parameters.
283    pub fn get_global_parameters(&self) -> &[Tensor] {
284        &self.global_parameters
285    }
286
287    /// Get current round number.
288    pub fn get_current_round(&self) -> usize {
289        self.current_round
290    }
291}
292
293/// FedProx optimizer with proximal regularization.
294///
295/// Extends FedAvg with a proximal term to handle client heterogeneity
296/// by adding regularization that keeps client updates close to global model.
297#[derive(Debug)]
298pub struct FedProx {
299    fedavg: FedAvg,
300    config: FedProxConfig,
301}
302
303impl FedProx {
304    /// Create a new FedProx optimizer.
305    pub fn new(config: FedProxConfig) -> Self {
306        Self {
307            fedavg: FedAvg::new(config.fedavg_config.clone()),
308            config,
309        }
310    }
311
312    /// Compute proximal term for client update.
313    pub fn compute_proximal_term(
314        &self,
315        client_params: &[Tensor],
316        global_params: &[Tensor],
317    ) -> Result<f32> {
318        if client_params.len() != global_params.len() {
319            return Err(anyhow!("Parameter count mismatch"));
320        }
321
322        let mut proximal_loss = 0.0;
323        for (client_param, global_param) in client_params.iter().zip(global_params.iter()) {
324            let diff = client_param.sub(global_param)?;
325            let norm_sq = diff.norm_squared()?.to_scalar()?;
326            proximal_loss += norm_sq;
327        }
328
329        Ok(self.config.mu * proximal_loss / 2.0)
330    }
331
332    /// Apply proximal update to client parameters.
333    pub fn apply_proximal_update(
334        &self,
335        client_params: &mut [Tensor],
336        global_params: &[Tensor],
337        learning_rate: f32,
338    ) -> Result<()> {
339        for (client_param, global_param) in client_params.iter_mut().zip(global_params.iter()) {
340            let diff = client_param.sub(global_param)?;
341            let proximal_grad = diff.mul_scalar(self.config.mu)?;
342            let update = proximal_grad.mul_scalar(learning_rate)?;
343            *client_param = client_param.sub(&update)?;
344        }
345        Ok(())
346    }
347
348    /// Delegate to FedAvg for other operations.
349    pub fn select_clients(
350        &mut self,
351        available_clients: &[ClientInfo],
352        strategy: ClientSelectionStrategy,
353    ) -> Result<Vec<String>> {
354        self.fedavg.select_clients(available_clients, strategy)
355    }
356
357    pub fn aggregate_updates(
358        &mut self,
359        client_updates: HashMap<String, Vec<Tensor>>,
360    ) -> Result<Vec<Tensor>> {
361        self.fedavg.aggregate_updates(client_updates)
362    }
363
364    pub fn get_global_parameters(&self) -> &[Tensor] {
365        self.fedavg.get_global_parameters()
366    }
367
368    pub fn get_current_round(&self) -> usize {
369        self.fedavg.get_current_round()
370    }
371}
372
373/// Differential privacy mechanism for federated learning.
374pub struct DifferentialPrivacy {
375    config: DifferentialPrivacyConfig,
376    rng: StdRng,
377}
378
379impl DifferentialPrivacy {
380    /// Create a new differential privacy mechanism.
381    pub fn new(config: DifferentialPrivacyConfig) -> Self {
382        Self {
383            config,
384            rng: StdRng::seed_from_u64(42),
385        }
386    }
387
388    /// Add noise to parameters for differential privacy.
389    pub fn add_noise(&mut self, parameters: &mut [Tensor]) -> Result<()> {
390        let noise_scale = self.compute_noise_scale()?;
391
392        for param in parameters.iter_mut() {
393            let noise = self.generate_noise_tensor(param, noise_scale)?;
394            *param = param.add(&noise)?;
395        }
396
397        Ok(())
398    }
399
400    fn compute_noise_scale(&self) -> Result<f32> {
401        match self.config.noise_mechanism {
402            NoiseMechanism::Gaussian => {
403                // For Gaussian mechanism: σ = sqrt(2 * ln(1.25/δ)) * Δf / ε
404                let ln_term = (1.25 / self.config.delta).ln();
405                let sigma = (2.0 * ln_term).sqrt() * self.config.sensitivity / self.config.epsilon;
406                Ok(sigma)
407            },
408            NoiseMechanism::Laplace => {
409                // For Laplace mechanism: b = Δf / ε
410                Ok(self.config.sensitivity / self.config.epsilon)
411            },
412        }
413    }
414
415    fn generate_noise_tensor(&mut self, reference: &Tensor, scale: f32) -> Result<Tensor> {
416        let shape = reference.shape();
417        let mut noise_data = Vec::new();
418
419        match self.config.noise_mechanism {
420            NoiseMechanism::Gaussian => {
421                use scirs2_core::random::{Distribution, Normal}; // SciRS2 Integration Policy
422                let normal = Normal::new(0.0, scale)
423                    .map_err(|e| anyhow!("Normal distribution error: {}", e))?;
424
425                for _ in 0..shape.iter().product::<usize>() {
426                    noise_data.push(normal.sample(&mut self.rng));
427                }
428            },
429            NoiseMechanism::Laplace => {
430                // Use exponential distribution to simulate Laplace
431                // Laplace(0, b) can be simulated as: sign * Exponential(1/b)
432                use scirs2_core::random::{Distribution, Exp}; // SciRS2 Integration Policy
433                let exp_dist = Exp::new(1.0 / scale)
434                    .map_err(|e| anyhow!("Exponential distribution error: {}", e))?;
435
436                for _ in 0..shape.iter().product::<usize>() {
437                    let sign = if self.rng.random::<bool>() { 1.0 } else { -1.0 };
438                    let exp_sample = exp_dist.sample(&mut self.rng);
439                    noise_data.push(sign * exp_sample);
440                }
441            },
442        }
443
444        Ok(Tensor::from_data(noise_data, &shape.to_vec())?)
445    }
446}
447
448/// Secure aggregation for federated learning.
449///
450/// Implements privacy-preserving aggregation where the server cannot
451/// see individual client updates, only the aggregated result.
452pub struct SecureAggregation {
453    threshold: usize,
454    total_clients: usize,
455}
456
457impl SecureAggregation {
458    /// Create a new secure aggregation instance.
459    pub fn new(threshold: usize, total_clients: usize) -> Result<Self> {
460        if threshold > total_clients {
461            return Err(anyhow!("Threshold cannot exceed total clients"));
462        }
463
464        Ok(Self {
465            threshold,
466            total_clients,
467        })
468    }
469
470    /// Generate random masks for secure aggregation.
471    /// In practice, this would use cryptographic protocols.
472    pub fn generate_masks(&self, client_id: &str, round: usize) -> Result<Vec<Tensor>> {
473        // This is a simplified implementation
474        // Real secure aggregation uses secret sharing and cryptographic techniques
475        let mut rng = StdRng::from_seed({
476            let mut seed = [0u8; 32];
477            let client_hash = format!("{}-{}", client_id, round);
478            let bytes = client_hash.as_bytes();
479            for (i, &byte) in bytes.iter().enumerate().take(32) {
480                seed[i] = byte;
481            }
482            seed
483        });
484
485        // Generate cryptographic masks for secure aggregation
486        // Each mask is a random tensor that will be used to blind the client's update
487        let mut masks = Vec::new();
488
489        // Generate masks based on client's expected parameter shapes
490        // In practice, these shapes would be communicated during federated setup
491        let parameter_shapes = vec![
492            vec![100, 50], // Example: First layer weights
493            vec![50],      // Example: First layer bias
494            vec![50, 20],  // Example: Second layer weights
495            vec![20],      // Example: Second layer bias
496        ];
497
498        for shape in parameter_shapes {
499            // Generate random mask with same shape as parameter
500            let mask_size = shape.iter().product::<usize>();
501            let mut mask_data: Vec<f32> = Vec::with_capacity(mask_size);
502
503            for _ in 0..mask_size {
504                // Generate random float in range [-1.0, 1.0] for better numerical stability
505                mask_data.push(rng.random_range(-1.0..1.0));
506            }
507
508            let mask = Tensor::from_data(mask_data, &shape)?;
509            masks.push(mask);
510        }
511
512        Ok(masks)
513    }
514
515    /// Aggregate masked updates securely.
516    pub fn secure_aggregate(
517        &self,
518        masked_updates: HashMap<String, Vec<Tensor>>,
519    ) -> Result<Vec<Tensor>> {
520        if masked_updates.len() < self.threshold {
521            return Err(anyhow!("Not enough clients for secure aggregation"));
522        }
523
524        // In a real implementation, this would:
525        // 1. Collect masked updates from clients
526        // 2. Aggregate the masks
527        // 3. Remove the aggregate mask to reveal the sum
528        // 4. Compute the average
529
530        // Enhanced secure aggregation with validation and error handling
531        let mut result = Vec::new();
532        let client_count = masked_updates.len() as f32;
533
534        // Validate that all clients have the same number of parameters
535        let parameter_count =
536            masked_updates.values().next().map(|update| update.len()).unwrap_or(0);
537
538        for (client_id, update) in &masked_updates {
539            if update.len() != parameter_count {
540                return Err(anyhow!(
541                    "Client {} has {} parameters, expected {}",
542                    client_id,
543                    update.len(),
544                    parameter_count
545                ));
546            }
547        }
548
549        // Aggregate masked updates parameter by parameter
550        for param_idx in 0..parameter_count {
551            // Collect all client updates for this parameter
552            let mut parameter_updates = Vec::new();
553            let mut expected_shape: Option<Vec<usize>> = None;
554
555            for (client_id, update) in &masked_updates {
556                let param_update = &update[param_idx];
557
558                // Validate tensor shapes are consistent across clients
559                if let Some(ref shape) = expected_shape {
560                    if param_update.shape() != *shape {
561                        return Err(anyhow!(
562                            "Client {} parameter {} has shape {:?}, expected {:?}",
563                            client_id,
564                            param_idx,
565                            param_update.shape(),
566                            shape
567                        ));
568                    }
569                } else {
570                    expected_shape = Some(param_update.shape());
571                }
572
573                parameter_updates.push(param_update);
574            }
575
576            // Sum all client updates for this parameter
577            let shape = expected_shape
578                .ok_or_else(|| anyhow!("No client updates found for parameter {}", param_idx))?;
579            let mut aggregated_param = Tensor::zeros(&shape)?;
580            for param_update in parameter_updates {
581                aggregated_param = aggregated_param.add(param_update)?;
582            }
583
584            // Average the aggregated parameter
585            // In secure aggregation, masks cancel out during summation
586            // so we get the true average without revealing individual updates
587            result.push(aggregated_param.div_scalar(client_count)?);
588        }
589
590        Ok(result)
591    }
592}
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597
598    #[test]
599    fn test_fedavg_config_default() {
600        let config = FedAvgConfig::default();
601        assert_eq!(config.local_epochs, 5);
602        assert_eq!(config.client_fraction, 0.1);
603        assert_eq!(config.min_clients, 2);
604    }
605
606    #[test]
607    fn test_fedprox_config_default() {
608        let config = FedProxConfig::default();
609        assert_eq!(config.mu, 0.01);
610        assert_eq!(config.fedavg_config.local_epochs, 5);
611    }
612
613    #[test]
614    fn test_differential_privacy_config() {
615        let config = DifferentialPrivacyConfig::default();
616        assert_eq!(config.epsilon, 1.0);
617        assert_eq!(config.delta, 1e-5);
618        assert!(matches!(config.noise_mechanism, NoiseMechanism::Gaussian));
619    }
620
621    #[test]
622    fn test_client_selection_strategies() {
623        let clients = vec![
624            ClientInfo {
625                client_id: "client1".to_string(),
626                data_size: 100,
627                compute_capacity: 0.8,
628                communication_quality: 0.9,
629                available: true,
630            },
631            ClientInfo {
632                client_id: "client2".to_string(),
633                data_size: 200,
634                compute_capacity: 0.6,
635                communication_quality: 0.7,
636                available: true,
637            },
638        ];
639
640        let mut fedavg = FedAvg::new(FedAvgConfig::default());
641
642        // Test random selection
643        let selected = fedavg
644            .select_clients(&clients, ClientSelectionStrategy::Random)
645            .expect("Operation failed in test");
646        assert!(!selected.is_empty());
647
648        // Test data size selection
649        let selected = fedavg
650            .select_clients(&clients, ClientSelectionStrategy::DataSize)
651            .expect("Operation failed in test");
652        assert!(!selected.is_empty());
653    }
654
655    #[test]
656    fn test_secure_aggregation_creation() {
657        let secure_agg = SecureAggregation::new(3, 5).expect("Construction failed");
658        assert_eq!(secure_agg.threshold, 3);
659        assert_eq!(secure_agg.total_clients, 5);
660
661        // Should fail if threshold > total clients
662        assert!(SecureAggregation::new(6, 5).is_err());
663    }
664}