1use scirs2_core::ndarray::{Array1, Array2, Array3};
7use scirs2_core::Complex64;
8use std::collections::HashMap;
9use std::f64::consts::PI;
10
11use crate::error::{MLError, Result};
12use crate::qnn::QuantumNeuralNetwork;
13use crate::utils::VariationalCircuit;
14use quantrs2_circuit::prelude::*;
15use quantrs2_core::gate::{multi::*, single::*, GateOp};
16
17#[derive(Debug)]
19pub struct QuantumFLClient {
20 client_id: String,
22 local_model: QuantumNeuralNetwork,
24 dataset_size: usize,
26 epsilon: f64,
28 noise_scale: f64,
30 local_params: HashMap<String, f64>,
32}
33
34impl QuantumFLClient {
35 pub fn new(
37 client_id: String,
38 model_config: &[(String, usize)], dataset_size: usize,
40 epsilon: f64,
41 ) -> Result<Self> {
42 let layers = model_config
44 .iter()
45 .map(|(layer_type, size)| match layer_type.as_str() {
46 "encoding" => crate::qnn::QNNLayerType::EncodingLayer {
47 num_features: *size,
48 },
49 "variational" => crate::qnn::QNNLayerType::VariationalLayer { num_params: *size },
50 "entanglement" => crate::qnn::QNNLayerType::EntanglementLayer {
51 connectivity: "full".to_string(),
52 },
53 _ => crate::qnn::QNNLayerType::MeasurementLayer {
54 measurement_basis: "computational".to_string(),
55 },
56 })
57 .collect();
58
59 let local_model = QuantumNeuralNetwork::new(layers, 4, 10, 2)?;
60 let noise_scale = (2.0 * (1.25 / epsilon).ln()).sqrt() / dataset_size as f64;
61
62 let local_params = local_model
66 .parameters
67 .iter()
68 .enumerate()
69 .map(|(i, &v)| (Self::param_key(i), v))
70 .collect();
71
72 Ok(Self {
73 client_id,
74 local_model,
75 dataset_size,
76 epsilon,
77 noise_scale,
78 local_params,
79 })
80 }
81
82 fn param_key(index: usize) -> String {
85 format!("param_{index}")
86 }
87
88 fn sync_local_params_from_model(&mut self) {
91 for (i, &v) in self.local_model.parameters.iter().enumerate() {
92 self.local_params.insert(Self::param_key(i), v);
93 }
94 }
95
96 fn sync_model_from_local_params(&mut self) {
100 let num_params = self.local_model.parameters.len();
101 for i in 0..num_params {
102 if let Some(&v) = self.local_params.get(&Self::param_key(i)) {
103 self.local_model.parameters[i] = v;
104 }
105 }
106 }
107
108 pub fn train_local(
110 &mut self,
111 local_data: &Array2<f64>,
112 local_labels: &Array1<i32>,
113 epochs: usize,
114 ) -> Result<f64> {
115 let mut total_loss = 0.0;
116
117 for _ in 0..epochs {
118 for i in 0..local_data.nrows() {
120 let input = local_data.row(i).to_owned();
121 let label = local_labels[i];
122
123 let output = self.local_model.forward(&input)?;
125
126 let loss = self.compute_loss(&output, label)?;
128 total_loss += loss;
129
130 self.update_parameters(&input, label, 0.01, &output)?;
133 }
134 }
135
136 self.add_dp_noise()?;
138
139 Ok(total_loss / (epochs * local_data.nrows()) as f64)
140 }
141
142 fn class_probabilities(output: &Array1<f64>) -> Array1<f64> {
150 let max_output = output.iter().copied().fold(f64::NEG_INFINITY, f64::max);
151 let mut probabilities = output.mapv(|value| (value - max_output).exp());
152 let total: f64 = probabilities.sum();
153 if total > 0.0 {
154 probabilities /= total;
155 }
156 probabilities
157 }
158
159 fn compute_loss(&self, output: &Array1<f64>, label: i32) -> Result<f64> {
161 let label_idx = label as usize;
163 if label_idx >= output.len() {
164 return Err(MLError::InvalidInput("Label out of bounds".to_string()));
165 }
166
167 let probabilities = Self::class_probabilities(output);
168 Ok(-probabilities[label_idx].ln())
169 }
170
171 fn update_parameters(
181 &mut self,
182 input: &Array1<f64>,
183 label: i32,
184 learning_rate: f64,
185 output: &Array1<f64>,
186 ) -> Result<()> {
187 let label_idx = label as usize;
188 if label_idx >= output.len() {
189 return Err(MLError::InvalidInput("Label out of bounds".to_string()));
190 }
191
192 let probabilities = Self::class_probabilities(output);
196 let num_params = self.local_model.parameters.len();
197 let mut gradient = vec![0.0; num_params];
198
199 for class in 0..output.len() {
200 let d_loss_d_output = probabilities[class] - if class == label_idx { 1.0 } else { 0.0 };
201 if d_loss_d_output == 0.0 {
202 continue;
203 }
204 let d_output_d_params = self.local_model.output_component_gradient(input, class)?;
206 for j in 0..num_params {
207 gradient[j] += d_loss_d_output * d_output_d_params[j];
208 }
209 }
210
211 for j in 0..num_params {
212 self.local_model.parameters[j] -= learning_rate * gradient[j];
213 }
214
215 self.sync_local_params_from_model();
216 Ok(())
217 }
218
219 fn add_dp_noise(&mut self) -> Result<()> {
223 for (_, value) in self.local_params.iter_mut() {
224 let noise = self.noise_scale * Self::gaussian_noise();
226 *value += noise;
227 }
228 self.sync_model_from_local_params();
229 Ok(())
230 }
231
232 fn gaussian_noise() -> f64 {
234 let u1 = fastrand::f64();
236 let u2 = fastrand::f64();
237 (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
238 }
239
240 pub fn get_parameters(&self) -> HashMap<String, f64> {
242 self.local_params.clone()
243 }
244
245 pub fn set_parameters(&mut self, params: HashMap<String, f64>) {
247 self.local_params = params;
248 self.sync_model_from_local_params();
249 }
250}
251
252#[derive(Debug)]
254pub struct QuantumFLServer {
255 model_config: Vec<(String, usize)>,
257 global_params: HashMap<String, f64>,
259 client_weights: HashMap<String, f64>,
261 aggregation_protocol: SecureAggregationProtocol,
263 byzantine_threshold: f64,
265}
266
267#[derive(Debug, Clone)]
268pub enum SecureAggregationProtocol {
269 FederatedAveraging,
271 SecureMultiparty,
273 HomomorphicEncryption,
275 QuantumSecretSharing,
277}
278
279impl QuantumFLServer {
280 pub fn new(
282 model_config: Vec<(String, usize)>,
283 aggregation_protocol: SecureAggregationProtocol,
284 byzantine_threshold: f64,
285 ) -> Self {
286 Self {
287 model_config,
288 global_params: HashMap::new(),
289 client_weights: HashMap::new(),
290 aggregation_protocol,
291 byzantine_threshold,
292 }
293 }
294
295 pub fn aggregate_updates(
297 &mut self,
298 client_updates: Vec<(String, HashMap<String, f64>, usize)>, ) -> Result<HashMap<String, f64>> {
300 match self.aggregation_protocol {
301 SecureAggregationProtocol::FederatedAveraging => {
302 self.federated_averaging(client_updates)
303 }
304 SecureAggregationProtocol::SecureMultiparty => {
305 self.secure_multiparty_aggregation(client_updates)
306 }
307 SecureAggregationProtocol::HomomorphicEncryption => {
308 self.homomorphic_aggregation(client_updates)
309 }
310 SecureAggregationProtocol::QuantumSecretSharing => {
311 self.quantum_secret_sharing_aggregation(client_updates)
312 }
313 }
314 }
315
316 fn federated_averaging(
318 &mut self,
319 client_updates: Vec<(String, HashMap<String, f64>, usize)>,
320 ) -> Result<HashMap<String, f64>> {
321 let total_samples: usize = client_updates.iter().map(|(_, _, size)| size).sum();
322 let mut aggregated = HashMap::new();
323
324 for (client_id, params, dataset_size) in client_updates {
326 let weight = dataset_size as f64 / total_samples as f64;
327 self.client_weights.insert(client_id.clone(), weight);
328
329 for (param_name, param_value) in params {
330 *aggregated.entry(param_name).or_insert(0.0) += weight * param_value;
331 }
332 }
333
334 self.global_params = aggregated.clone();
335 Ok(aggregated)
336 }
337
338 fn secure_multiparty_aggregation(
340 &mut self,
341 client_updates: Vec<(String, HashMap<String, f64>, usize)>,
342 ) -> Result<HashMap<String, f64>> {
343 let num_clients = client_updates.len();
345 let mut shares: HashMap<String, Vec<f64>> = HashMap::new();
346
347 for (_, params, _) in &client_updates {
349 for (param_name, param_value) in params {
350 shares
351 .entry(param_name.clone())
352 .or_insert(Vec::new())
353 .push(*param_value);
354 }
355 }
356
357 let mut aggregated = HashMap::new();
359 for (param_name, param_shares) in shares {
360 let aggregated_value = self.byzantine_robust_aggregation(¶m_shares)?;
361 aggregated.insert(param_name, aggregated_value);
362 }
363
364 self.global_params = aggregated.clone();
365 Ok(aggregated)
366 }
367
368 fn homomorphic_aggregation(
370 &mut self,
371 client_updates: Vec<(String, HashMap<String, f64>, usize)>,
372 ) -> Result<HashMap<String, f64>> {
373 let mut encrypted_sum = HashMap::new();
377
378 for (_, params, _) in &client_updates {
379 for (param_name, param_value) in params {
380 let encrypted = self.homomorphic_encrypt(*param_value)?;
382
383 *encrypted_sum.entry(param_name.clone()).or_insert(0.0) += encrypted;
385 }
386 }
387
388 let mut aggregated = HashMap::new();
390 for (param_name, encrypted_value) in encrypted_sum {
391 let decrypted = self.homomorphic_decrypt(encrypted_value)?;
392 aggregated.insert(param_name, decrypted / client_updates.len() as f64);
393 }
394
395 self.global_params = aggregated.clone();
396 Ok(aggregated)
397 }
398
399 fn quantum_secret_sharing_aggregation(
401 &mut self,
402 client_updates: Vec<(String, HashMap<String, f64>, usize)>,
403 ) -> Result<HashMap<String, f64>> {
404 let num_clients = client_updates.len();
405 let threshold = ((num_clients as f64) * self.byzantine_threshold).ceil() as usize;
406
407 let mut quantum_shares: HashMap<String, Vec<QuantumShare>> = HashMap::new();
409
410 for (client_id, params, _) in &client_updates {
411 for (param_name, param_value) in params {
412 let share = self.create_quantum_share(client_id, *param_value)?;
413 quantum_shares
414 .entry(param_name.clone())
415 .or_insert(Vec::new())
416 .push(share);
417 }
418 }
419
420 let mut aggregated = HashMap::new();
422 for (param_name, shares) in quantum_shares {
423 if shares.len() >= threshold {
424 let reconstructed = self.reconstruct_from_quantum_shares(&shares)?;
425 aggregated.insert(param_name, reconstructed);
426 }
427 }
428
429 self.global_params = aggregated.clone();
430 Ok(aggregated)
431 }
432
433 fn byzantine_robust_aggregation(&self, values: &[f64]) -> Result<f64> {
435 if values.is_empty() {
436 return Err(MLError::InvalidInput("No values to aggregate".to_string()));
437 }
438
439 let n = values.len();
441 let f = ((n as f64 * self.byzantine_threshold) as usize).min(n / 2);
442
443 let mut scores = vec![0.0; n];
445 for i in 0..n {
446 let mut distances: Vec<f64> = (0..n)
447 .filter(|&j| j != i)
448 .map(|j| (values[i] - values[j]).abs())
449 .collect();
450 distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
451
452 scores[i] = distances.iter().take(n - f - 1).sum();
454 }
455
456 let best_idx = scores
458 .iter()
459 .enumerate()
460 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
461 .map(|(idx, _)| idx)
462 .unwrap_or(0);
463
464 Ok(values[best_idx])
465 }
466
467 fn homomorphic_encrypt(&self, value: f64) -> Result<f64> {
469 Ok(value * 1000.0 + fastrand::f64() * 10.0)
471 }
472
473 fn homomorphic_decrypt(&self, encrypted: f64) -> Result<f64> {
475 Ok((encrypted - 5.0) / 1000.0)
477 }
478
479 fn create_quantum_share(&self, client_id: &str, value: f64) -> Result<QuantumShare> {
481 let num_qubits = 3;
482 let mut circuit = VariationalCircuit::new(num_qubits);
483
484 circuit.add_gate("RY", vec![0], vec![(value * PI).to_string()]);
486
487 circuit.add_gate("H", vec![1], vec![]);
489 circuit.add_gate("CNOT", vec![1, 2], vec![]);
490 circuit.add_gate("CNOT", vec![0, 1], vec![]);
491
492 Ok(QuantumShare {
493 client_id: client_id.to_string(),
494 share_circuit: circuit,
495 share_value: value,
496 })
497 }
498
499 fn reconstruct_from_quantum_shares(&self, shares: &[QuantumShare]) -> Result<f64> {
501 let sum: f64 = shares.iter().map(|s| s.share_value).sum();
504 Ok(sum / shares.len() as f64)
505 }
506}
507
508#[derive(Debug)]
510struct QuantumShare {
511 client_id: String,
512 share_circuit: VariationalCircuit,
513 share_value: f64,
514}
515
516#[derive(Debug)]
518pub struct DistributedQuantumLearning {
519 server: QuantumFLServer,
521 clients: HashMap<String, QuantumFLClient>,
523 rounds: usize,
525 convergence_threshold: f64,
527}
528
529impl DistributedQuantumLearning {
530 pub fn new(
532 num_clients: usize,
533 model_config: Vec<(String, usize)>,
534 aggregation_protocol: SecureAggregationProtocol,
535 epsilon: f64,
536 ) -> Result<Self> {
537 let server = QuantumFLServer::new(
538 model_config.clone(),
539 aggregation_protocol,
540 0.2, );
542
543 let mut clients = HashMap::new();
544 for i in 0..num_clients {
545 let client_id = format!("client_{}", i);
546 let dataset_size = 100 + fastrand::usize(..900); let client =
548 QuantumFLClient::new(client_id.clone(), &model_config, dataset_size, epsilon)?;
549 clients.insert(client_id, client);
550 }
551
552 Ok(Self {
553 server,
554 clients,
555 rounds: 0,
556 convergence_threshold: 1e-4,
557 })
558 }
559
560 pub fn train(
562 &mut self,
563 data_distribution: &HashMap<String, (Array2<f64>, Array1<i32>)>,
564 num_rounds: usize,
565 clients_per_round: usize,
566 ) -> Result<FederatedTrainingResult> {
567 let mut round_losses = Vec::new();
568 let mut convergence_metric = f64::INFINITY;
569
570 for round in 0..num_rounds {
571 self.rounds = round + 1;
572
573 let selected_clients = self.select_clients(clients_per_round);
575
576 let mut client_updates = Vec::new();
578 let mut round_loss = 0.0;
579
580 for client_id in selected_clients {
581 if let Some(client) = self.clients.get_mut(&client_id) {
582 if let Some((data, labels)) = data_distribution.get(&client_id) {
583 let loss = client.train_local(data, labels, 5)?;
585 round_loss += loss;
586
587 let params = client.get_parameters();
589 let dataset_size = data.nrows();
590 client_updates.push((client_id.clone(), params, dataset_size));
591 }
592 }
593 }
594
595 let aggregated = self.server.aggregate_updates(client_updates)?;
597
598 for (_, client) in self.clients.iter_mut() {
600 client.set_parameters(aggregated.clone());
601 }
602
603 if round > 0 {
605 let prev_params = self.server.global_params.clone();
606 convergence_metric = self.compute_convergence(&prev_params, &aggregated)?;
607
608 if convergence_metric < self.convergence_threshold {
609 round_losses.push(round_loss / clients_per_round as f64);
610 break;
611 }
612 }
613
614 round_losses.push(round_loss / clients_per_round as f64);
615
616 self.server.global_params = aggregated.clone();
618 }
619
620 Ok(FederatedTrainingResult {
621 final_model_params: self.server.global_params.clone(),
622 round_losses,
623 num_rounds: self.rounds,
624 converged: convergence_metric < self.convergence_threshold,
625 convergence_metric,
626 })
627 }
628
629 fn select_clients(&self, num_clients: usize) -> Vec<String> {
631 let all_clients: Vec<String> = self.clients.keys().cloned().collect();
632 let mut selected = Vec::new();
633
634 while selected.len() < num_clients.min(all_clients.len()) {
635 let idx = fastrand::usize(..all_clients.len());
636 let client = all_clients[idx].clone();
637 if !selected.contains(&client) {
638 selected.push(client);
639 }
640 }
641
642 selected
643 }
644
645 fn compute_convergence(
647 &self,
648 old_params: &HashMap<String, f64>,
649 new_params: &HashMap<String, f64>,
650 ) -> Result<f64> {
651 let mut diff_sum = 0.0;
652 let mut count = 0;
653
654 for (key, new_val) in new_params {
655 if let Some(old_val) = old_params.get(key) {
656 diff_sum += (new_val - old_val).abs();
657 count += 1;
658 }
659 }
660
661 Ok(if count > 0 {
662 diff_sum / count as f64
663 } else {
664 0.0
665 })
666 }
667}
668
669#[derive(Debug)]
671pub struct FederatedTrainingResult {
672 pub final_model_params: HashMap<String, f64>,
674 pub round_losses: Vec<f64>,
676 pub num_rounds: usize,
678 pub converged: bool,
680 pub convergence_metric: f64,
682}
683
684pub mod privacy {
686 use super::*;
687
688 #[derive(Debug)]
690 pub struct QuantumDifferentialPrivacy {
691 epsilon: f64,
693 sensitivity: f64,
695 mechanism: NoiseType,
697 }
698
699 #[derive(Debug, Clone)]
700 pub enum NoiseType {
701 Laplace,
702 Gaussian,
703 Quantum,
704 }
705
706 impl QuantumDifferentialPrivacy {
707 pub fn new(epsilon: f64, sensitivity: f64, mechanism: NoiseType) -> Self {
709 Self {
710 epsilon,
711 sensitivity,
712 mechanism,
713 }
714 }
715
716 pub fn add_noise(&self, params: &mut HashMap<String, f64>) -> Result<()> {
718 for (_, value) in params.iter_mut() {
719 let noise = match self.mechanism {
720 NoiseType::Laplace => self.laplace_noise(),
721 NoiseType::Gaussian => self.gaussian_noise(),
722 NoiseType::Quantum => self.quantum_noise()?,
723 };
724 *value += noise;
725 }
726 Ok(())
727 }
728
729 fn laplace_noise(&self) -> f64 {
731 let scale = self.sensitivity / self.epsilon;
732 let u = fastrand::f64() - 0.5;
733 -scale * u.signum() * (1.0 - 2.0 * u.abs()).ln()
734 }
735
736 fn gaussian_noise(&self) -> f64 {
738 let scale = self.sensitivity * (2.0 * (1.25 / self.epsilon).ln()).sqrt();
739 QuantumFLClient::gaussian_noise() * scale
740 }
741
742 fn quantum_noise(&self) -> Result<f64> {
744 let p = (-self.epsilon).exp();
746 Ok(if fastrand::f64() < p {
747 fastrand::f64() * 2.0 - 1.0
748 } else {
749 0.0
750 })
751 }
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use scirs2_core::ndarray::array;
759
760 #[test]
761 fn test_quantum_fl_client_real_parameter_update() {
762 let config = vec![
767 ("encoding".to_string(), 4),
768 ("variational".to_string(), 8),
769 ("measurement".to_string(), 0),
770 ];
771
772 let mut client = QuantumFLClient::new("client_1".to_string(), &config, 100, 1.0)
773 .expect("Failed to create client");
774
775 let initial_params = client.get_parameters();
778 assert_eq!(initial_params.len(), 8);
779
780 let initial_model_params = client.local_model.parameters.clone();
781
782 let data = array![[0.1, 0.2, 0.3, 0.4], [0.5, 0.6, 0.7, 0.8]];
783 let labels = array![0, 1];
784
785 client
786 .train_local(&data, &labels, 1)
787 .expect("Training failed");
788
789 let moved = client
792 .local_model
793 .parameters
794 .iter()
795 .zip(initial_model_params.iter())
796 .any(|(&after, &before)| (after - before).abs() > 1e-9);
797 assert!(
798 moved,
799 "local_model.parameters did not change after train_local()"
800 );
801
802 let updated_params = client.get_parameters();
806 assert_eq!(updated_params.len(), 8);
807 let params_changed = (0..8).any(|i| {
808 let key = format!("param_{i}");
809 (updated_params[&key] - initial_params[&key]).abs() > 1e-9
810 });
811 assert!(
812 params_changed,
813 "get_parameters() did not reflect the real gradient update"
814 );
815
816 for (i, &model_val) in client.local_model.parameters.iter().enumerate() {
818 let key = format!("param_{i}");
819 assert!((updated_params[&key] - model_val).abs() < 1e-12);
820 }
821 }
822
823 #[test]
824 fn test_quantum_fl_client() {
825 let config = vec![
826 ("encoding".to_string(), 4),
827 ("variational".to_string(), 8),
828 ("measurement".to_string(), 0),
829 ];
830
831 let mut client = QuantumFLClient::new("client_1".to_string(), &config, 100, 1.0)
832 .expect("Failed to create client");
833
834 let data = array![[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]];
835 let labels = array![0, 1, 0];
836
837 let loss = client
838 .train_local(&data, &labels, 1)
839 .expect("Training failed");
840 assert!(loss >= 0.0);
841 }
842
843 #[test]
844 fn test_federated_averaging() {
845 let config = vec![("encoding".to_string(), 4)];
846 let mut server =
847 QuantumFLServer::new(config, SecureAggregationProtocol::FederatedAveraging, 0.2);
848
849 let mut params1 = HashMap::new();
850 params1.insert("w1".to_string(), 0.5);
851 params1.insert("w2".to_string(), 0.3);
852
853 let mut params2 = HashMap::new();
854 params2.insert("w1".to_string(), 0.7);
855 params2.insert("w2".to_string(), 0.4);
856
857 let updates = vec![
858 ("client1".to_string(), params1, 100),
859 ("client2".to_string(), params2, 200),
860 ];
861
862 let aggregated = server
863 .aggregate_updates(updates)
864 .expect("Aggregation failed");
865
866 assert!((aggregated["w1"] - 0.633).abs() < 0.01);
868 }
869
870 #[test]
871 fn test_byzantine_robust_aggregation() {
872 let server = QuantumFLServer::new(vec![], SecureAggregationProtocol::SecureMultiparty, 0.3);
873
874 let values = vec![0.5, 0.52, 0.48, 0.51, 10.0]; let robust_value = server
877 .byzantine_robust_aggregation(&values)
878 .expect("Byzantine aggregation failed");
879
880 assert!(robust_value < 1.0);
882 }
883
884 #[test]
885 fn test_differential_privacy() {
886 use privacy::*;
887
888 let dp = QuantumDifferentialPrivacy::new(1.0, 0.1, NoiseType::Gaussian);
889
890 let mut params = HashMap::new();
891 params.insert("param1".to_string(), 0.5);
892 params.insert("param2".to_string(), 0.3);
893
894 let original = params.clone();
895 dp.add_noise(&mut params).expect("Failed to add noise");
896
897 assert_ne!(params["param1"], original["param1"]);
899 assert_ne!(params["param2"], original["param2"]);
900 }
901
902 #[test]
903 fn test_distributed_learning() {
904 let config = vec![("encoding".to_string(), 4), ("variational".to_string(), 8)];
905
906 let mut system = DistributedQuantumLearning::new(
907 3, config,
909 SecureAggregationProtocol::FederatedAveraging,
910 1.0,
911 )
912 .expect("Failed to create distributed learning system");
913
914 let mut data_dist = HashMap::new();
916 for i in 0..3 {
917 let data = Array2::zeros((10, 4));
918 let labels = Array1::zeros(10);
919 data_dist.insert(format!("client_{}", i), (data, labels));
920 }
921
922 let result = system.train(&data_dist, 2, 2).expect("Training failed");
923
924 assert_eq!(result.num_rounds, 2);
925 assert_eq!(result.round_losses.len(), 2);
926 }
927}