1use scirs2_core::ndarray::{Array1, Array2, Array3};
7use scirs2_core::random::prelude::*;
8use scirs2_core::Complex64;
9use std::collections::HashMap;
10use std::f64::consts::PI;
11
12use crate::autodiff::DifferentiableParam;
13use crate::error::{MLError, Result};
14use crate::utils::VariationalCircuit;
15use quantrs2_circuit::prelude::*;
16use quantrs2_core::gate::{multi::*, single::*, GateOp};
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum ActivationType {
21 Linear,
23 ReLU,
25 Sigmoid,
27 Tanh,
29}
30
31#[derive(Debug, Clone)]
33pub struct QuantumGraph {
34 num_nodes: usize,
36 adjacency: Array2<f64>,
38 node_features: Array2<f64>,
40 edge_features: Option<HashMap<(usize, usize), Array1<f64>>>,
42 graph_features: Option<Array1<f64>>,
44}
45
46impl QuantumGraph {
47 pub fn new(num_nodes: usize, edges: Vec<(usize, usize)>, node_features: Array2<f64>) -> Self {
49 let mut adjacency = Array2::zeros((num_nodes, num_nodes));
50
51 for (src, dst) in edges {
53 adjacency[[src, dst]] = 1.0;
54 adjacency[[dst, src]] = 1.0; }
56
57 Self {
58 num_nodes,
59 adjacency,
60 node_features,
61 edge_features: None,
62 graph_features: None,
63 }
64 }
65
66 pub fn with_edge_features(
68 mut self,
69 edge_features: HashMap<(usize, usize), Array1<f64>>,
70 ) -> Self {
71 self.edge_features = Some(edge_features);
72 self
73 }
74
75 pub fn with_graph_features(mut self, graph_features: Array1<f64>) -> Self {
77 self.graph_features = Some(graph_features);
78 self
79 }
80
81 pub fn degree(&self, node: usize) -> usize {
83 self.adjacency
84 .row(node)
85 .iter()
86 .filter(|&&x| x > 0.0)
87 .count()
88 }
89
90 pub fn neighbors(&self, node: usize) -> Vec<usize> {
92 self.adjacency
93 .row(node)
94 .iter()
95 .enumerate()
96 .filter(|(_, &val)| val > 0.0)
97 .map(|(idx, _)| idx)
98 .collect()
99 }
100
101 pub fn laplacian(&self) -> Array2<f64> {
103 let mut degree_matrix = Array2::zeros((self.num_nodes, self.num_nodes));
104 for i in 0..self.num_nodes {
105 degree_matrix[[i, i]] = self.degree(i) as f64;
106 }
107 °ree_matrix - &self.adjacency
108 }
109
110 pub fn normalized_laplacian(&self) -> Array2<f64> {
112 let mut degree_matrix = Array2::zeros((self.num_nodes, self.num_nodes));
113 let mut degree_sqrt_inv = Array1::zeros(self.num_nodes);
114
115 for i in 0..self.num_nodes {
116 let degree = self.degree(i) as f64;
117 degree_matrix[[i, i]] = degree;
118 if degree > 0.0 {
119 degree_sqrt_inv[i] = 1.0 / degree.sqrt();
120 }
121 }
122
123 let mut norm_laplacian = Array2::eye(self.num_nodes);
124 for i in 0..self.num_nodes {
125 for j in 0..self.num_nodes {
126 if self.adjacency[[i, j]] > 0.0 {
127 norm_laplacian[[i, j]] -=
128 degree_sqrt_inv[i] * self.adjacency[[i, j]] * degree_sqrt_inv[j];
129 }
130 }
131 }
132
133 norm_laplacian
134 }
135}
136
137#[derive(Debug)]
139pub struct QuantumGCNLayer {
140 input_dim: usize,
142 output_dim: usize,
144 num_qubits: usize,
146 node_circuit: VariationalCircuit,
148 aggregation_circuit: VariationalCircuit,
150 parameters: HashMap<String, f64>,
152 activation: ActivationType,
154}
155
156impl QuantumGCNLayer {
157 pub fn new(input_dim: usize, output_dim: usize, activation: ActivationType) -> Self {
159 let num_qubits = ((input_dim.max(output_dim)) as f64).log2().ceil() as usize;
160 let node_circuit = Self::build_node_circuit(num_qubits);
161 let aggregation_circuit = Self::build_aggregation_circuit(num_qubits);
162
163 let mut parameters = HashMap::new();
168 let mut rng = thread_rng();
169 for (_, _, param_names) in node_circuit
170 .gates
171 .iter()
172 .chain(aggregation_circuit.gates.iter())
173 {
174 for name in param_names {
175 parameters
176 .entry(name.clone())
177 .or_insert_with(|| (rng.random::<f64>() - 0.5) * 2.0 * PI);
178 }
179 }
180
181 Self {
182 input_dim,
183 output_dim,
184 num_qubits,
185 node_circuit,
186 aggregation_circuit,
187 parameters,
188 activation,
189 }
190 }
191
192 fn build_node_circuit(num_qubits: usize) -> VariationalCircuit {
194 let mut circuit = VariationalCircuit::new(num_qubits);
195
196 for q in 0..num_qubits {
198 circuit.add_gate("RY", vec![q], vec![format!("node_encode_{}", q)]);
199 }
200
201 for layer in 0..2 {
203 for q in 0..num_qubits - 1 {
204 circuit.add_gate("CNOT", vec![q, q + 1], vec![]);
205 }
206 if num_qubits > 2 {
207 circuit.add_gate("CNOT", vec![num_qubits - 1, 0], vec![]);
208 }
209
210 for q in 0..num_qubits {
212 circuit.add_gate("RX", vec![q], vec![format!("node_rx_{}_{}", layer, q)]);
213 circuit.add_gate("RZ", vec![q], vec![format!("node_rz_{}_{}", layer, q)]);
214 }
215 }
216
217 circuit
218 }
219
220 fn build_aggregation_circuit(num_qubits: usize) -> VariationalCircuit {
222 let mut circuit = VariationalCircuit::new(num_qubits * 2); for q in 0..num_qubits {
226 circuit.add_gate("CZ", vec![q, q + num_qubits], vec![]);
227 }
228
229 for q in 0..num_qubits * 2 {
231 circuit.add_gate("RY", vec![q], vec![format!("agg_ry_{}", q)]);
232 }
233
234 for q in 0..num_qubits * 2 - 1 {
236 circuit.add_gate("CNOT", vec![q, q + 1], vec![]);
237 }
238
239 for q in 0..num_qubits {
241 circuit.add_gate("RX", vec![q], vec![format!("agg_final_{}", q)]);
242 }
243
244 circuit
245 }
246
247 pub fn forward(&self, graph: &QuantumGraph) -> Result<Array2<f64>> {
249 let mut output_features = Array2::zeros((graph.num_nodes, self.output_dim));
250
251 for node in 0..graph.num_nodes {
253 let node_feat = graph.node_features.row(node);
255
256 let neighbors = graph.neighbors(node);
258 let mut aggregated = Array1::zeros(self.input_dim);
259
260 for &neighbor in &neighbors {
262 let neighbor_feat = graph.node_features.row(neighbor);
263 aggregated = &aggregated + &neighbor_feat.to_owned();
264 }
265
266 let degree = neighbors.len().max(1) as f64;
268 aggregated = aggregated / degree;
269
270 let transformed = self.quantum_transform(&node_feat.to_owned(), &aggregated)?;
272
273 for i in 0..self.output_dim {
275 output_features[[node, i]] = transformed[i];
276 }
277 }
278
279 Ok(output_features)
280 }
281
282 fn quantum_transform(
294 &self,
295 node_features: &Array1<f64>,
296 aggregated_features: &Array1<f64>,
297 ) -> Result<Array1<f64>> {
298 let node_encoded = self.encode_features(node_features)?;
300 let agg_encoded = self.encode_features(aggregated_features)?;
301
302 let joint_state = kronecker_product(&node_encoded, &agg_encoded);
305 let evolved_state = self.simulate_circuit(&self.aggregation_circuit, joint_state)?;
306
307 let quantum_signal: Vec<f64> = (0..self.num_qubits.max(1))
311 .map(|q| expectation_z(&evolved_state, q))
312 .collect();
313
314 let mut output = Array1::zeros(self.output_dim);
315 for i in 0..self.output_dim {
316 let idx_node = i % node_features.len();
317 let idx_agg = i % aggregated_features.len();
318 let idx_quantum = i % quantum_signal.len();
319
320 let classical_blend =
321 0.5 * node_features[idx_node] + 0.5 * aggregated_features[idx_agg];
322 let x = 0.5 * classical_blend + 0.5 * quantum_signal[idx_quantum];
323
324 output[i] = match self.activation {
325 ActivationType::ReLU => x.max(0.0),
326 ActivationType::Tanh => x.tanh(),
327 ActivationType::Sigmoid => 1.0 / (1.0 + (-x).exp()),
328 ActivationType::Linear => x,
329 };
330 }
331
332 Ok(output)
333 }
334
335 fn encode_features(&self, features: &Array1<f64>) -> Result<Vec<Complex64>> {
337 let state_dim = 2_usize.pow(self.num_qubits as u32);
338 let mut quantum_state = vec![Complex64::new(0.0, 0.0); state_dim];
339
340 let norm: f64 = features.iter().map(|x| x * x).sum::<f64>().sqrt();
342 if norm < 1e-10 {
343 quantum_state[0] = Complex64::new(1.0, 0.0);
344 } else {
345 for (i, &val) in features.iter().enumerate() {
346 if i < state_dim {
347 quantum_state[i] = Complex64::new(val / norm, 0.0);
348 }
349 }
350 }
351
352 Ok(quantum_state)
353 }
354
355 fn parameter_value(&self, name: &str) -> f64 {
357 *self.parameters.get(name).unwrap_or(&0.0)
358 }
359
360 fn simulate_circuit(
366 &self,
367 circuit: &VariationalCircuit,
368 mut state: Vec<Complex64>,
369 ) -> Result<Vec<Complex64>> {
370 let expected_dim = 1usize << circuit.num_qubits;
371 if state.len() != expected_dim {
372 return Err(MLError::InvalidInput(format!(
373 "quantum GCN state dimension {} does not match circuit width {} ({} qubits)",
374 state.len(),
375 expected_dim,
376 circuit.num_qubits
377 )));
378 }
379
380 for (gate_name, qubits, param_names) in &circuit.gates {
381 match gate_name.as_str() {
382 "RY" => apply_ry(&mut state, qubits[0], self.parameter_value(¶m_names[0])),
383 "RX" => apply_rx(&mut state, qubits[0], self.parameter_value(¶m_names[0])),
384 "RZ" => apply_rz(&mut state, qubits[0], self.parameter_value(¶m_names[0])),
385 "CNOT" => apply_cnot(&mut state, qubits[0], qubits[1]),
386 "CZ" => apply_cz(&mut state, qubits[0], qubits[1]),
387 other => {
388 return Err(MLError::InvalidConfiguration(format!(
389 "Unsupported gate '{other}' in quantum GCN circuit"
390 )));
391 }
392 }
393 }
394
395 Ok(state)
396 }
397}
398
399fn kronecker_product(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
402 let mut result = Vec::with_capacity(a.len() * b.len());
403 for &litude_a in a {
404 for &litude_b in b {
405 result.push(amplitude_a * amplitude_b);
406 }
407 }
408 result
409}
410
411fn expectation_z(state: &[Complex64], qubit: usize) -> f64 {
414 let bit = 1usize << qubit;
415 state
416 .iter()
417 .enumerate()
418 .map(|(i, amplitude)| {
419 let probability = amplitude.norm_sqr();
420 if i & bit == 0 {
421 probability
422 } else {
423 -probability
424 }
425 })
426 .sum()
427}
428
429fn apply_single_qubit_gate(
431 state: &mut [Complex64],
432 qubit: usize,
433 m00: Complex64,
434 m01: Complex64,
435 m10: Complex64,
436 m11: Complex64,
437) {
438 let bit = 1usize << qubit;
439 let dim = state.len();
440 for i in 0..dim {
441 if i & bit == 0 {
442 let j = i | bit;
443 let amplitude_0 = state[i];
444 let amplitude_1 = state[j];
445 state[i] = m00 * amplitude_0 + m01 * amplitude_1;
446 state[j] = m10 * amplitude_0 + m11 * amplitude_1;
447 }
448 }
449}
450
451fn apply_ry(state: &mut [Complex64], qubit: usize, theta: f64) {
453 let c = Complex64::new((theta / 2.0).cos(), 0.0);
454 let s = Complex64::new((theta / 2.0).sin(), 0.0);
455 apply_single_qubit_gate(state, qubit, c, -s, s, c);
456}
457
458fn apply_rx(state: &mut [Complex64], qubit: usize, theta: f64) {
460 let c = Complex64::new((theta / 2.0).cos(), 0.0);
461 let neg_i_s = Complex64::new(0.0, -(theta / 2.0).sin());
462 apply_single_qubit_gate(state, qubit, c, neg_i_s, neg_i_s, c);
463}
464
465fn apply_rz(state: &mut [Complex64], qubit: usize, theta: f64) {
467 let bit = 1usize << qubit;
468 let phase_0 = Complex64::from_polar(1.0, -theta / 2.0);
469 let phase_1 = Complex64::from_polar(1.0, theta / 2.0);
470 for (i, amplitude) in state.iter_mut().enumerate() {
471 *amplitude *= if i & bit == 0 { phase_0 } else { phase_1 };
472 }
473}
474
475fn apply_cnot(state: &mut [Complex64], control: usize, target: usize) {
477 let control_bit = 1usize << control;
478 let target_bit = 1usize << target;
479 let dim = state.len();
480 for i in 0..dim {
481 if i & control_bit != 0 && i & target_bit == 0 {
482 let j = i | target_bit;
483 state.swap(i, j);
484 }
485 }
486}
487
488fn apply_cz(state: &mut [Complex64], control: usize, target: usize) {
490 let control_bit = 1usize << control;
491 let target_bit = 1usize << target;
492 for (i, amplitude) in state.iter_mut().enumerate() {
493 if i & control_bit != 0 && i & target_bit != 0 {
494 *amplitude = -*amplitude;
495 }
496 }
497}
498
499#[derive(Debug)]
501pub struct QuantumGATLayer {
502 input_dim: usize,
504 output_dim: usize,
506 num_heads: usize,
508 attention_circuits: Vec<VariationalCircuit>,
510 transform_circuits: Vec<VariationalCircuit>,
512 dropout_rate: f64,
514}
515
516impl QuantumGATLayer {
517 pub fn new(input_dim: usize, output_dim: usize, num_heads: usize, dropout_rate: f64) -> Self {
519 let mut attention_circuits = Vec::new();
520 let mut transform_circuits = Vec::new();
521
522 let qubits_per_head = ((output_dim / num_heads) as f64).log2().ceil() as usize;
523
524 for _ in 0..num_heads {
525 attention_circuits.push(Self::build_attention_circuit(qubits_per_head));
526 transform_circuits.push(Self::build_transform_circuit(qubits_per_head));
527 }
528
529 Self {
530 input_dim,
531 output_dim,
532 num_heads,
533 attention_circuits,
534 transform_circuits,
535 dropout_rate,
536 }
537 }
538
539 fn build_attention_circuit(num_qubits: usize) -> VariationalCircuit {
541 let mut circuit = VariationalCircuit::new(num_qubits * 2);
542
543 for q in 0..num_qubits {
545 circuit.add_gate("RY", vec![q], vec![format!("att_src_{}", q)]);
546 circuit.add_gate("RY", vec![q + num_qubits], vec![format!("att_dst_{}", q)]);
547 }
548
549 for q in 0..num_qubits {
551 circuit.add_gate("CZ", vec![q, q + num_qubits], vec![]);
552 }
553
554 circuit.add_gate("H", vec![0], vec![]);
556 for q in 1..num_qubits * 2 {
557 circuit.add_gate("CNOT", vec![0, q], vec![]);
558 }
559
560 circuit
561 }
562
563 fn build_transform_circuit(num_qubits: usize) -> VariationalCircuit {
565 let mut circuit = VariationalCircuit::new(num_qubits);
566
567 for layer in 0..2 {
569 for q in 0..num_qubits {
570 circuit.add_gate("RY", vec![q], vec![format!("trans_ry_{}_{}", layer, q)]);
571 circuit.add_gate("RZ", vec![q], vec![format!("trans_rz_{}_{}", layer, q)]);
572 }
573
574 for q in 0..num_qubits - 1 {
576 circuit.add_gate("CX", vec![q, q + 1], vec![]);
577 }
578 }
579
580 circuit
581 }
582
583 pub fn forward(&self, graph: &QuantumGraph) -> Result<Array2<f64>> {
585 let head_dim = self.output_dim / self.num_heads;
586 let mut all_head_outputs = Vec::new();
587
588 for head in 0..self.num_heads {
590 let head_output = self.process_attention_head(graph, head)?;
591 all_head_outputs.push(head_output);
592 }
593
594 let mut output = Array2::zeros((graph.num_nodes, self.output_dim));
596 for (h, head_output) in all_head_outputs.iter().enumerate() {
597 for node in 0..graph.num_nodes {
598 for d in 0..head_dim {
599 output[[node, h * head_dim + d]] = head_output[[node, d]];
600 }
601 }
602 }
603
604 Ok(output)
605 }
606
607 fn process_attention_head(&self, graph: &QuantumGraph, head: usize) -> Result<Array2<f64>> {
609 let head_dim = self.output_dim / self.num_heads;
610 let mut output = Array2::zeros((graph.num_nodes, head_dim));
611
612 let attention_scores = self.compute_attention_scores(graph, head)?;
614
615 for node in 0..graph.num_nodes {
617 let neighbors = graph.neighbors(node);
618 let feature_dim = graph.node_features.ncols();
619 let mut weighted_features = Array1::zeros(feature_dim);
620
621 let self_score = attention_scores[[node, node]];
623 weighted_features =
624 &weighted_features + &(&graph.node_features.row(node).to_owned() * self_score);
625
626 for &neighbor in &neighbors {
628 let score = attention_scores[[node, neighbor]];
629 weighted_features =
630 &weighted_features + &(&graph.node_features.row(neighbor).to_owned() * score);
631 }
632
633 let transformed = self.transform_features(&weighted_features, head)?;
635
636 for d in 0..head_dim {
637 output[[node, d]] = transformed[d];
638 }
639 }
640
641 Ok(output)
642 }
643
644 fn compute_attention_scores(&self, graph: &QuantumGraph, head: usize) -> Result<Array2<f64>> {
646 let mut scores = Array2::zeros((graph.num_nodes, graph.num_nodes));
647
648 for i in 0..graph.num_nodes {
650 for j in 0..graph.num_nodes {
651 if i == j || graph.adjacency[[i, j]] > 0.0 {
652 let score = self.quantum_attention_score(
654 &graph.node_features.row(i).to_owned(),
655 &graph.node_features.row(j).to_owned(),
656 head,
657 )?;
658 scores[[i, j]] = score;
659 }
660 }
661
662 let neighbors = graph.neighbors(i);
664 if !neighbors.is_empty() {
665 let mut sum_exp = (scores[[i, i]]).exp();
666 for &j in &neighbors {
667 sum_exp += scores[[i, j]].exp();
668 }
669
670 scores[[i, i]] = scores[[i, i]].exp() / sum_exp;
671 for &j in &neighbors {
672 scores[[i, j]] = scores[[i, j]].exp() / sum_exp;
673 }
674 } else {
675 scores[[i, i]] = 1.0;
676 }
677 }
678
679 Ok(scores)
680 }
681
682 fn quantum_attention_score(
684 &self,
685 feat_i: &Array1<f64>,
686 feat_j: &Array1<f64>,
687 head: usize,
688 ) -> Result<f64> {
689 let dot_product: f64 = feat_i.iter().zip(feat_j.iter()).map(|(a, b)| a * b).sum();
691
692 Ok((dot_product / (self.input_dim as f64).sqrt()).tanh())
693 }
694
695 fn transform_features(&self, features: &Array1<f64>, head: usize) -> Result<Array1<f64>> {
697 let head_dim = self.output_dim / self.num_heads;
698 let mut output = Array1::zeros(head_dim);
699
700 for i in 0..head_dim {
702 if i < features.len() {
703 output[i] = features[i] * (1.0 + 0.1 * (i as f64).sin());
704 }
705 }
706
707 Ok(output)
708 }
709}
710
711#[derive(Debug)]
713pub struct QuantumMPNN {
714 message_circuit: VariationalCircuit,
716 update_circuit: VariationalCircuit,
718 readout_circuit: VariationalCircuit,
720 hidden_dim: usize,
722 num_steps: usize,
724}
725
726impl QuantumMPNN {
727 pub fn new(input_dim: usize, hidden_dim: usize, output_dim: usize, num_steps: usize) -> Self {
729 let num_qubits = (hidden_dim as f64).log2().ceil() as usize;
730
731 Self {
732 message_circuit: Self::build_message_circuit(num_qubits),
733 update_circuit: Self::build_update_circuit(num_qubits),
734 readout_circuit: Self::build_readout_circuit(num_qubits),
735 hidden_dim,
736 num_steps,
737 }
738 }
739
740 fn build_message_circuit(num_qubits: usize) -> VariationalCircuit {
742 let mut circuit = VariationalCircuit::new(num_qubits * 3); for q in 0..num_qubits * 3 {
746 circuit.add_gate("RY", vec![q], vec![format!("msg_encode_{}", q)]);
747 }
748
749 for layer in 0..2 {
751 for q in 0..num_qubits {
753 circuit.add_gate("CZ", vec![q, q + num_qubits * 2], vec![]);
754 }
755
756 for q in 0..num_qubits {
758 circuit.add_gate("CZ", vec![q + num_qubits, q + num_qubits * 2], vec![]);
759 }
760
761 for q in 0..num_qubits * 3 {
763 circuit.add_gate("RX", vec![q], vec![format!("msg_rx_{}_{}", layer, q)]);
764 }
765 }
766
767 circuit
768 }
769
770 fn build_update_circuit(num_qubits: usize) -> VariationalCircuit {
772 let mut circuit = VariationalCircuit::new(num_qubits * 2); for q in 0..num_qubits {
776 circuit.add_gate("CNOT", vec![q, q + num_qubits], vec![]);
777 }
778
779 for layer in 0..2 {
781 for q in 0..num_qubits * 2 {
782 circuit.add_gate("RY", vec![q], vec![format!("upd_ry_{}_{}", layer, q)]);
783 circuit.add_gate("RZ", vec![q], vec![format!("upd_rz_{}_{}", layer, q)]);
784 }
785
786 for q in 0..num_qubits * 2 - 1 {
788 circuit.add_gate("CX", vec![q, q + 1], vec![]);
789 }
790 }
791
792 circuit
793 }
794
795 fn build_readout_circuit(num_qubits: usize) -> VariationalCircuit {
797 let mut circuit = VariationalCircuit::new(num_qubits);
798
799 for layer in 0..3 {
801 for q in 0..num_qubits {
802 circuit.add_gate("RY", vec![q], vec![format!("read_ry_{}_{}", layer, q)]);
803 }
804
805 for i in 0..num_qubits {
807 for j in i + 1..num_qubits {
808 circuit.add_gate("CZ", vec![i, j], vec![]);
809 }
810 }
811 }
812
813 circuit
814 }
815
816 pub fn forward(&self, graph: &QuantumGraph) -> Result<Array1<f64>> {
818 let mut hidden_states = Array2::zeros((graph.num_nodes, self.hidden_dim));
820
821 for node in 0..graph.num_nodes {
823 for d in 0..self.hidden_dim.min(graph.node_features.ncols()) {
824 hidden_states[[node, d]] = graph.node_features[[node, d]];
825 }
826 }
827
828 for _ in 0..self.num_steps {
830 hidden_states = self.message_passing_step(graph, &hidden_states)?;
831 }
832
833 self.readout(graph, &hidden_states)
835 }
836
837 fn message_passing_step(
839 &self,
840 graph: &QuantumGraph,
841 hidden_states: &Array2<f64>,
842 ) -> Result<Array2<f64>> {
843 let mut new_hidden = Array2::zeros((graph.num_nodes, self.hidden_dim));
844
845 for node in 0..graph.num_nodes {
846 let neighbors = graph.neighbors(node);
847 let mut messages = Array1::zeros(self.hidden_dim);
848
849 for &neighbor in &neighbors {
851 let message = self.compute_message(
852 &hidden_states.row(neighbor).to_owned(),
853 &hidden_states.row(node).to_owned(),
854 graph
855 .edge_features
856 .as_ref()
857 .and_then(|ef| ef.get(&(neighbor, node))),
858 )?;
859 messages = &messages + &message;
860 }
861
862 let updated = self.update_node(&hidden_states.row(node).to_owned(), &messages)?;
864
865 new_hidden.row_mut(node).assign(&updated);
866 }
867
868 Ok(new_hidden)
869 }
870
871 fn compute_message(
873 &self,
874 source_hidden: &Array1<f64>,
875 dest_hidden: &Array1<f64>,
876 edge_features: Option<&Array1<f64>>,
877 ) -> Result<Array1<f64>> {
878 let mut message = Array1::zeros(self.hidden_dim);
880
881 for i in 0..self.hidden_dim {
882 let src_val = if i < source_hidden.len() {
883 source_hidden[i]
884 } else {
885 0.0
886 };
887 let dst_val = if i < dest_hidden.len() {
888 dest_hidden[i]
889 } else {
890 0.0
891 };
892 let edge_val = edge_features
893 .and_then(|ef| ef.get(i))
894 .copied()
895 .unwrap_or(1.0);
896
897 message[i] = (src_val + dst_val) * edge_val * 0.5;
898 }
899
900 Ok(message)
901 }
902
903 fn update_node(&self, hidden: &Array1<f64>, messages: &Array1<f64>) -> Result<Array1<f64>> {
905 let mut new_hidden = Array1::zeros(self.hidden_dim);
907
908 for i in 0..self.hidden_dim {
909 let h = if i < hidden.len() { hidden[i] } else { 0.0 };
910 let m = if i < messages.len() { messages[i] } else { 0.0 };
911
912 let z = (h + m).tanh(); let r = 1.0 / (1.0 + (-(h * m)).exp()); let h_tilde = ((r * h) + m).tanh(); new_hidden[i] = (1.0 - z) * h + z * h_tilde;
918 }
919
920 Ok(new_hidden)
921 }
922
923 fn readout(&self, graph: &QuantumGraph, hidden_states: &Array2<f64>) -> Result<Array1<f64>> {
925 let mut global_state: Array1<f64> = Array1::zeros(self.hidden_dim);
927
928 for node in 0..graph.num_nodes {
929 global_state = &global_state + &hidden_states.row(node).to_owned();
930 }
931 global_state = global_state / (graph.num_nodes as f64);
932
933 let mut output = Array1::zeros(self.hidden_dim);
935 for i in 0..self.hidden_dim {
936 output[i] = global_state[i].tanh();
937 }
938
939 Ok(output)
940 }
941}
942
943#[derive(Debug)]
945pub struct QuantumGraphPool {
946 pool_ratio: f64,
948 method: PoolingMethod,
950 score_circuit: VariationalCircuit,
952}
953
954#[derive(Debug, Clone)]
955pub enum PoolingMethod {
956 TopK,
958 SelfAttention,
960 DiffPool,
962}
963
964impl QuantumGraphPool {
965 pub fn new(pool_ratio: f64, method: PoolingMethod, feature_dim: usize) -> Self {
967 let num_qubits = (feature_dim as f64).log2().ceil() as usize;
968
969 Self {
970 pool_ratio,
971 method,
972 score_circuit: Self::build_score_circuit(num_qubits),
973 }
974 }
975
976 fn build_score_circuit(num_qubits: usize) -> VariationalCircuit {
978 let mut circuit = VariationalCircuit::new(num_qubits);
979
980 for layer in 0..2 {
982 for q in 0..num_qubits {
983 circuit.add_gate("RY", vec![q], vec![format!("pool_ry_{}_{}", layer, q)]);
984 }
985
986 for q in 0..num_qubits - 1 {
988 circuit.add_gate("CZ", vec![q, q + 1], vec![]);
989 }
990 }
991
992 for q in 0..num_qubits {
994 circuit.add_gate("RX", vec![q], vec![format!("pool_measure_{}", q)]);
995 }
996
997 circuit
998 }
999
1000 pub fn pool(
1002 &self,
1003 graph: &QuantumGraph,
1004 node_features: &Array2<f64>,
1005 ) -> Result<(Vec<usize>, Array2<f64>)> {
1006 match self.method {
1007 PoolingMethod::TopK => self.topk_pool(graph, node_features),
1008 PoolingMethod::SelfAttention => self.attention_pool(graph, node_features),
1009 PoolingMethod::DiffPool => self.diff_pool(graph, node_features),
1010 }
1011 }
1012
1013 fn topk_pool(
1015 &self,
1016 graph: &QuantumGraph,
1017 node_features: &Array2<f64>,
1018 ) -> Result<(Vec<usize>, Array2<f64>)> {
1019 let mut scores = Vec::new();
1021 for node in 0..graph.num_nodes {
1022 let score = self.compute_node_score(&node_features.row(node).to_owned())?;
1023 scores.push((node, score));
1024 }
1025
1026 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1028
1029 let k = ((graph.num_nodes as f64) * self.pool_ratio).ceil() as usize;
1031 let selected_nodes: Vec<usize> = scores.iter().take(k).map(|(idx, _)| *idx).collect();
1032
1033 let mut pooled_features = Array2::zeros((k, node_features.ncols()));
1035 for (i, &node) in selected_nodes.iter().enumerate() {
1036 pooled_features.row_mut(i).assign(&node_features.row(node));
1037 }
1038
1039 Ok((selected_nodes, pooled_features))
1040 }
1041
1042 fn attention_pool(
1044 &self,
1045 graph: &QuantumGraph,
1046 node_features: &Array2<f64>,
1047 ) -> Result<(Vec<usize>, Array2<f64>)> {
1048 let mut attention_scores = Array1::zeros(graph.num_nodes);
1050 for node in 0..graph.num_nodes {
1051 attention_scores[node] =
1052 self.compute_node_score(&node_features.row(node).to_owned())?;
1053 }
1054
1055 let max_score = attention_scores
1057 .iter()
1058 .cloned()
1059 .fold(f64::NEG_INFINITY, f64::max);
1060 let exp_scores: Array1<f64> = attention_scores.mapv(|x| (x - max_score).exp());
1061 let sum_exp = exp_scores.sum();
1062 let normalized_scores = exp_scores / sum_exp;
1063
1064 let k = ((graph.num_nodes as f64) * self.pool_ratio).ceil() as usize;
1066 let mut selected_nodes = Vec::new();
1067 let mut remaining_scores = normalized_scores.clone();
1068
1069 for _ in 0..k {
1070 let node = self.sample_node(&remaining_scores);
1071 selected_nodes.push(node);
1072 remaining_scores[node] = 0.0;
1073 }
1074
1075 let mut pooled_features = Array2::zeros((k, node_features.ncols()));
1077 for (i, &node) in selected_nodes.iter().enumerate() {
1078 let weighted_feature = &node_features.row(node).to_owned() * normalized_scores[node];
1079 pooled_features.row_mut(i).assign(&weighted_feature);
1080 }
1081
1082 Ok((selected_nodes, pooled_features))
1083 }
1084
1085 fn diff_pool(
1087 &self,
1088 graph: &QuantumGraph,
1089 node_features: &Array2<f64>,
1090 ) -> Result<(Vec<usize>, Array2<f64>)> {
1091 let k = ((graph.num_nodes as f64) * self.pool_ratio).ceil() as usize;
1093 let mut assignments = Array2::zeros((graph.num_nodes, k));
1094
1095 for node in 0..graph.num_nodes {
1097 for cluster in 0..k {
1098 let score =
1099 self.compute_cluster_assignment(&node_features.row(node).to_owned(), cluster)?;
1100 assignments[[node, cluster]] = score;
1101 }
1102 }
1103
1104 for node in 0..graph.num_nodes {
1106 let row_sum: f64 = assignments.row(node).sum();
1107 if row_sum > 0.0 {
1108 for cluster in 0..k {
1109 assignments[[node, cluster]] /= row_sum;
1110 }
1111 }
1112 }
1113
1114 let pooled_features = assignments.t().dot(node_features);
1116
1117 let mut selected_nodes = Vec::new();
1119 for cluster in 0..k {
1120 let mut best_node = 0;
1121 let mut best_score = 0.0;
1122
1123 for node in 0..graph.num_nodes {
1124 if assignments[[node, cluster]] > best_score {
1125 best_score = assignments[[node, cluster]];
1126 best_node = node;
1127 }
1128 }
1129
1130 selected_nodes.push(best_node);
1131 }
1132
1133 Ok((selected_nodes, pooled_features))
1134 }
1135
1136 fn compute_node_score(&self, features: &Array1<f64>) -> Result<f64> {
1138 let norm: f64 = features.iter().map(|x| x * x).sum::<f64>().sqrt();
1140 Ok(norm * (1.0 + 0.1 * fastrand::f64()))
1141 }
1142
1143 fn compute_cluster_assignment(&self, features: &Array1<f64>, cluster: usize) -> Result<f64> {
1145 let base_score = features.iter().sum::<f64>() / features.len() as f64;
1147 let cluster_bias = (cluster as f64) * 0.1;
1148 Ok((base_score + cluster_bias).exp() / (1.0 + (base_score + cluster_bias).exp()))
1149 }
1150
1151 fn sample_node(&self, scores: &Array1<f64>) -> usize {
1153 let cumsum: Vec<f64> = scores
1154 .iter()
1155 .scan(0.0, |acc, &x| {
1156 *acc += x;
1157 Some(*acc)
1158 })
1159 .collect();
1160
1161 let r = fastrand::f64() * cumsum.last().unwrap_or(&1.0);
1162
1163 for (i, &cs) in cumsum.iter().enumerate() {
1164 if r <= cs {
1165 return i;
1166 }
1167 }
1168
1169 scores.len() - 1
1170 }
1171}
1172
1173#[derive(Debug)]
1175pub struct QuantumGNN {
1176 layers: Vec<GNNLayer>,
1178 pooling: Vec<Option<QuantumGraphPool>>,
1180 readout: ReadoutType,
1182 output_dim: usize,
1184}
1185
1186#[derive(Debug)]
1187enum GNNLayer {
1188 GCN(QuantumGCNLayer),
1189 GAT(QuantumGATLayer),
1190 MPNN(QuantumMPNN),
1191}
1192
1193#[derive(Debug, Clone)]
1194pub enum ReadoutType {
1195 Mean,
1196 Max,
1197 Sum,
1198 Attention,
1199}
1200
1201impl QuantumGNN {
1202 pub fn new(
1204 layer_configs: Vec<(String, usize, usize)>, pooling_configs: Vec<Option<(f64, PoolingMethod)>>,
1206 readout: ReadoutType,
1207 output_dim: usize,
1208 ) -> Result<Self> {
1209 let mut layers = Vec::new();
1210 let mut pooling = Vec::new();
1211
1212 for (layer_type, input_dim, output_dim) in layer_configs {
1213 let layer = match layer_type.as_str() {
1214 "gcn" => GNNLayer::GCN(QuantumGCNLayer::new(
1215 input_dim,
1216 output_dim,
1217 ActivationType::ReLU,
1218 )),
1219 "gat" => GNNLayer::GAT(QuantumGATLayer::new(
1220 input_dim, output_dim, 4, 0.1, )),
1223 "mpnn" => GNNLayer::MPNN(QuantumMPNN::new(
1224 input_dim, output_dim, output_dim, 3, )),
1226 _ => {
1227 return Err(MLError::InvalidConfiguration(format!(
1228 "Unknown layer type: {}",
1229 layer_type
1230 )))
1231 }
1232 };
1233 layers.push(layer);
1234 }
1235
1236 for pool_config in pooling_configs {
1237 let pool_layer = pool_config.map(|(ratio, method)| {
1238 QuantumGraphPool::new(ratio, method, 64) });
1240 pooling.push(pool_layer);
1241 }
1242
1243 Ok(Self {
1244 layers,
1245 pooling,
1246 readout,
1247 output_dim,
1248 })
1249 }
1250
1251 pub fn forward(&self, graph: &QuantumGraph) -> Result<Array1<f64>> {
1253 let mut current_graph = graph.clone();
1254 let mut current_features = graph.node_features.clone();
1255 let mut selected_nodes: Vec<usize> = (0..graph.num_nodes).collect();
1256
1257 for (i, layer) in self.layers.iter().enumerate() {
1259 current_features = match layer {
1261 GNNLayer::GCN(gcn) => gcn.forward(¤t_graph)?,
1262 GNNLayer::GAT(gat) => gat.forward(¤t_graph)?,
1263 GNNLayer::MPNN(mpnn) => {
1264 let graph_features = mpnn.forward(¤t_graph)?;
1266 let mut node_features =
1268 Array2::zeros((current_graph.num_nodes, graph_features.len()));
1269 for node in 0..current_graph.num_nodes {
1270 node_features.row_mut(node).assign(&graph_features);
1271 }
1272 node_features
1273 }
1274 };
1275
1276 if let Some(Some(pool)) = self.pooling.get(i) {
1278 let (new_selected, pooled_features) =
1279 pool.pool(¤t_graph, ¤t_features)?;
1280
1281 current_graph =
1283 self.create_subgraph(¤t_graph, &new_selected, &pooled_features);
1284 current_features = pooled_features;
1285 selected_nodes = new_selected;
1286 }
1287 }
1288
1289 self.apply_readout(¤t_features)
1291 }
1292
1293 fn create_subgraph(
1295 &self,
1296 graph: &QuantumGraph,
1297 selected_nodes: &[usize],
1298 pooled_features: &Array2<f64>,
1299 ) -> QuantumGraph {
1300 let num_nodes = selected_nodes.len();
1301 let mut new_adjacency = Array2::zeros((num_nodes, num_nodes));
1302
1303 let index_map: HashMap<usize, usize> = selected_nodes
1305 .iter()
1306 .enumerate()
1307 .map(|(new_idx, &old_idx)| (old_idx, new_idx))
1308 .collect();
1309
1310 for (i, &old_i) in selected_nodes.iter().enumerate() {
1312 for (j, &old_j) in selected_nodes.iter().enumerate() {
1313 new_adjacency[[i, j]] = graph.adjacency[[old_i, old_j]];
1314 }
1315 }
1316
1317 let mut edges = Vec::new();
1319 for i in 0..num_nodes {
1320 for j in i + 1..num_nodes {
1321 if new_adjacency[[i, j]] > 0.0 {
1322 edges.push((i, j));
1323 }
1324 }
1325 }
1326
1327 QuantumGraph::new(num_nodes, edges, pooled_features.clone())
1329 }
1330
1331 fn apply_readout(&self, node_features: &Array2<f64>) -> Result<Array1<f64>> {
1333 let readout_features = match self.readout {
1334 ReadoutType::Mean => node_features
1335 .mean_axis(scirs2_core::ndarray::Axis(0))
1336 .ok_or_else(|| {
1337 MLError::InvalidInput("Cannot compute mean of empty array".to_string())
1338 })?,
1339 ReadoutType::Max => {
1340 let mut max_features = Array1::from_elem(node_features.ncols(), f64::NEG_INFINITY);
1341 for row in node_features.rows() {
1342 for (i, &val) in row.iter().enumerate() {
1343 max_features[i] = max_features[i].max(val);
1344 }
1345 }
1346 max_features
1347 }
1348 ReadoutType::Sum => node_features.sum_axis(scirs2_core::ndarray::Axis(0)),
1349 ReadoutType::Attention => {
1350 let mut weights = Array1::zeros(node_features.nrows());
1352 for (i, row) in node_features.rows().into_iter().enumerate() {
1353 weights[i] = row.sum(); }
1355
1356 let max_weight = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1358 let exp_weights = weights.mapv(|x| (x - max_weight).exp());
1359 let weights_norm = exp_weights.clone() / exp_weights.sum();
1360
1361 let mut result = Array1::zeros(node_features.ncols());
1363 for (i, row) in node_features.rows().into_iter().enumerate() {
1364 result = &result + &(&row.to_owned() * weights_norm[i]);
1365 }
1366 result
1367 }
1368 };
1369
1370 let mut output = Array1::zeros(self.output_dim);
1372 for i in 0..self.output_dim {
1373 if i < readout_features.len() {
1374 output[i] = readout_features[i];
1375 }
1376 }
1377
1378 Ok(output)
1379 }
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384 use super::*;
1385
1386 #[test]
1387 fn test_quantum_graph() {
1388 let nodes = 5;
1389 let edges = vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)];
1390 let features = Array2::ones((nodes, 4));
1391
1392 let graph = QuantumGraph::new(nodes, edges, features);
1393
1394 assert_eq!(graph.num_nodes, 5);
1395 assert_eq!(graph.degree(0), 2);
1396 assert_eq!(graph.neighbors(0), vec![1, 4]);
1397 }
1398
1399 #[test]
1400 fn test_quantum_gcn_layer() {
1401 let graph = QuantumGraph::new(
1402 3,
1403 vec![(0, 1), (1, 2)],
1404 Array2::from_shape_vec(
1405 (3, 4),
1406 vec![1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
1407 )
1408 .expect("Failed to create node features"),
1409 );
1410
1411 let gcn = QuantumGCNLayer::new(4, 8, ActivationType::ReLU);
1412 let output = gcn.forward(&graph).expect("Forward pass failed");
1413
1414 assert_eq!(output.shape(), &[3, 8]);
1415 }
1416
1417 #[test]
1422 fn test_quantum_gcn_layer_uses_real_quantum_circuit() {
1423 let gcn_a = QuantumGCNLayer::new(4, 8, ActivationType::Linear);
1424 let gcn_b = QuantumGCNLayer::new(4, 8, ActivationType::Linear);
1425
1426 assert!(!gcn_a.parameters.is_empty());
1431 assert_ne!(gcn_a.parameters, gcn_b.parameters);
1432
1433 let graph = QuantumGraph::new(
1434 3,
1435 vec![(0, 1), (1, 2)],
1436 Array2::from_shape_vec(
1437 (3, 4),
1438 vec![1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
1439 )
1440 .expect("Failed to create node features"),
1441 );
1442
1443 let output = gcn_a.forward(&graph).expect("Forward pass failed");
1444 assert_eq!(output.shape(), &[3, 8]);
1445 assert!(output.iter().all(|x| x.is_finite()));
1446
1447 let node_feat = graph.node_features.row(1).to_owned();
1453 let neighbor_avg =
1454 (&graph.node_features.row(0).to_owned() + &graph.node_features.row(2)) / 2.0;
1455 let mut differs = false;
1456 for i in 0..8 {
1457 let classical_only = 0.5 * node_feat[i % 4] + 0.5 * neighbor_avg[i % 4];
1458 if (output[[1, i]] - classical_only).abs() > 1e-9 {
1459 differs = true;
1460 }
1461 }
1462 assert!(
1463 differs,
1464 "expected the quantum expectation-value term to change the output \
1465 from the pure classical blend"
1466 );
1467
1468 let output_again = gcn_a.forward(&graph).expect("Forward pass failed");
1471 for (a, b) in output.iter().zip(output_again.iter()) {
1472 assert!((a - b).abs() < 1e-12);
1473 }
1474 }
1475
1476 #[test]
1477 fn test_quantum_gat_layer() {
1478 let graph = QuantumGraph::new(
1479 4,
1480 vec![(0, 1), (1, 2), (2, 3), (3, 0)],
1481 Array2::ones((4, 8)),
1482 );
1483
1484 let gat = QuantumGATLayer::new(8, 16, 4, 0.1);
1485 let output = gat.forward(&graph).expect("Forward pass failed");
1486
1487 assert_eq!(output.shape(), &[4, 16]);
1488 }
1489
1490 #[test]
1491 fn test_quantum_mpnn() {
1492 let graph = QuantumGraph::new(3, vec![(0, 1), (1, 2)], Array2::zeros((3, 4)));
1493
1494 let mpnn = QuantumMPNN::new(4, 8, 16, 2);
1495 let output = mpnn.forward(&graph).expect("Forward pass failed");
1496
1497 assert_eq!(output.len(), 8);
1498 }
1499
1500 #[test]
1501 fn test_graph_pooling() {
1502 let graph = QuantumGraph::new(
1503 6,
1504 vec![(0, 1), (1, 2), (3, 4), (4, 5)],
1505 Array2::ones((6, 4)),
1506 );
1507
1508 let pool = QuantumGraphPool::new(0.5, PoolingMethod::TopK, 4);
1509 let (selected, pooled) = pool
1510 .pool(&graph, &graph.node_features)
1511 .expect("Pooling failed");
1512
1513 assert_eq!(selected.len(), 3);
1514 assert_eq!(pooled.shape(), &[3, 4]);
1515 }
1516
1517 #[test]
1518 fn test_complete_gnn() {
1519 let layer_configs = vec![("gcn".to_string(), 4, 8), ("gat".to_string(), 8, 16)];
1520 let pooling_configs = vec![None, Some((0.5, PoolingMethod::TopK))];
1521
1522 let gnn = QuantumGNN::new(layer_configs, pooling_configs, ReadoutType::Mean, 10)
1523 .expect("Failed to create GNN");
1524
1525 let graph = QuantumGraph::new(
1526 5,
1527 vec![(0, 1), (1, 2), (2, 3), (3, 4)],
1528 Array2::ones((5, 4)),
1529 );
1530
1531 let output = gnn.forward(&graph).expect("Forward pass failed");
1532 assert_eq!(output.len(), 10);
1533 }
1534}