1use super::config::*;
21use crate::error::Result;
22use scirs2_core::ndarray::Array1;
23use scirs2_core::numeric::Float;
24use std::collections::{HashMap, VecDeque};
25use std::fmt::Debug;
26
27pub use super::super::federated::byzantine_aggregation::{
39 AdaptivePrivacyAllocation, ByzantineRobustAggregator, OutlierDetectionResult, RobustEstimators,
40 StatisticalAnalyzer, TestStatistic,
41};
42
43pub use super::super::federated::cross_device_manager::{
51 CrossDevicePrivacyManager, DeviceProfile, DeviceType, TemporalEvent, TemporalEventType,
52};
53
54pub struct PersonalizationManager<T: Float + Debug + Send + Sync + 'static> {
58 config: PersonalizationConfig,
59 client_models: HashMap<String, PersonalizedModel<T>>,
60 global_model: Option<Array1<T>>,
61 meta_learner: FederatedMetaLearner<T>,
62}
63
64pub struct AdaptiveBudgetManager<T: Float + Debug + Send + Sync + 'static> {
66 config: AdaptiveBudgetConfig,
67 client_budgets: HashMap<String, AdaptiveBudget>,
68 fairness_monitor: FairnessMonitor,
69 _phantom: std::marker::PhantomData<T>,
70}
71
72pub struct ContinualLearningCoordinator<T: Float + Debug + Send + Sync + 'static> {
74 config: ContinualLearningConfig,
75 task_detector: TaskDetector<T>,
76 task_history: VecDeque<TaskInfo>,
77}
78
79#[derive(Debug, Clone)]
83pub struct PersonalizedModel<T: Float + Debug + Send + Sync + 'static> {
84 pub model_parameters: Array1<T>,
85 pub personal_layers: HashMap<usize, Array1<T>>,
86 pub adaptation_state: AdaptationState<T>,
87 pub performance_history: Vec<f64>,
88 pub last_update_round: usize,
89}
90
91#[derive(Debug, Clone)]
93pub struct AdaptationState<T: Float + Debug + Send + Sync + 'static> {
94 pub learning_rate: f64,
95 pub momentum: Array1<T>,
96 pub adaptation_count: usize,
97 pub gradient_history: VecDeque<Array1<T>>,
98}
99
100pub struct FederatedMetaLearner<T: Float + Debug + Send + Sync + 'static> {
102 pub(super) meta_parameters: Array1<T>,
103 pub(super) client_adaptations: HashMap<String, Array1<T>>,
104 pub(super) meta_gradient_buffer: Array1<T>,
105 pub(super) task_distributions: HashMap<String, TaskDistribution<T>>,
106}
107
108#[derive(Debug, Clone)]
110pub struct TaskDistribution<T: Float + Debug + Send + Sync + 'static> {
111 pub support_gradient: Array1<T>,
112 pub query_gradient: Array1<T>,
113 pub task_similarity: f64,
114 pub adaptation_steps: usize,
115}
116
117#[derive(Debug, Clone)]
119pub struct AdaptiveBudget {
120 pub current_epsilon: f64,
121 pub current_delta: f64,
122 pub allocated_epsilon: f64,
123 pub allocated_delta: f64,
124 pub consumption_rate: f64,
125 pub importance_weight: f64,
126 pub context_factors: HashMap<String, f64>,
127}
128
129pub struct FairnessMonitor {
131 fairness_metrics: FairnessMetrics,
132 client_fairness_scores: HashMap<String, f64>,
133}
134
135#[derive(Debug, Clone)]
137pub struct FairnessMetrics {
138 pub demographic_parity: f64,
139 pub equalized_opportunity: f64,
140 pub individual_fairness: f64,
141 pub group_fairness: f64,
142}
143
144pub struct TaskDetector<T: Float + Debug + Send + Sync + 'static> {
146 pub(super) detection_method: TaskDetectionMethod,
147 pub(super) gradient_buffer: VecDeque<Array1<T>>,
148 pub(super) change_points: Vec<ChangePoint>,
149 pub(super) detection_threshold: f64,
150}
151
152#[derive(Debug, Clone)]
154pub struct ChangePoint {
155 pub round: usize,
156 pub confidence: f64,
157 pub change_magnitude: f64,
158}
159
160#[derive(Debug, Clone)]
162pub struct TaskInfo {
163 pub task_id: usize,
164 pub start_round: usize,
165 pub end_round: Option<usize>,
166 pub task_description: String,
167 pub performance_metrics: HashMap<String, f64>,
168}
169
170pub use super::super::federated::secure_aggregation::SecureAggregator;
180
181pub struct PrivacyAmplificationAnalyzer {
183 pub(super) config: AmplificationConfig,
184 pub(super) subsampling_history: VecDeque<SubsamplingEvent>,
185 pub(super) amplification_factors: HashMap<String, f64>,
186}
187
188pub struct FederatedCompositionAnalyzer {
190 pub(super) method: FederatedCompositionMethod,
191 pub(super) round_compositions: Vec<RoundComposition>,
192 pub(super) client_compositions: HashMap<String, Vec<ClientComposition>>,
193}
194
195#[derive(Debug, Clone)]
197pub struct ParticipationRound {
198 pub round: usize,
199 pub participating_clients: Vec<String>,
200 pub sampling_probability: f64,
201 pub privacy_cost: PrivacyCost,
202 pub aggregation_noise: f64,
203}
204
205#[derive(Debug, Clone)]
207pub struct PrivacyCost {
208 pub epsilon: f64,
209 pub delta: f64,
210 pub client_contribution: f64,
211 pub amplification_factor: f64,
212 pub composition_cost: f64,
213}
214
215#[derive(Debug, Clone)]
217pub struct SubsamplingEvent {
218 pub round: usize,
219 pub sampling_rate: f64,
220 pub clients_sampled: usize,
221 pub total_clients: usize,
222 pub amplification_factor: f64,
223}
224
225#[derive(Debug, Clone)]
227pub struct RoundComposition {
228 pub round: usize,
230 pub participating_clients: usize,
232 pub total_clients: usize,
237 pub epsilon_consumed: f64,
239 pub delta_consumed: f64,
241 pub amplification_applied: bool,
243 pub composition_method: FederatedCompositionMethod,
245 pub noise_multiplier: Option<f64>,
249}
250
251#[derive(Debug, Clone)]
253pub struct ClientComposition {
254 pub client_id: String,
255 pub round: usize,
256 pub local_epsilon: f64,
257 pub local_delta: f64,
258 pub contribution_weight: f64,
259}
260
261impl FairnessMonitor {
264 pub fn new() -> Self {
266 Self {
267 fairness_metrics: FairnessMetrics {
268 demographic_parity: 0.0,
269 equalized_opportunity: 0.0,
270 individual_fairness: 0.0,
271 group_fairness: 0.0,
272 },
273 client_fairness_scores: HashMap::new(),
274 }
275 }
276
277 pub fn set_client_score(&mut self, client_id: String, score: f64) -> Result<()> {
283 if !score.is_finite() || score < 0.0 {
284 return Err(crate::error::OptimError::InvalidParameter(format!(
285 "a fairness score must be non-negative and finite, got {score}"
286 )));
287 }
288 self.client_fairness_scores.insert(client_id, score);
289 Ok(())
290 }
291
292 pub fn get_metrics(&self) -> &FairnessMetrics {
294 &self.fairness_metrics
295 }
296
297 pub fn compute_fairness_weights(&self, client_ids: &[String]) -> HashMap<String, f64> {
299 let mut weights = HashMap::new();
300 for client_id in client_ids {
301 let weight = self
303 .client_fairness_scores
304 .get(client_id)
305 .copied()
306 .unwrap_or(1.0);
307 weights.insert(client_id.clone(), weight);
308 }
309 weights
310 }
311}
312
313impl Default for FairnessMonitor {
314 fn default() -> Self {
315 Self::new()
316 }
317}
318
319impl<
320 T: Float
321 + Debug
322 + Send
323 + Sync
324 + 'static
325 + Default
326 + Clone
327 + scirs2_core::ndarray::ScalarOperand,
328 > FederatedMetaLearner<T>
329{
330 pub fn new(parameter_size: usize) -> Self {
341 Self {
342 meta_parameters: Array1::zeros(parameter_size),
343 client_adaptations: HashMap::new(),
344 meta_gradient_buffer: Array1::zeros(parameter_size),
345 task_distributions: HashMap::new(),
346 }
347 }
348
349 pub fn parameter_size(&self) -> usize {
351 self.meta_parameters.len()
352 }
353
354 pub fn meta_parameters(&self) -> &Array1<T> {
356 &self.meta_parameters
357 }
358
359 pub fn meta_gradient_buffer(&self) -> &Array1<T> {
361 &self.meta_gradient_buffer
362 }
363
364 pub fn client_adaptation(&self, client_id: &str) -> Option<&Array1<T>> {
366 self.client_adaptations.get(client_id)
367 }
368
369 pub fn task_distribution(&self, client_id: &str) -> Option<&TaskDistribution<T>> {
371 self.task_distributions.get(client_id)
372 }
373}
374
375impl<T: Float + Debug + Send + Sync + 'static> TaskDetector<T> {
376 pub fn new() -> Self {
378 Self {
379 detection_method: TaskDetectionMethod::GradientBased,
380 gradient_buffer: VecDeque::with_capacity(100),
381 change_points: Vec::new(),
382 detection_threshold: 0.1,
383 }
384 }
385
386 pub fn detection_threshold(&self) -> f64 {
388 self.detection_threshold
389 }
390
391 pub fn set_detection_threshold(&mut self, threshold: f64) -> Result<()> {
393 if !threshold.is_finite() || threshold <= 0.0 {
394 return Err(crate::error::OptimError::InvalidParameter(format!(
395 "the task-detection threshold must be positive and finite, got {threshold}"
396 )));
397 }
398 self.detection_threshold = threshold;
399 Ok(())
400 }
401
402 pub fn detection_method(&self) -> TaskDetectionMethod {
404 self.detection_method
405 }
406
407 pub fn change_points(&self) -> &[ChangePoint] {
409 &self.change_points
410 }
411}
412
413impl<T: Float + Debug + Send + Sync + 'static> Default for TaskDetector<T> {
414 fn default() -> Self {
415 Self::new()
416 }
417}
418
419impl<
422 T: Float
423 + Debug
424 + Send
425 + Sync
426 + 'static
427 + Default
428 + Clone
429 + scirs2_core::ndarray::ScalarOperand,
430 > PersonalizationManager<T>
431{
432 pub fn new() -> Result<Self> {
438 Self::with_config(
439 PersonalizationConfig {
440 strategy: PersonalizationStrategy::None,
441 local_adaptation: LocalAdaptationConfig::default(),
442 clustering: ClusteringConfig::default(),
443 meta_learning: MetaLearningConfig::default(),
444 privacy_preserving: false,
445 },
446 0,
447 )
448 }
449
450 pub fn with_config(config: PersonalizationConfig, parameter_size: usize) -> Result<Self> {
453 Ok(Self {
454 config,
455 client_models: HashMap::new(),
456 global_model: None,
457 meta_learner: FederatedMetaLearner::new(parameter_size),
458 })
459 }
460
461 pub fn config(&self) -> &PersonalizationConfig {
463 &self.config
464 }
465
466 pub fn meta_learner(&self) -> &FederatedMetaLearner<T> {
468 &self.meta_learner
469 }
470
471 pub fn meta_learner_mut(&mut self) -> &mut FederatedMetaLearner<T> {
473 &mut self.meta_learner
474 }
475
476 pub fn update_global_model(&mut self, aggregated_update: &Array1<T>) -> Result<Array1<T>> {
498 if aggregated_update.is_empty() {
499 return Err(crate::error::OptimError::InvalidParameter(
500 "the aggregated update is empty; there is nothing to apply".to_string(),
501 ));
502 }
503 match self.global_model.as_mut() {
504 Some(model) => {
505 if model.len() != aggregated_update.len() {
506 return Err(crate::error::OptimError::DimensionMismatch(format!(
507 "the aggregated update has {} coordinates but the global model has {}",
508 aggregated_update.len(),
509 model.len()
510 )));
511 }
512 for (slot, &delta) in model.iter_mut().zip(aggregated_update.iter()) {
513 *slot = *slot + delta;
514 }
515 Ok(model.clone())
516 }
517 None => {
518 self.global_model = Some(aggregated_update.clone());
519 Ok(aggregated_update.clone())
520 }
521 }
522 }
523
524 pub fn global_model(&self) -> Option<&Array1<T>> {
527 self.global_model.as_ref()
528 }
529
530 pub fn set_client_model(&mut self, client_id: String, model: PersonalizedModel<T>) {
535 self.client_models.insert(client_id, model);
536 }
537
538 pub fn client_model(&self, client_id: &str) -> Option<&PersonalizedModel<T>> {
540 self.client_models.get(client_id)
541 }
542}
543
544impl<T: Float + Debug + Send + Sync + 'static> AdaptiveBudgetManager<T> {
545 pub fn new() -> Result<Self> {
550 Self::with_config(AdaptiveBudgetConfig::default())
551 }
552
553 pub fn with_config(config: AdaptiveBudgetConfig) -> Result<Self> {
555 Ok(Self {
556 config,
557 client_budgets: HashMap::new(),
558 fairness_monitor: FairnessMonitor::new(),
559 _phantom: std::marker::PhantomData,
560 })
561 }
562
563 pub fn config(&self) -> &AdaptiveBudgetConfig {
565 &self.config
566 }
567
568 pub fn fairness_monitor(&self) -> &FairnessMonitor {
570 &self.fairness_monitor
571 }
572
573 pub fn client_budget(&self, client_id: &str) -> Option<&AdaptiveBudget> {
575 self.client_budgets.get(client_id)
576 }
577}
578
579impl<T: Float + Debug + Send + Sync + 'static + Default> ContinualLearningCoordinator<T> {
580 pub fn new() -> Result<Self> {
585 Self::with_config(ContinualLearningConfig {
586 strategy: ContinualLearningStrategy::TaskAgnostic,
587 memory_management: MemoryManagementConfig::default(),
588 task_detection: TaskDetectionConfig::default(),
589 knowledge_transfer: KnowledgeTransferConfig::default(),
590 forgetting_prevention: ForgettingPreventionConfig::default(),
591 })
592 }
593
594 pub fn with_config(config: ContinualLearningConfig) -> Result<Self> {
600 let mut task_detector = TaskDetector::new();
601 task_detector.detection_method = config.task_detection.detection_method;
602 task_detector.set_detection_threshold(config.task_detection.sensitivity_threshold)?;
603 Ok(Self {
604 config,
605 task_detector,
606 task_history: VecDeque::new(),
607 })
608 }
609
610 pub fn config(&self) -> &ContinualLearningConfig {
612 &self.config
613 }
614
615 pub fn task_detector(&self) -> &TaskDetector<T> {
617 &self.task_detector
618 }
619
620 pub fn task_detector_mut(&mut self) -> &mut TaskDetector<T> {
622 &mut self.task_detector
623 }
624
625 pub fn task_history(&self) -> impl Iterator<Item = &TaskInfo> {
627 self.task_history.iter()
628 }
629}
630
631impl PrivacyAmplificationAnalyzer {
632 pub fn new(config: AmplificationConfig) -> Self {
639 Self {
640 config,
641 subsampling_history: VecDeque::new(),
642 amplification_factors: HashMap::new(),
643 }
644 }
645}
646
647impl FederatedCompositionAnalyzer {
648 pub fn new(method: FederatedCompositionMethod) -> Self {
650 Self {
651 method,
652 round_compositions: Vec::new(),
653 client_compositions: HashMap::new(),
654 }
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use crate::privacy::federated_privacy::config::{
662 AdaptiveBudgetConfig, ByzantineRobustConfig, ByzantineRobustMethod, ClusteringConfig,
663 ContinualLearningConfig, ContinualLearningStrategy, CrossDeviceConfig,
664 ForgettingPreventionConfig, KnowledgeTransferConfig, LocalAdaptationConfig,
665 MemoryManagementConfig, MetaLearningConfig, PersonalizationConfig, PersonalizationStrategy,
666 ReputationSystemConfig, SecureAggregationConfig, StatisticalTestConfig,
667 TaskDetectionConfig, TaskDetectionMethod,
668 };
669
670 fn byzantine_config(trim_ratio: f64, byzantine_ratio: f64) -> ByzantineRobustConfig {
671 ByzantineRobustConfig {
672 method: ByzantineRobustMethod::TrimmedMean { trim_ratio },
673 expected_byzantine_ratio: byzantine_ratio,
674 dynamic_detection: true,
675 reputation_system: ReputationSystemConfig::default(),
676 statistical_tests: StatisticalTestConfig::default(),
677 }
678 }
679
680 #[test]
681 fn the_byzantine_aggregator_takes_its_configuration() {
682 let aggregator =
686 match ByzantineRobustAggregator::<f64>::with_config(byzantine_config(0.35, 0.4)) {
687 Ok(aggregator) => aggregator,
688 Err(err) => panic!("construction failed: {err}"),
689 };
690 assert_eq!(aggregator.config().expected_byzantine_ratio, 0.4);
691 match aggregator.config().method {
692 ByzantineRobustMethod::TrimmedMean { trim_ratio } => {
693 assert!((trim_ratio - 0.35).abs() < 1e-12)
694 }
695 other => panic!("unexpected method {other:?}"),
696 }
697 assert!((aggregator.statistical_analyzer().significance_level() - 0.05).abs() < 1e-12);
698 }
699
700 #[test]
701 fn an_unusable_byzantine_configuration_is_refused() {
702 for (trim, byzantine) in [
706 (1.0f64, 0.2f64),
707 (1.5, 0.2),
708 (-0.1, 0.2),
709 (0.2, 0.5),
710 (0.2, 1.0),
711 ] {
712 assert!(
713 ByzantineRobustAggregator::<f64>::with_config(byzantine_config(trim, byzantine))
714 .is_err(),
715 "trim={trim}, byzantine={byzantine} must be refused"
716 );
717 }
718 let mut config = byzantine_config(0.2, 0.2);
719 config.statistical_tests.significancelevel = 1.0;
720 assert!(ByzantineRobustAggregator::<f64>::with_config(config).is_err());
721 }
722
723 #[test]
728 fn the_secure_aggregator_is_the_audited_implementation() {
729 let config = SecureAggregationConfig {
730 min_clients: 25,
731 max_dropouts: 4,
732 ..SecureAggregationConfig::default()
733 };
734 let aggregator = match SecureAggregator::<f64>::new(config) {
735 Ok(aggregator) => aggregator,
736 Err(err) => panic!("construction failed: {err}"),
737 };
738 assert_eq!(aggregator.aggregation_threshold(), 25);
739 assert_eq!(aggregator.config().min_clients, 25);
740 assert_eq!(aggregator.rounds_prepared(), 0);
743 assert!(aggregator.current_plan().is_none());
744 assert!(aggregator.modulus() > 0);
745
746 for min_clients in [0usize, 1] {
747 let config = SecureAggregationConfig {
748 min_clients,
749 max_dropouts: 0,
750 ..SecureAggregationConfig::default()
751 };
752 assert!(
753 SecureAggregator::<f64>::new(config).is_err(),
754 "min_clients={min_clients} must be refused"
755 );
756 }
757 }
758
759 #[test]
760 fn the_personalization_manager_takes_its_strategy_and_size() {
761 let config = PersonalizationConfig {
762 strategy: PersonalizationStrategy::MetaLearning {
763 inner_lr: 0.01,
764 outer_lr: 0.001,
765 },
766 local_adaptation: LocalAdaptationConfig::default(),
767 clustering: ClusteringConfig::default(),
768 meta_learning: MetaLearningConfig::default(),
769 privacy_preserving: true,
770 };
771 let manager = match PersonalizationManager::<f64>::with_config(config, 128) {
772 Ok(manager) => manager,
773 Err(err) => panic!("construction failed: {err}"),
774 };
775 assert!(matches!(
776 manager.config().strategy,
777 PersonalizationStrategy::MetaLearning { .. }
778 ));
779 assert!(manager.config().privacy_preserving);
780 assert_eq!(
781 manager.meta_learner().parameter_size(),
782 128,
783 "the meta-learner must be sized for the model, not left at 0"
784 );
785
786 let default_manager = match PersonalizationManager::<f64>::new() {
788 Ok(manager) => manager,
789 Err(err) => panic!("construction failed: {err}"),
790 };
791 assert!(matches!(
792 default_manager.config().strategy,
793 PersonalizationStrategy::None
794 ));
795 }
796
797 #[test]
798 fn the_continual_learning_coordinator_configures_its_detector() {
799 let config = ContinualLearningConfig {
800 strategy: ContinualLearningStrategy::EWC { lambda: 0.5 },
801 memory_management: MemoryManagementConfig::default(),
802 task_detection: TaskDetectionConfig {
803 enabled: true,
804 detection_method: TaskDetectionMethod::GradientBased,
805 sensitivity_threshold: 0.42,
806 adaptation_delay: 3,
807 },
808 knowledge_transfer: KnowledgeTransferConfig::default(),
809 forgetting_prevention: ForgettingPreventionConfig::default(),
810 };
811 let coordinator = match ContinualLearningCoordinator::<f64>::with_config(config) {
812 Ok(coordinator) => coordinator,
813 Err(err) => panic!("construction failed: {err}"),
814 };
815 assert!(matches!(
816 coordinator.config().strategy,
817 ContinualLearningStrategy::EWC { .. }
818 ));
819 assert!(
820 (coordinator.task_detector().detection_threshold() - 0.42).abs() < 1e-12,
821 "the configured sensitivity must reach the detector, got {}",
822 coordinator.task_detector().detection_threshold()
823 );
824 assert!(coordinator.task_history().next().is_none());
825 }
826
827 #[test]
828 fn an_unusable_task_detection_threshold_is_refused() {
829 let config = ContinualLearningConfig {
830 strategy: ContinualLearningStrategy::TaskAgnostic,
831 memory_management: MemoryManagementConfig::default(),
832 task_detection: TaskDetectionConfig {
833 enabled: true,
834 detection_method: TaskDetectionMethod::GradientBased,
835 sensitivity_threshold: 0.0,
836 adaptation_delay: 1,
837 },
838 knowledge_transfer: KnowledgeTransferConfig::default(),
839 forgetting_prevention: ForgettingPreventionConfig::default(),
840 };
841 assert!(ContinualLearningCoordinator::<f64>::with_config(config).is_err());
842 }
843
844 #[test]
845 fn the_adaptive_budget_manager_takes_its_configuration() {
846 let config = AdaptiveBudgetConfig {
847 enabled: true,
848 ..AdaptiveBudgetConfig::default()
849 };
850 let manager = match AdaptiveBudgetManager::<f64>::with_config(config) {
851 Ok(manager) => manager,
852 Err(err) => panic!("construction failed: {err}"),
853 };
854 assert!(
855 manager.config().enabled,
856 "the configured `enabled` flag must reach the manager"
857 );
858 assert!(manager.client_budget("nobody").is_none());
859
860 let default_manager = match AdaptiveBudgetManager::<f64>::new() {
861 Ok(manager) => manager,
862 Err(err) => panic!("construction failed: {err}"),
863 };
864 assert!(!default_manager.config().enabled);
865 }
866
867 #[test]
868 fn the_cross_device_manager_exposes_its_configuration() {
869 let manager = CrossDevicePrivacyManager::<f64>::new(CrossDeviceConfig::default());
870 assert!(!manager.config().user_level_privacy);
871 assert!(manager.get_user_cluster("nobody").is_none());
872 assert_eq!(manager.device_count(), 0);
873 }
874
875 #[test]
876 fn fairness_weights_default_to_one_for_unknown_clients() {
877 let monitor = FairnessMonitor::new();
878 let weights = monitor.compute_fairness_weights(&["a".to_string(), "b".to_string()]);
879 assert_eq!(weights.len(), 2);
880 assert!(weights.values().all(|weight| (*weight - 1.0).abs() < 1e-12));
881 assert_eq!(monitor.get_metrics().demographic_parity, 0.0);
882 }
883}