1#![allow(dead_code)]
18
19use anyhow::{anyhow, Result};
20use scirs2_core::random::StdRng; use scirs2_core::random::*; use serde::{Deserialize, Serialize};
23use std::collections::hash_map::DefaultHasher;
24use std::collections::HashMap;
25use std::hash::{Hash, Hasher};
26use trustformers_core::tensor::Tensor;
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct FedAvgConfig {
31 pub local_epochs: usize,
33 pub local_learning_rate: f32,
35 pub client_fraction: f32,
37 pub min_clients: usize,
39 pub max_clients: usize,
41 pub weight_decay: f32,
43}
44
45impl Default for FedAvgConfig {
46 fn default() -> Self {
47 Self {
48 local_epochs: 5,
49 local_learning_rate: 1e-3,
50 client_fraction: 0.1,
51 min_clients: 2,
52 max_clients: 100,
53 weight_decay: 0.0,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct FedProxConfig {
61 pub fedavg_config: FedAvgConfig,
63 pub mu: f32,
65}
66
67impl Default for FedProxConfig {
68 fn default() -> Self {
69 Self {
70 fedavg_config: FedAvgConfig::default(),
71 mu: 0.01,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct DifferentialPrivacyConfig {
79 pub epsilon: f32,
81 pub delta: f32,
83 pub sensitivity: f32,
85 pub noise_mechanism: NoiseMechanism,
87}
88
89impl Default for DifferentialPrivacyConfig {
90 fn default() -> Self {
91 Self {
92 epsilon: 1.0,
93 delta: 1e-5,
94 sensitivity: 1.0,
95 noise_mechanism: NoiseMechanism::Gaussian,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub enum NoiseMechanism {
103 Gaussian,
105 Laplace,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum ClientSelectionStrategy {
112 Random,
114 DataSize,
116 ComputeCapacity,
118 CommunicationQuality,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ClientInfo {
125 pub client_id: String,
127 pub data_size: usize,
129 pub compute_capacity: f32,
131 pub communication_quality: f32,
133 pub available: bool,
135}
136
137#[derive(Debug)]
142pub struct FedAvg {
143 config: FedAvgConfig,
144 global_parameters: Vec<Tensor>,
145 client_weights: HashMap<String, f32>,
146 current_round: usize,
147 selected_clients: Vec<String>,
148 rng: StdRng,
149}
150
151impl FedAvg {
152 pub fn new(config: FedAvgConfig) -> Self {
154 Self {
155 config,
156 global_parameters: Vec::new(),
157 client_weights: HashMap::new(),
158 current_round: 0,
159 selected_clients: Vec::new(),
160 rng: StdRng::seed_from_u64(42),
161 }
162 }
163
164 pub fn initialize_global_parameters(&mut self, parameters: Vec<Tensor>) {
166 self.global_parameters = parameters;
167 }
168
169 pub fn select_clients(
171 &mut self,
172 available_clients: &[ClientInfo],
173 strategy: ClientSelectionStrategy,
174 ) -> Result<Vec<String>> {
175 let available: Vec<&ClientInfo> =
176 available_clients.iter().filter(|c| c.available).collect();
177
178 if available.is_empty() {
179 return Err(anyhow!("No available clients"));
180 }
181
182 let num_clients = (available.len() as f32 * self.config.client_fraction).round() as usize;
183 let num_clients = num_clients
184 .max(self.config.min_clients)
185 .min(self.config.max_clients)
186 .min(available.len());
187
188 let selected = match strategy {
189 ClientSelectionStrategy::Random => {
190 let mut indices: Vec<usize> = (0..available.len()).collect();
191 for i in 0..num_clients {
192 let j = self.rng.random_range(i..indices.len());
193 indices.swap(i, j);
194 }
195 indices[..num_clients].iter().map(|&i| available[i].client_id.clone()).collect()
196 },
197 ClientSelectionStrategy::DataSize => {
198 let mut clients_with_size: Vec<_> =
199 available.iter().map(|c| (c.client_id.clone(), c.data_size)).collect();
200 clients_with_size.sort_by_key(|(_, size)| std::cmp::Reverse(*size));
201 clients_with_size[..num_clients].iter().map(|(id, _)| id.clone()).collect()
202 },
203 ClientSelectionStrategy::ComputeCapacity => {
204 let mut clients_with_capacity: Vec<_> =
205 available.iter().map(|c| (c.client_id.clone(), c.compute_capacity)).collect();
206 clients_with_capacity.sort_by(|(_, a), (_, b)| {
207 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
208 });
209 clients_with_capacity[..num_clients].iter().map(|(id, _)| id.clone()).collect()
210 },
211 ClientSelectionStrategy::CommunicationQuality => {
212 let mut clients_with_quality: Vec<_> = available
213 .iter()
214 .map(|c| (c.client_id.clone(), c.communication_quality))
215 .collect();
216 clients_with_quality.sort_by(|(_, a), (_, b)| {
217 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
218 });
219 clients_with_quality[..num_clients].iter().map(|(id, _)| id.clone()).collect()
220 },
221 };
222
223 self.selected_clients = selected;
224 Ok(self.selected_clients.clone())
225 }
226
227 pub fn aggregate_updates(
229 &mut self,
230 client_updates: HashMap<String, Vec<Tensor>>,
231 ) -> Result<Vec<Tensor>> {
232 if client_updates.is_empty() {
233 return Err(anyhow!("No client updates to aggregate"));
234 }
235
236 let total_weight: f32 = client_updates
237 .keys()
238 .map(|client_id| self.client_weights.get(client_id).unwrap_or(&1.0))
239 .sum();
240
241 if total_weight == 0.0 {
242 return Err(anyhow!("Total client weight is zero"));
243 }
244
245 let param_count = client_updates
247 .values()
248 .next()
249 .ok_or_else(|| anyhow::anyhow!("client_updates must have at least one entry"))?
250 .len();
251 let mut aggregated = Vec::with_capacity(param_count);
252
253 for i in 0..param_count {
254 let first_param = &client_updates
256 .values()
257 .next()
258 .ok_or_else(|| anyhow::anyhow!("client_updates must have at least one entry"))?[i];
259 aggregated.push(Tensor::zeros_like(first_param)?);
260 }
261
262 for (client_id, updates) in &client_updates {
264 let weight = self.client_weights.get(client_id).unwrap_or(&1.0) / total_weight;
265
266 for (i, update) in updates.iter().enumerate() {
267 let weighted_update = update.mul_scalar(weight)?;
268 aggregated[i] = aggregated[i].add(&weighted_update)?;
269 }
270 }
271
272 self.global_parameters = aggregated.clone();
274 self.current_round += 1;
275
276 Ok(aggregated)
277 }
278
279 pub fn set_client_weights(&mut self, weights: HashMap<String, f32>) {
281 self.client_weights = weights;
282 }
283
284 pub fn get_global_parameters(&self) -> &[Tensor] {
286 &self.global_parameters
287 }
288
289 pub fn get_current_round(&self) -> usize {
291 self.current_round
292 }
293}
294
295#[derive(Debug)]
300pub struct FedProx {
301 fedavg: FedAvg,
302 config: FedProxConfig,
303}
304
305impl FedProx {
306 pub fn new(config: FedProxConfig) -> Self {
308 Self {
309 fedavg: FedAvg::new(config.fedavg_config.clone()),
310 config,
311 }
312 }
313
314 pub fn compute_proximal_term(
316 &self,
317 client_params: &[Tensor],
318 global_params: &[Tensor],
319 ) -> Result<f32> {
320 if client_params.len() != global_params.len() {
321 return Err(anyhow!("Parameter count mismatch"));
322 }
323
324 let mut proximal_loss = 0.0;
325 for (client_param, global_param) in client_params.iter().zip(global_params.iter()) {
326 let diff = client_param.sub(global_param)?;
327 let norm_sq = diff.norm_squared()?.to_scalar()?;
328 proximal_loss += norm_sq;
329 }
330
331 Ok(self.config.mu * proximal_loss / 2.0)
332 }
333
334 pub fn apply_proximal_update(
336 &self,
337 client_params: &mut [Tensor],
338 global_params: &[Tensor],
339 learning_rate: f32,
340 ) -> Result<()> {
341 for (client_param, global_param) in client_params.iter_mut().zip(global_params.iter()) {
342 let diff = client_param.sub(global_param)?;
343 let proximal_grad = diff.mul_scalar(self.config.mu)?;
344 let update = proximal_grad.mul_scalar(learning_rate)?;
345 *client_param = client_param.sub(&update)?;
346 }
347 Ok(())
348 }
349
350 pub fn select_clients(
352 &mut self,
353 available_clients: &[ClientInfo],
354 strategy: ClientSelectionStrategy,
355 ) -> Result<Vec<String>> {
356 self.fedavg.select_clients(available_clients, strategy)
357 }
358
359 pub fn aggregate_updates(
360 &mut self,
361 client_updates: HashMap<String, Vec<Tensor>>,
362 ) -> Result<Vec<Tensor>> {
363 self.fedavg.aggregate_updates(client_updates)
364 }
365
366 pub fn get_global_parameters(&self) -> &[Tensor] {
367 self.fedavg.get_global_parameters()
368 }
369
370 pub fn get_current_round(&self) -> usize {
371 self.fedavg.get_current_round()
372 }
373}
374
375pub struct DifferentialPrivacy {
377 config: DifferentialPrivacyConfig,
378 rng: StdRng,
379}
380
381impl DifferentialPrivacy {
382 pub fn new(config: DifferentialPrivacyConfig) -> Self {
384 Self {
385 config,
386 rng: StdRng::seed_from_u64(42),
387 }
388 }
389
390 pub fn add_noise(&mut self, parameters: &mut [Tensor]) -> Result<()> {
392 let noise_scale = self.compute_noise_scale()?;
393
394 for param in parameters.iter_mut() {
395 let noise = self.generate_noise_tensor(param, noise_scale)?;
396 *param = param.add(&noise)?;
397 }
398
399 Ok(())
400 }
401
402 fn compute_noise_scale(&self) -> Result<f32> {
403 match self.config.noise_mechanism {
404 NoiseMechanism::Gaussian => {
405 let ln_term = (1.25 / self.config.delta).ln();
407 let sigma = (2.0 * ln_term).sqrt() * self.config.sensitivity / self.config.epsilon;
408 Ok(sigma)
409 },
410 NoiseMechanism::Laplace => {
411 Ok(self.config.sensitivity / self.config.epsilon)
413 },
414 }
415 }
416
417 fn generate_noise_tensor(&mut self, reference: &Tensor, scale: f32) -> Result<Tensor> {
418 let shape = reference.shape();
419 let mut noise_data = Vec::new();
420
421 match self.config.noise_mechanism {
422 NoiseMechanism::Gaussian => {
423 use scirs2_core::random::{Distribution, Normal}; let normal = Normal::new(0.0, scale)
425 .map_err(|e| anyhow!("Normal distribution error: {}", e))?;
426
427 for _ in 0..shape.iter().product::<usize>() {
428 noise_data.push(normal.sample(&mut self.rng));
429 }
430 },
431 NoiseMechanism::Laplace => {
432 use scirs2_core::random::{Distribution, Exp}; let exp_dist = Exp::new(1.0 / scale)
436 .map_err(|e| anyhow!("Exponential distribution error: {}", e))?;
437
438 for _ in 0..shape.iter().product::<usize>() {
439 let sign = if self.rng.random::<bool>() { 1.0 } else { -1.0 };
440 let exp_sample = exp_dist.sample(&mut self.rng);
441 noise_data.push(sign * exp_sample);
442 }
443 },
444 }
445
446 Ok(Tensor::from_data(noise_data, &shape.to_vec())?)
447 }
448}
449
450pub struct SecureAggregation {
455 threshold: usize,
456 total_clients: usize,
457}
458
459impl SecureAggregation {
460 pub fn new(threshold: usize, total_clients: usize) -> Result<Self> {
462 if threshold > total_clients {
463 return Err(anyhow!("Threshold cannot exceed total clients"));
464 }
465
466 Ok(Self {
467 threshold,
468 total_clients,
469 })
470 }
471
472 fn pairwise_seed(client_a: &str, client_b: &str, round: usize) -> u64 {
485 let (lower, upper) =
486 if client_a <= client_b { (client_a, client_b) } else { (client_b, client_a) };
487 let mut hasher = DefaultHasher::new();
488 lower.hash(&mut hasher);
489 upper.hash(&mut hasher);
490 round.hash(&mut hasher);
491 hasher.finish()
492 }
493
494 pub fn generate_masks(
521 &self,
522 client_id: &str,
523 all_client_ids: &[String],
524 round: usize,
525 parameter_shapes: &[Vec<usize>],
526 ) -> Result<Vec<Tensor>> {
527 if !all_client_ids.iter().any(|id| id == client_id) {
528 return Err(anyhow!(
529 "client_id {client_id} is not present in all_client_ids; this client's masks \
530 would not have matching pairwise partners to cancel against"
531 ));
532 }
533
534 let mut accumulators: Vec<Vec<f32>> = parameter_shapes
535 .iter()
536 .map(|shape| vec![0.0f32; shape.iter().product::<usize>()])
537 .collect();
538
539 for other_id in all_client_ids {
540 if other_id == client_id {
541 continue;
542 }
543 let sign: f32 = if client_id < other_id.as_str() { 1.0 } else { -1.0 };
547 let mut pair_rng =
548 StdRng::seed_from_u64(Self::pairwise_seed(client_id, other_id, round));
549
550 for (accumulator, shape) in accumulators.iter_mut().zip(parameter_shapes.iter()) {
555 let mask_size = shape.iter().product::<usize>();
556 for slot in accumulator.iter_mut().take(mask_size) {
557 let value: f32 = pair_rng.random_range(-1.0..1.0);
558 *slot += sign * value;
559 }
560 }
561 }
562
563 let mut masks = Vec::with_capacity(accumulators.len());
564 for (data, shape) in accumulators.into_iter().zip(parameter_shapes.iter()) {
565 masks.push(Tensor::from_data(data, shape)?);
566 }
567 Ok(masks)
568 }
569
570 pub fn secure_aggregate(
586 &self,
587 masked_updates: HashMap<String, Vec<Tensor>>,
588 ) -> Result<Vec<Tensor>> {
589 if masked_updates.len() < self.threshold {
590 return Err(anyhow!("Not enough clients for secure aggregation"));
591 }
592
593 let mut result = Vec::new();
595 let client_count = masked_updates.len() as f32;
596
597 let parameter_count =
599 masked_updates.values().next().map(|update| update.len()).unwrap_or(0);
600
601 for (client_id, update) in &masked_updates {
602 if update.len() != parameter_count {
603 return Err(anyhow!(
604 "Client {} has {} parameters, expected {}",
605 client_id,
606 update.len(),
607 parameter_count
608 ));
609 }
610 }
611
612 for param_idx in 0..parameter_count {
614 let mut parameter_updates = Vec::new();
616 let mut expected_shape: Option<Vec<usize>> = None;
617
618 for (client_id, update) in &masked_updates {
619 let param_update = &update[param_idx];
620
621 if let Some(ref shape) = expected_shape {
623 if param_update.shape() != *shape {
624 return Err(anyhow!(
625 "Client {} parameter {} has shape {:?}, expected {:?}",
626 client_id,
627 param_idx,
628 param_update.shape(),
629 shape
630 ));
631 }
632 } else {
633 expected_shape = Some(param_update.shape());
634 }
635
636 parameter_updates.push(param_update);
637 }
638
639 let shape = expected_shape
641 .ok_or_else(|| anyhow!("No client updates found for parameter {}", param_idx))?;
642 let mut aggregated_param = Tensor::zeros(&shape)?;
643 for param_update in parameter_updates {
644 aggregated_param = aggregated_param.add(param_update)?;
645 }
646
647 result.push(aggregated_param.div_scalar(client_count)?);
653 }
654
655 Ok(result)
656 }
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
664 fn test_fedavg_config_default() {
665 let config = FedAvgConfig::default();
666 assert_eq!(config.local_epochs, 5);
667 assert_eq!(config.client_fraction, 0.1);
668 assert_eq!(config.min_clients, 2);
669 }
670
671 #[test]
672 fn test_fedprox_config_default() {
673 let config = FedProxConfig::default();
674 assert_eq!(config.mu, 0.01);
675 assert_eq!(config.fedavg_config.local_epochs, 5);
676 }
677
678 #[test]
679 fn test_differential_privacy_config() {
680 let config = DifferentialPrivacyConfig::default();
681 assert_eq!(config.epsilon, 1.0);
682 assert_eq!(config.delta, 1e-5);
683 assert!(matches!(config.noise_mechanism, NoiseMechanism::Gaussian));
684 }
685
686 #[test]
687 fn test_client_selection_strategies() {
688 let clients = vec![
689 ClientInfo {
690 client_id: "client1".to_string(),
691 data_size: 100,
692 compute_capacity: 0.8,
693 communication_quality: 0.9,
694 available: true,
695 },
696 ClientInfo {
697 client_id: "client2".to_string(),
698 data_size: 200,
699 compute_capacity: 0.6,
700 communication_quality: 0.7,
701 available: true,
702 },
703 ];
704
705 let mut fedavg = FedAvg::new(FedAvgConfig::default());
706
707 let selected = fedavg
709 .select_clients(&clients, ClientSelectionStrategy::Random)
710 .expect("Operation failed in test");
711 assert!(!selected.is_empty());
712
713 let selected = fedavg
715 .select_clients(&clients, ClientSelectionStrategy::DataSize)
716 .expect("Operation failed in test");
717 assert!(!selected.is_empty());
718 }
719
720 #[test]
721 fn test_secure_aggregation_creation() {
722 let secure_agg = SecureAggregation::new(3, 5).expect("Construction failed");
723 assert_eq!(secure_agg.threshold, 3);
724 assert_eq!(secure_agg.total_clients, 5);
725
726 assert!(SecureAggregation::new(6, 5).is_err());
728 }
729
730 #[test]
733 fn test_generate_masks_uses_the_callers_shapes_not_hardcoded_ones() {
734 let secure_agg = SecureAggregation::new(2, 2).expect("Construction failed");
735 let all_clients = vec!["alice".to_string(), "bob".to_string()];
736 let shapes = vec![vec![3], vec![2, 2], vec![5, 1, 2]];
738
739 let masks = secure_agg
740 .generate_masks("alice", &all_clients, 0, &shapes)
741 .expect("generate_masks failed");
742
743 assert_eq!(masks.len(), shapes.len());
744 for (mask, expected_shape) in masks.iter().zip(shapes.iter()) {
745 assert_eq!(&mask.shape(), expected_shape);
746 }
747 }
748
749 #[test]
750 fn test_generate_masks_is_deterministic_for_the_same_inputs() {
751 let secure_agg = SecureAggregation::new(2, 3).expect("Construction failed");
752 let all_clients = vec!["alice".to_string(), "bob".to_string(), "carol".to_string()];
753 let shapes = vec![vec![4], vec![3, 2]];
754
755 let first = secure_agg
756 .generate_masks("bob", &all_clients, 7, &shapes)
757 .expect("generate_masks failed");
758 let second = secure_agg
759 .generate_masks("bob", &all_clients, 7, &shapes)
760 .expect("generate_masks failed");
761
762 for (a, b) in first.iter().zip(second.iter()) {
763 assert_eq!(
764 a.to_vec_f32().expect("read"),
765 b.to_vec_f32().expect("read"),
766 "the same client/round/shapes must derive the same masks every time"
767 );
768 }
769 }
770
771 #[test]
772 fn test_generate_masks_rejects_a_client_id_missing_from_all_client_ids() {
773 let secure_agg = SecureAggregation::new(2, 2).expect("Construction failed");
774 let all_clients = vec!["alice".to_string(), "bob".to_string()];
775 let shapes = vec![vec![2]];
776
777 assert!(secure_agg.generate_masks("carol", &all_clients, 0, &shapes).is_err());
778 }
779
780 #[test]
785 fn test_masks_cancel_exactly_between_two_clients() {
786 let secure_agg = SecureAggregation::new(2, 2).expect("Construction failed");
787 let all_clients = vec!["alice".to_string(), "bob".to_string()];
788 let shapes = vec![vec![6], vec![3, 2]];
789
790 let alice_masks = secure_agg
791 .generate_masks("alice", &all_clients, 3, &shapes)
792 .expect("generate_masks failed");
793 let bob_masks = secure_agg
794 .generate_masks("bob", &all_clients, 3, &shapes)
795 .expect("generate_masks failed");
796
797 for (alice_mask, bob_mask) in alice_masks.iter().zip(bob_masks.iter()) {
798 let a = alice_mask.to_vec_f32().expect("read");
799 let b = bob_mask.to_vec_f32().expect("read");
800 assert_eq!(a.len(), b.len());
801 for (av, bv) in a.iter().zip(b.iter()) {
802 assert_eq!(
803 *av, -*bv,
804 "alice's and bob's pairwise mask values must be exact negatives"
805 );
806 }
807 }
808 }
809
810 #[test]
816 fn test_secure_aggregate_of_pairwise_masked_updates_recovers_true_average() {
817 let secure_agg = SecureAggregation::new(2, 4).expect("Construction failed");
818 let client_ids: Vec<String> = ["client-0", "client-1", "client-2", "client-3"]
819 .iter()
820 .map(|s| s.to_string())
821 .collect();
822 let shapes = vec![vec![4], vec![2, 3]];
823 let round = 11;
824
825 let true_updates: HashMap<String, Vec<Tensor>> = client_ids
829 .iter()
830 .enumerate()
831 .map(|(client_index, id)| {
832 let updates = shapes
833 .iter()
834 .map(|shape| {
835 let size = shape.iter().product::<usize>();
836 let data: Vec<f32> =
837 (0..size).map(|i| (client_index * 10 + i) as f32 * 0.1).collect();
838 Tensor::from_data(data, shape).expect("tensor must build in test")
839 })
840 .collect();
841 (id.clone(), updates)
842 })
843 .collect();
844
845 let masked_updates: HashMap<String, Vec<Tensor>> = client_ids
846 .iter()
847 .map(|id| {
848 let masks = secure_agg
849 .generate_masks(id, &client_ids, round, &shapes)
850 .expect("generate_masks failed");
851 let true_update = &true_updates[id];
852 let masked: Vec<Tensor> = true_update
853 .iter()
854 .zip(masks.iter())
855 .map(|(update, mask)| update.add(mask).expect("tensor add failed in test"))
856 .collect();
857 (id.clone(), masked)
858 })
859 .collect();
860
861 let aggregated =
862 secure_agg.secure_aggregate(masked_updates).expect("secure_aggregate failed");
863
864 for (param_idx, shape) in shapes.iter().enumerate() {
865 let size = shape.iter().product::<usize>();
866 let mut expected_sum = vec![0.0f32; size];
867 for (client_index, _) in client_ids.iter().enumerate() {
868 for (slot, value) in expected_sum.iter_mut().enumerate() {
869 *value += (client_index * 10 + slot) as f32 * 0.1;
870 }
871 }
872 let expected_average: Vec<f32> =
873 expected_sum.iter().map(|v| v / client_ids.len() as f32).collect();
874
875 let actual = aggregated[param_idx].to_vec_f32().expect("read");
876 for (actual_value, expected_value) in actual.iter().zip(expected_average.iter()) {
877 assert!(
878 (actual_value - expected_value).abs() < 1e-3,
879 "pairwise masks must cancel to within floating-point rounding: expected \
880 {expected_value}, got {actual_value}"
881 );
882 }
883 }
884 }
885}