Skip to main content

quantrs2_ml/
gnn.rs

1//! Quantum Graph Neural Networks (GNNs) implementation.
2//!
3//! This module provides quantum versions of graph neural networks including
4//! graph convolutional networks, graph attention networks, and message passing.
5
6use 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/// Activation function types for quantum layers
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum ActivationType {
21    /// Linear activation (identity)
22    Linear,
23    /// ReLU activation
24    ReLU,
25    /// Sigmoid activation
26    Sigmoid,
27    /// Tanh activation
28    Tanh,
29}
30
31/// Graph structure for quantum processing
32#[derive(Debug, Clone)]
33pub struct QuantumGraph {
34    /// Number of nodes
35    num_nodes: usize,
36    /// Adjacency matrix
37    adjacency: Array2<f64>,
38    /// Node features
39    node_features: Array2<f64>,
40    /// Edge features (optional)
41    edge_features: Option<HashMap<(usize, usize), Array1<f64>>>,
42    /// Graph-level features (optional)
43    graph_features: Option<Array1<f64>>,
44}
45
46impl QuantumGraph {
47    /// Create a new quantum graph
48    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        // Build adjacency matrix
52        for (src, dst) in edges {
53            adjacency[[src, dst]] = 1.0;
54            adjacency[[dst, src]] = 1.0; // Undirected graph
55        }
56
57        Self {
58            num_nodes,
59            adjacency,
60            node_features,
61            edge_features: None,
62            graph_features: None,
63        }
64    }
65
66    /// Add edge features
67    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    /// Add graph-level features
76    pub fn with_graph_features(mut self, graph_features: Array1<f64>) -> Self {
77        self.graph_features = Some(graph_features);
78        self
79    }
80
81    /// Get node degree
82    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    /// Get neighbors of a node
91    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    /// Compute Laplacian matrix
102    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        &degree_matrix - &self.adjacency
108    }
109
110    /// Compute normalized Laplacian
111    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/// Quantum Graph Convolutional Layer
138#[derive(Debug)]
139pub struct QuantumGCNLayer {
140    /// Input feature dimension
141    input_dim: usize,
142    /// Output feature dimension
143    output_dim: usize,
144    /// Number of qubits
145    num_qubits: usize,
146    /// Variational circuit for node transformation
147    node_circuit: VariationalCircuit,
148    /// Variational circuit for aggregation
149    aggregation_circuit: VariationalCircuit,
150    /// Parameters
151    parameters: HashMap<String, f64>,
152    /// Activation type
153    activation: ActivationType,
154}
155
156impl QuantumGCNLayer {
157    /// Create a new quantum GCN layer
158    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        // Draw a real initial value for every named variational parameter
164        // that appears in either circuit, so `quantum_transform` has actual
165        // bound rotation angles to run the circuits with (previously this
166        // map was always left empty and the circuits were never executed).
167        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    /// Build node transformation circuit
193    fn build_node_circuit(num_qubits: usize) -> VariationalCircuit {
194        let mut circuit = VariationalCircuit::new(num_qubits);
195
196        // Layer 1: Feature encoding
197        for q in 0..num_qubits {
198            circuit.add_gate("RY", vec![q], vec![format!("node_encode_{}", q)]);
199        }
200
201        // Layer 2: Entangling
202        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            // Parameterized rotations
211            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    /// Build aggregation circuit
221    fn build_aggregation_circuit(num_qubits: usize) -> VariationalCircuit {
222        let mut circuit = VariationalCircuit::new(num_qubits * 2); // For neighbor aggregation
223
224        // Combine node and neighbor features
225        for q in 0..num_qubits {
226            circuit.add_gate("CZ", vec![q, q + num_qubits], vec![]);
227        }
228
229        // Mixing layer
230        for q in 0..num_qubits * 2 {
231            circuit.add_gate("RY", vec![q], vec![format!("agg_ry_{}", q)]);
232        }
233
234        // Entangling
235        for q in 0..num_qubits * 2 - 1 {
236            circuit.add_gate("CNOT", vec![q, q + 1], vec![]);
237        }
238
239        // Final rotation
240        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    /// Forward pass through GCN layer
248    pub fn forward(&self, graph: &QuantumGraph) -> Result<Array2<f64>> {
249        let mut output_features = Array2::zeros((graph.num_nodes, self.output_dim));
250
251        // Process each node
252        for node in 0..graph.num_nodes {
253            // Get node features
254            let node_feat = graph.node_features.row(node);
255
256            // Get neighbor features
257            let neighbors = graph.neighbors(node);
258            let mut aggregated = Array1::zeros(self.input_dim);
259
260            // Aggregate neighbor features
261            for &neighbor in &neighbors {
262                let neighbor_feat = graph.node_features.row(neighbor);
263                aggregated = &aggregated + &neighbor_feat.to_owned();
264            }
265
266            // Normalize by degree
267            let degree = neighbors.len().max(1) as f64;
268            aggregated = aggregated / degree;
269
270            // Apply quantum transformation
271            let transformed = self.quantum_transform(&node_feat.to_owned(), &aggregated)?;
272
273            // Store output
274            for i in 0..self.output_dim {
275                output_features[[node, i]] = transformed[i];
276            }
277        }
278
279        Ok(output_features)
280    }
281
282    /// Apply quantum transformation.
283    ///
284    /// The node and neighbor features are amplitude-encoded into two
285    /// `num_qubits`-wide quantum registers, combined into one genuine joint
286    /// state via a tensor (Kronecker) product, and evolved through the real
287    /// `aggregation_circuit` (whose `CZ` gates entangle the two registers,
288    /// mirroring quantum message passing between a node and its neighbors).
289    /// The real Pauli-Z expectation values measured on the resulting state
290    /// are then blended with the classical feature averages to form the
291    /// output, so the encoded quantum states are actually used rather than
292    /// discarded.
293    fn quantum_transform(
294        &self,
295        node_features: &Array1<f64>,
296        aggregated_features: &Array1<f64>,
297    ) -> Result<Array1<f64>> {
298        // Encode features into quantum states.
299        let node_encoded = self.encode_features(node_features)?;
300        let agg_encoded = self.encode_features(aggregated_features)?;
301
302        // Combine the node and neighbor registers into one joint state and
303        // run the real aggregation circuit over it.
304        let joint_state = kronecker_product(&node_encoded, &agg_encoded);
305        let evolved_state = self.simulate_circuit(&self.aggregation_circuit, joint_state)?;
306
307        // Read out real <Z> expectation values on the neighbor-register
308        // qubits (qubits 0..num_qubits of the joint state), which is where
309        // the aggregation circuit's final rotation layer acts.
310        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    /// Encode classical features to quantum state
336    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        // Amplitude encoding
341        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    /// Look up this layer's bound value for a named variational parameter.
356    fn parameter_value(&self, name: &str) -> f64 {
357        *self.parameters.get(name).unwrap_or(&0.0)
358    }
359
360    /// Execute a [`VariationalCircuit`] against a statevector using this
361    /// layer's bound parameter values, returning the resulting statevector.
362    /// This is a genuine gate-by-gate statevector simulation (not a
363    /// placeholder): `RY`/`RX`/`RZ` apply the standard single-qubit rotation
364    /// matrices and `CNOT`/`CZ` apply the standard two-qubit gates.
365    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(&param_names[0])),
383                "RX" => apply_rx(&mut state, qubits[0], self.parameter_value(&param_names[0])),
384                "RZ" => apply_rz(&mut state, qubits[0], self.parameter_value(&param_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
399/// Tensor (Kronecker) product of two statevectors, combining two separate
400/// quantum registers into one joint state: `result[i * b.len() + j] = a[i] * b[j]`.
401fn kronecker_product(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
402    let mut result = Vec::with_capacity(a.len() * b.len());
403    for &amplitude_a in a {
404        for &amplitude_b in b {
405            result.push(amplitude_a * amplitude_b);
406        }
407    }
408    result
409}
410
411/// Real Pauli-Z expectation value on `qubit` of a state:
412/// `<Z> = sum_i (-1)^{bit_qubit(i)} |amplitude_i|^2`.
413fn 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
429/// Apply a single-qubit gate given by its 2x2 matrix entries to `qubit`.
430fn 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
451/// Apply the standard `RY(theta)` rotation gate to `qubit`.
452fn 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
458/// Apply the standard `RX(theta)` rotation gate to `qubit`.
459fn 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
465/// Apply the standard `RZ(theta)` rotation gate to `qubit`.
466fn 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
475/// Apply the standard `CNOT` gate (flip `target` when `control` is set).
476fn 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
488/// Apply the standard `CZ` gate (phase-flip when both qubits are set).
489fn 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/// Quantum Graph Attention Layer
500#[derive(Debug)]
501pub struct QuantumGATLayer {
502    /// Input dimension
503    input_dim: usize,
504    /// Output dimension
505    output_dim: usize,
506    /// Number of attention heads
507    num_heads: usize,
508    /// Attention circuits for each head
509    attention_circuits: Vec<VariationalCircuit>,
510    /// Feature transformation circuits
511    transform_circuits: Vec<VariationalCircuit>,
512    /// Dropout rate
513    dropout_rate: f64,
514}
515
516impl QuantumGATLayer {
517    /// Create a new quantum GAT layer
518    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    /// Build attention circuit
540    fn build_attention_circuit(num_qubits: usize) -> VariationalCircuit {
541        let mut circuit = VariationalCircuit::new(num_qubits * 2);
542
543        // Attention computation between node pairs
544        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        // Interaction layer
550        for q in 0..num_qubits {
551            circuit.add_gate("CZ", vec![q, q + num_qubits], vec![]);
552        }
553
554        // Attention score computation
555        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    /// Build feature transformation circuit
564    fn build_transform_circuit(num_qubits: usize) -> VariationalCircuit {
565        let mut circuit = VariationalCircuit::new(num_qubits);
566
567        // Feature transformation
568        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            // Entangling
575            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    /// Forward pass
584    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        // Process each attention head
589        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        // Concatenate heads
595        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    /// Process single attention head
608    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        // Compute attention scores
613        let attention_scores = self.compute_attention_scores(graph, head)?;
614
615        // Apply attention to features
616        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            // Self-attention
622            let self_score = attention_scores[[node, node]];
623            weighted_features =
624                &weighted_features + &(&graph.node_features.row(node).to_owned() * self_score);
625
626            // Neighbor attention
627            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            // Transform features
634            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    /// Compute attention scores
645    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        // Compute pairwise attention scores
649        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                    // Quantum attention computation (simplified)
653                    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            // Softmax normalization
663            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    /// Compute quantum attention score
683    fn quantum_attention_score(
684        &self,
685        feat_i: &Array1<f64>,
686        feat_j: &Array1<f64>,
687        head: usize,
688    ) -> Result<f64> {
689        // Simplified attention score computation
690        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    /// Transform features using quantum circuit
696    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        // Apply transformation (simplified)
701        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/// Quantum Message Passing Neural Network
712#[derive(Debug)]
713pub struct QuantumMPNN {
714    /// Message function circuit
715    message_circuit: VariationalCircuit,
716    /// Update function circuit
717    update_circuit: VariationalCircuit,
718    /// Readout function circuit
719    readout_circuit: VariationalCircuit,
720    /// Hidden dimension
721    hidden_dim: usize,
722    /// Number of message passing steps
723    num_steps: usize,
724}
725
726impl QuantumMPNN {
727    /// Create a new quantum MPNN
728    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    /// Build message function circuit
741    fn build_message_circuit(num_qubits: usize) -> VariationalCircuit {
742        let mut circuit = VariationalCircuit::new(num_qubits * 3); // Source, dest, edge
743
744        // Encode node and edge features
745        for q in 0..num_qubits * 3 {
746            circuit.add_gate("RY", vec![q], vec![format!("msg_encode_{}", q)]);
747        }
748
749        // Interaction layers
750        for layer in 0..2 {
751            // Source-edge interaction
752            for q in 0..num_qubits {
753                circuit.add_gate("CZ", vec![q, q + num_qubits * 2], vec![]);
754            }
755
756            // Dest-edge interaction
757            for q in 0..num_qubits {
758                circuit.add_gate("CZ", vec![q + num_qubits, q + num_qubits * 2], vec![]);
759            }
760
761            // Parameterized rotations
762            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    /// Build update function circuit
771    fn build_update_circuit(num_qubits: usize) -> VariationalCircuit {
772        let mut circuit = VariationalCircuit::new(num_qubits * 2); // Hidden state + messages
773
774        // Combine hidden state and messages
775        for q in 0..num_qubits {
776            circuit.add_gate("CNOT", vec![q, q + num_qubits], vec![]);
777        }
778
779        // Update layers
780        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            // Entangling
787            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    /// Build readout function circuit
796    fn build_readout_circuit(num_qubits: usize) -> VariationalCircuit {
797        let mut circuit = VariationalCircuit::new(num_qubits);
798
799        // Global pooling layers
800        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            // All-to-all connectivity
806            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    /// Forward pass
817    pub fn forward(&self, graph: &QuantumGraph) -> Result<Array1<f64>> {
818        // Initialize hidden states
819        let mut hidden_states = Array2::zeros((graph.num_nodes, self.hidden_dim));
820
821        // Initialize with node features
822        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        // Message passing steps
829        for _ in 0..self.num_steps {
830            hidden_states = self.message_passing_step(graph, &hidden_states)?;
831        }
832
833        // Global readout
834        self.readout(graph, &hidden_states)
835    }
836
837    /// Single message passing step
838    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            // Aggregate messages from neighbors
850            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            // Update hidden state
863            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    /// Compute message between nodes
872    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        // Simplified message computation
879        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    /// Update node hidden state
904    fn update_node(&self, hidden: &Array1<f64>, messages: &Array1<f64>) -> Result<Array1<f64>> {
905        // GRU-like update
906        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            // Simplified GRU update
913            let z = (h + m).tanh(); // Update gate
914            let r = 1.0 / (1.0 + (-(h * m)).exp()); // Reset gate (sigmoid)
915            let h_tilde = ((r * h) + m).tanh(); // Candidate
916
917            new_hidden[i] = (1.0 - z) * h + z * h_tilde;
918        }
919
920        Ok(new_hidden)
921    }
922
923    /// Global graph readout
924    fn readout(&self, graph: &QuantumGraph, hidden_states: &Array2<f64>) -> Result<Array1<f64>> {
925        // Mean pooling
926        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        // Apply readout transformation (simplified)
934        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/// Quantum Graph Pooling Layer
944#[derive(Debug)]
945pub struct QuantumGraphPool {
946    /// Pooling ratio
947    pool_ratio: f64,
948    /// Pooling method
949    method: PoolingMethod,
950    /// Score computation circuit
951    score_circuit: VariationalCircuit,
952}
953
954#[derive(Debug, Clone)]
955pub enum PoolingMethod {
956    /// Top-K pooling
957    TopK,
958    /// Self-attention pooling
959    SelfAttention,
960    /// Differential pooling
961    DiffPool,
962}
963
964impl QuantumGraphPool {
965    /// Create a new quantum graph pooling layer
966    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    /// Build score computation circuit
977    fn build_score_circuit(num_qubits: usize) -> VariationalCircuit {
978        let mut circuit = VariationalCircuit::new(num_qubits);
979
980        // Score computation layers
981        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            // Entangling
987            for q in 0..num_qubits - 1 {
988                circuit.add_gate("CZ", vec![q, q + 1], vec![]);
989            }
990        }
991
992        // Measurement preparation
993        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    /// Pool graph nodes
1001    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    /// Top-K pooling
1014    fn topk_pool(
1015        &self,
1016        graph: &QuantumGraph,
1017        node_features: &Array2<f64>,
1018    ) -> Result<(Vec<usize>, Array2<f64>)> {
1019        // Compute node scores
1020        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        // Sort by score
1027        scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1028
1029        // Select top-k nodes
1030        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        // Extract pooled features
1034        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    /// Self-attention pooling
1043    fn attention_pool(
1044        &self,
1045        graph: &QuantumGraph,
1046        node_features: &Array2<f64>,
1047    ) -> Result<(Vec<usize>, Array2<f64>)> {
1048        // Compute attention scores
1049        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        // Softmax normalization
1056        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        // Sample nodes based on attention
1065        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        // Weight features by attention
1076        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    /// Differentiable pooling
1086    fn diff_pool(
1087        &self,
1088        graph: &QuantumGraph,
1089        node_features: &Array2<f64>,
1090    ) -> Result<(Vec<usize>, Array2<f64>)> {
1091        // Compute soft cluster assignments
1092        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        // Initialize with quantum circuit outputs
1096        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        // Normalize assignments (soft clustering)
1105        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        // Compute pooled features
1115        let pooled_features = assignments.t().dot(node_features);
1116
1117        // Select representative nodes (hard assignment)
1118        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    /// Compute node score using quantum circuit
1137    fn compute_node_score(&self, features: &Array1<f64>) -> Result<f64> {
1138        // Simplified score computation
1139        let norm: f64 = features.iter().map(|x| x * x).sum::<f64>().sqrt();
1140        Ok(norm * (1.0 + 0.1 * fastrand::f64()))
1141    }
1142
1143    /// Compute cluster assignment score
1144    fn compute_cluster_assignment(&self, features: &Array1<f64>, cluster: usize) -> Result<f64> {
1145        // Simplified cluster assignment
1146        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    /// Sample node based on scores
1152    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/// Complete Quantum GNN model
1174#[derive(Debug)]
1175pub struct QuantumGNN {
1176    /// GNN layers
1177    layers: Vec<GNNLayer>,
1178    /// Pooling layers
1179    pooling: Vec<Option<QuantumGraphPool>>,
1180    /// Final readout
1181    readout: ReadoutType,
1182    /// Output dimension
1183    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    /// Create a new quantum GNN
1203    pub fn new(
1204        layer_configs: Vec<(String, usize, usize)>, // (type, input_dim, output_dim)
1205        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,   // num_heads
1221                    0.1, // dropout
1222                )),
1223                "mpnn" => GNNLayer::MPNN(QuantumMPNN::new(
1224                    input_dim, output_dim, output_dim, 3, // num_steps
1225                )),
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) // feature_dim placeholder
1239            });
1240            pooling.push(pool_layer);
1241        }
1242
1243        Ok(Self {
1244            layers,
1245            pooling,
1246            readout,
1247            output_dim,
1248        })
1249    }
1250
1251    /// Forward pass through the GNN
1252    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        // Pass through layers with optional pooling
1258        for (i, layer) in self.layers.iter().enumerate() {
1259            // Apply GNN layer
1260            current_features = match layer {
1261                GNNLayer::GCN(gcn) => gcn.forward(&current_graph)?,
1262                GNNLayer::GAT(gat) => gat.forward(&current_graph)?,
1263                GNNLayer::MPNN(mpnn) => {
1264                    // MPNN returns graph-level features
1265                    let graph_features = mpnn.forward(&current_graph)?;
1266                    // Broadcast to all nodes for consistency
1267                    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            // Apply pooling if configured
1277            if let Some(Some(pool)) = self.pooling.get(i) {
1278                let (new_selected, pooled_features) =
1279                    pool.pool(&current_graph, &current_features)?;
1280
1281                // Create subgraph with updated features
1282                current_graph =
1283                    self.create_subgraph(&current_graph, &new_selected, &pooled_features);
1284                current_features = pooled_features;
1285                selected_nodes = new_selected;
1286            }
1287        }
1288
1289        // Global readout
1290        self.apply_readout(&current_features)
1291    }
1292
1293    /// Create subgraph from selected nodes
1294    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        // Map old indices to new indices
1304        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        // Build new adjacency matrix
1311        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        // Build edge list
1318        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        // Use the pooled features instead of extracting from old graph
1328        QuantumGraph::new(num_nodes, edges, pooled_features.clone())
1329    }
1330
1331    /// Apply readout operation
1332    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                // Compute attention weights
1351                let mut weights = Array1::zeros(node_features.nrows());
1352                for (i, row) in node_features.rows().into_iter().enumerate() {
1353                    weights[i] = row.sum(); // Simple attention
1354                }
1355
1356                // Softmax
1357                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                // Weighted sum
1362                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        // Final projection to output dimension
1371        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    /// Regression test for the `quantum_transform` fabrication bug: the
1418    /// encoded node/neighbor quantum states must actually be run through a
1419    /// real circuit and measured, rather than being computed and discarded
1420    /// in favor of a purely classical 0.5/0.5 blend.
1421    #[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        // Real random parameter initialization: the variational parameter
1427        // map must be populated (not left permanently empty), and two
1428        // independently constructed layers should not draw identical
1429        // parameter values.
1430        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        // Node 1's pure classical blend (ignoring the quantum contribution
1448        // entirely) would be exactly 0.5 * node_features + 0.5 *
1449        // neighbor_average; the real quantum expectation-value term mixed
1450        // in by `quantum_transform` should make at least one output entry
1451        // differ from that pure-classical value.
1452        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        // Determinism: repeated forward passes with the same (fixed)
1469        // parameters must reproduce the same output.
1470        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}