Skip to main content

quantrs2_ml/
qcnn.rs

1//! Quantum Convolutional Neural Networks (QCNN)
2//!
3//! This module implements quantum convolutional neural networks for
4//! quantum data processing and feature extraction.
5
6use crate::error::MLError;
7use quantrs2_circuit::prelude::*;
8use quantrs2_sim::optimized_simple::OptimizedStateVector;
9use scirs2_core::Complex64 as Complex;
10use std::f64::consts::PI;
11
12// Simple matrix types for QCNN
13type DMatrix = Vec<Vec<f64>>;
14type DVector<T> = Vec<T>;
15
16/// Quantum convolutional filter
17#[derive(Debug, Clone)]
18pub struct QuantumConvFilter {
19    /// Number of qubits in the filter
20    pub num_qubits: usize,
21    /// Stride of the convolution
22    pub stride: usize,
23    /// Variational parameters
24    pub params: Vec<f64>,
25}
26
27impl QuantumConvFilter {
28    /// Create a new quantum convolutional filter
29    pub fn new(num_qubits: usize, stride: usize) -> Self {
30        // Parameters for rotation gates
31        let num_params = num_qubits * 3; // RX, RY, RZ per qubit
32        let params = vec![0.1; num_params];
33
34        Self {
35            num_qubits,
36            stride,
37            params,
38        }
39    }
40
41    /// Apply the filter to a subset of qubits
42    pub fn apply_filter<const N: usize>(
43        &self,
44        circuit: &mut Circuit<N>,
45        start_qubit: usize,
46    ) -> Result<(), MLError> {
47        let end_qubit = (start_qubit + self.num_qubits).min(N);
48
49        // Apply parameterized rotations
50        let mut param_idx = 0;
51        for i in start_qubit..end_qubit {
52            if param_idx < self.params.len() {
53                circuit.rx(i, self.params[param_idx])?;
54                param_idx += 1;
55            }
56            if param_idx < self.params.len() {
57                circuit.ry(i, self.params[param_idx])?;
58                param_idx += 1;
59            }
60            if param_idx < self.params.len() {
61                circuit.rz(i, self.params[param_idx])?;
62                param_idx += 1;
63            }
64        }
65
66        // Apply entangling gates
67        for i in start_qubit..(end_qubit - 1) {
68            circuit.cnot(i, i + 1)?;
69        }
70
71        Ok(())
72    }
73}
74
75/// Quantum pooling layer
76#[derive(Debug, Clone)]
77pub struct QuantumPooling {
78    /// Pooling size (number of qubits to pool)
79    pub pool_size: usize,
80    /// Pooling type
81    pub pool_type: PoolingType,
82}
83
84#[derive(Debug, Clone, Copy)]
85pub enum PoolingType {
86    /// Trace out qubits (dimensionality reduction)
87    TraceOut,
88    /// Measure and reset qubits
89    MeasureReset,
90    /// Quantum pooling
91    Quantum,
92}
93
94impl QuantumPooling {
95    /// Create a new quantum pooling layer
96    pub fn new(pool_size: usize, pool_type: PoolingType) -> Self {
97        Self {
98            pool_size,
99            pool_type,
100        }
101    }
102
103    /// Apply pooling to reduce the number of active qubits
104    pub fn apply_pooling<const N: usize>(
105        &self,
106        circuit: &mut Circuit<N>,
107        active_qubits: &mut Vec<usize>,
108    ) -> Result<(), MLError> {
109        match self.pool_type {
110            PoolingType::TraceOut => {
111                // Simply remove qubits from active set
112                let new_size = active_qubits.len() / self.pool_size;
113                active_qubits.truncate(new_size);
114            }
115            PoolingType::MeasureReset => {
116                // Measure and reset every nth qubit
117                let mut new_active = Vec::new();
118                for (i, &qubit) in active_qubits.iter().enumerate() {
119                    if i % self.pool_size == 0 {
120                        new_active.push(qubit);
121                    } else {
122                        // In a real implementation, we'd measure and reset
123                        // For now, we just exclude from active set
124                    }
125                }
126                *active_qubits = new_active;
127            }
128            PoolingType::Quantum => {
129                // Quantum pooling using unitary operations
130                let pool_size = self.pool_size;
131                let new_size = active_qubits.len() / pool_size;
132
133                // Apply quantum pooling gates (simplified)
134                for i in 0..new_size {
135                    let start_idx = i * pool_size;
136                    let end_idx = (start_idx + pool_size).min(active_qubits.len());
137
138                    if end_idx > start_idx + 1 {
139                        // Apply entangling gates between qubits in pool
140                        for j in start_idx..end_idx - 1 {
141                            circuit.cnot(active_qubits[j], active_qubits[j + 1])?;
142                        }
143                    }
144                }
145
146                // Keep only the first qubit from each pool
147                active_qubits.truncate(new_size);
148            }
149        }
150        Ok(())
151    }
152}
153
154/// Quantum Convolutional Neural Network
155pub struct QCNN {
156    /// Number of qubits
157    pub num_qubits: usize,
158    /// Convolutional layers
159    pub conv_layers: Vec<(QuantumConvFilter, QuantumPooling)>,
160    /// Final fully connected layer parameters
161    pub fc_params: Vec<f64>,
162}
163
164impl QCNN {
165    /// Create a new QCNN
166    pub fn new(
167        num_qubits: usize,
168        conv_filters: Vec<(usize, usize)>, // (filter_size, stride)
169        pool_sizes: Vec<usize>,
170        fc_params: usize,
171    ) -> Result<Self, MLError> {
172        if conv_filters.len() != pool_sizes.len() {
173            return Err(MLError::ModelCreationError(
174                "Number of conv filters must match number of pooling layers".to_string(),
175            ));
176        }
177
178        let mut conv_layers = Vec::new();
179        for ((filter_size, stride), pool_size) in conv_filters.into_iter().zip(pool_sizes) {
180            let filter = QuantumConvFilter::new(filter_size, stride);
181            let pooling = QuantumPooling::new(pool_size, PoolingType::TraceOut);
182            conv_layers.push((filter, pooling));
183        }
184
185        let fc_params = vec![0.1; fc_params];
186
187        Ok(Self {
188            num_qubits,
189            conv_layers,
190            fc_params,
191        })
192    }
193
194    /// Forward pass through the QCNN
195    pub fn forward(&self, input_state: &DVector<Complex>) -> Result<DVector<Complex>, MLError> {
196        // For simulation, we'll use a fixed circuit size
197        const MAX_QUBITS: usize = 20;
198
199        if self.num_qubits > MAX_QUBITS {
200            return Err(MLError::InvalidParameter(format!(
201                "QCNN supports up to {} qubits",
202                MAX_QUBITS
203            )));
204        }
205
206        let mut circuit = Circuit::<MAX_QUBITS>::new();
207        let mut active_qubits: Vec<usize> = (0..self.num_qubits).collect();
208
209        // Build the convolutional and pooling layers as a real gate sequence.
210        for (conv_filter, pooling) in &self.conv_layers {
211            // Apply convolution with sliding window
212            let mut pos = 0;
213            while pos + conv_filter.num_qubits <= active_qubits.len() {
214                let start_qubit = active_qubits[pos];
215                conv_filter.apply_filter(&mut circuit, start_qubit)?;
216                pos += conv_filter.stride;
217            }
218
219            // Apply pooling
220            pooling.apply_pooling(&mut circuit, &mut active_qubits)?;
221        }
222
223        // Apply fully connected layer to remaining active qubits
224        for (i, &qubit) in active_qubits.iter().enumerate() {
225            if i < self.fc_params.len() {
226                circuit.ry(qubit, self.fc_params[i])?;
227            }
228        }
229
230        // --- Real simulation of U(params)|input_state⟩ ---------------------
231        // The circuit is applied to the supplied `input_state` (amplitude
232        // encoding) using a state-vector sized to `num_qubits`.
233        let num_qubits = self.num_qubits;
234        let dim = 1usize << num_qubits;
235        let mut simulator = OptimizedStateVector::new(num_qubits);
236
237        // Load the (normalized) input state into the simulator.
238        {
239            let state = simulator.state_mut();
240            for value in state.iter_mut() {
241                *value = Complex::new(0.0, 0.0);
242            }
243            let mut norm_sq = 0.0;
244            for i in 0..dim.min(input_state.len()) {
245                state[i] = input_state[i];
246                norm_sq += input_state[i].norm_sqr();
247            }
248            if norm_sq > 1e-12 {
249                let inv_norm = 1.0 / norm_sq.sqrt();
250                for value in state.iter_mut() {
251                    *value *= inv_norm;
252                }
253            } else {
254                // Empty / zero input: fall back to |0...0⟩.
255                state[0] = Complex::new(1.0, 0.0);
256            }
257        }
258
259        // Apply every gate of the circuit to the state vector.
260        for gate in circuit.gates() {
261            let qubits = gate.qubits();
262            match qubits.len() {
263                1 => {
264                    let target = qubits[0].id() as usize;
265                    if target >= num_qubits {
266                        return Err(MLError::InvalidParameter(format!(
267                            "gate targets qubit {target} outside the {num_qubits}-qubit register"
268                        )));
269                    }
270                    let matrix = gate.matrix()?;
271                    simulator.apply_single_qubit_gate(&matrix, target);
272                }
273                2 => {
274                    let control = qubits[0].id() as usize;
275                    let target = qubits[1].id() as usize;
276                    if control >= num_qubits || target >= num_qubits {
277                        return Err(MLError::InvalidParameter(format!(
278                            "two-qubit gate on ({control}, {target}) outside the \
279                             {num_qubits}-qubit register"
280                        )));
281                    }
282                    match gate.name() {
283                        "CNOT" | "CX" => simulator.apply_cnot(control, target),
284                        other => {
285                            return Err(MLError::InvalidParameter(format!(
286                                "QCNN forward pass does not support two-qubit gate '{other}'"
287                            )))
288                        }
289                    }
290                }
291                other => {
292                    return Err(MLError::InvalidParameter(format!(
293                        "QCNN forward pass does not support {other}-qubit gates"
294                    )))
295                }
296            }
297        }
298
299        // Read out the reduced state on the surviving (active) qubits by
300        // marginalising over the pooled-out qubits.  The returned amplitudes are
301        // √(marginal probability) per active-qubit basis state — the diagonal of
302        // the reduced density matrix expressed as a normalized real state vector.
303        let output_size = 1usize << active_qubits.len();
304        let mut probabilities = vec![0.0_f64; output_size];
305        let full_state = simulator.state();
306        for (full_idx, amplitude) in full_state.iter().enumerate() {
307            let mut active_index = 0usize;
308            for (bit, &qubit) in active_qubits.iter().enumerate() {
309                if (full_idx >> qubit) & 1 == 1 {
310                    active_index |= 1 << bit;
311                }
312            }
313            probabilities[active_index] += amplitude.norm_sqr();
314        }
315
316        let output = probabilities
317            .into_iter()
318            .map(|p| Complex::new(p.sqrt(), 0.0))
319            .collect();
320
321        Ok(output)
322    }
323
324    /// Get all trainable parameters
325    pub fn get_parameters(&self) -> Vec<f64> {
326        let mut params = Vec::new();
327
328        for (conv_filter, _) in &self.conv_layers {
329            params.extend(&conv_filter.params);
330        }
331        params.extend(&self.fc_params);
332
333        params
334    }
335
336    /// Set parameters from a flat vector
337    pub fn set_parameters(&mut self, params: &[f64]) -> Result<(), MLError> {
338        let mut idx = 0;
339
340        for (conv_filter, _) in &mut self.conv_layers {
341            let filter_params = conv_filter.params.len();
342            if idx + filter_params > params.len() {
343                return Err(MLError::InvalidParameter(
344                    "Not enough parameters provided".to_string(),
345                ));
346            }
347            conv_filter
348                .params
349                .copy_from_slice(&params[idx..idx + filter_params]);
350            idx += filter_params;
351        }
352
353        let fc_params_len = self.fc_params.len();
354        if idx + fc_params_len > params.len() {
355            return Err(MLError::InvalidParameter(
356                "Not enough parameters for FC layer".to_string(),
357            ));
358        }
359        self.fc_params
360            .copy_from_slice(&params[idx..idx + fc_params_len]);
361
362        Ok(())
363    }
364
365    /// Compute gradients using parameter shift rule
366    pub fn compute_gradients(
367        &mut self,
368        input_state: &DVector<Complex>,
369        target: &DVector<Complex>,
370        loss_fn: impl Fn(&DVector<Complex>, &DVector<Complex>) -> f64,
371    ) -> Result<Vec<f64>, MLError> {
372        let params = self.get_parameters();
373        let mut gradients = vec![0.0; params.len()];
374        let shift = PI / 2.0;
375
376        for i in 0..params.len() {
377            // Positive shift
378            let mut params_plus = params.clone();
379            params_plus[i] += shift;
380            self.set_parameters(&params_plus)?;
381            let output_plus = self.forward(input_state)?;
382            let loss_plus = loss_fn(&output_plus, target);
383
384            // Negative shift
385            let mut params_minus = params.clone();
386            params_minus[i] -= shift;
387            self.set_parameters(&params_minus)?;
388            let output_minus = self.forward(input_state)?;
389            let loss_minus = loss_fn(&output_minus, target);
390
391            // Parameter shift gradient
392            gradients[i] = (loss_plus - loss_minus) / (2.0 * shift);
393        }
394
395        // Restore original parameters
396        self.set_parameters(&params)?;
397
398        Ok(gradients)
399    }
400}
401
402/// Quantum image encoding for QCNN
403pub struct QuantumImageEncoder {
404    /// Image dimensions
405    pub width: usize,
406    pub height: usize,
407    /// Number of qubits for encoding
408    pub num_qubits: usize,
409}
410
411impl QuantumImageEncoder {
412    /// Create a new quantum image encoder
413    pub fn new(width: usize, height: usize, num_qubits: usize) -> Self {
414        Self {
415            width,
416            height,
417            num_qubits,
418        }
419    }
420
421    /// Encode a classical image into quantum state
422    pub fn encode(&self, image: &DMatrix) -> Result<DVector<Complex>, MLError> {
423        if image.len() != self.height || image[0].len() != self.width {
424            return Err(MLError::InvalidParameter(
425                "Image dimensions don't match encoder settings".to_string(),
426            ));
427        }
428
429        // Flatten and normalize image
430        let pixels: Vec<f64> = image.iter().flat_map(|row| row.iter()).copied().collect();
431        let norm = pixels.iter().map(|x| x * x).sum::<f64>().sqrt();
432
433        // Create quantum state with amplitude encoding
434        let state_size = 1 << self.num_qubits;
435        let mut state = vec![Complex::new(0.0, 0.0); state_size];
436
437        for (i, &pixel) in pixels.iter().enumerate() {
438            if i < state_size {
439                state[i] = Complex::new(pixel / norm, 0.0);
440            }
441        }
442
443        Ok(state)
444    }
445
446    /// Decode quantum state back to classical image representation
447    pub fn decode(&self, state: &DVector<Complex>) -> DMatrix {
448        let mut image = vec![vec![0.0; self.width]; self.height];
449        let mut idx = 0;
450
451        for i in 0..self.height {
452            for j in 0..self.width {
453                if idx < state.len() {
454                    image[i][j] = state[idx].norm();
455                    idx += 1;
456                }
457            }
458        }
459
460        image
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn test_qcnn_creation() {
470        let qcnn = QCNN::new(
471            8,                    // 8 qubits
472            vec![(4, 2), (2, 1)], // Two conv layers
473            vec![2, 2],           // Two pooling layers
474            4,                    // FC layer params
475        )
476        .expect("Failed to create QCNN with valid configuration");
477
478        assert_eq!(qcnn.num_qubits, 8);
479        assert_eq!(qcnn.conv_layers.len(), 2);
480    }
481
482    #[test]
483    fn test_quantum_filter() {
484        let filter = QuantumConvFilter::new(3, 1);
485        assert_eq!(filter.num_qubits, 3);
486        assert_eq!(filter.params.len(), 9); // 3 qubits * 3 gates
487    }
488
489    #[test]
490    fn test_filter_application() {
491        let filter = QuantumConvFilter::new(3, 1);
492        let mut circuit = Circuit::<8>::new();
493
494        // Apply filter starting at qubit 0
495        filter
496            .apply_filter(&mut circuit, 0)
497            .expect("Failed to apply quantum filter to circuit");
498
499        // Should have applied gates
500        assert!(circuit.num_gates() > 0);
501    }
502
503    #[test]
504    fn test_pooling_trace_out() {
505        let pooling = QuantumPooling::new(2, PoolingType::TraceOut);
506        let mut circuit = Circuit::<8>::new();
507        let mut active_qubits = vec![0, 1, 2, 3, 4, 5, 6, 7];
508
509        pooling
510            .apply_pooling(&mut circuit, &mut active_qubits)
511            .expect("Failed to apply trace-out pooling");
512
513        // Should reduce active qubits by pool_size
514        assert_eq!(active_qubits.len(), 4);
515    }
516
517    #[test]
518    fn test_pooling_measure_reset() {
519        let pooling = QuantumPooling::new(2, PoolingType::MeasureReset);
520        let mut circuit = Circuit::<8>::new();
521        let mut active_qubits = vec![0, 1, 2, 3, 4, 5, 6, 7];
522
523        pooling
524            .apply_pooling(&mut circuit, &mut active_qubits)
525            .expect("Failed to apply measure-reset pooling");
526
527        // Should keep every 2nd qubit
528        assert_eq!(active_qubits.len(), 4);
529        assert_eq!(active_qubits, vec![0, 2, 4, 6]);
530    }
531
532    #[test]
533    fn test_image_encoding() {
534        let encoder = QuantumImageEncoder::new(2, 2, 2);
535        let image = vec![vec![0.5, 0.5], vec![0.5, 0.5]];
536
537        let encoded = encoder.encode(&image).expect("Failed to encode image");
538        assert_eq!(encoded.len(), 4); // 2^2 = 4
539
540        // Check normalization
541        let norm: f64 = encoded.iter().map(|c| c.norm_sqr()).sum();
542        assert!((norm - 1.0).abs() < 1e-10);
543    }
544
545    #[test]
546    fn test_image_decode() {
547        let encoder = QuantumImageEncoder::new(2, 2, 2);
548        let state = vec![
549            Complex::new(0.5, 0.0),
550            Complex::new(0.5, 0.0),
551            Complex::new(0.5, 0.0),
552            Complex::new(0.5, 0.0),
553        ];
554
555        let decoded = encoder.decode(&state);
556        assert_eq!(decoded.len(), 2);
557        assert_eq!(decoded[0].len(), 2);
558    }
559
560    #[test]
561    fn test_qcnn_forward() {
562        let qcnn = QCNN::new(
563            4,            // 4 qubits
564            vec![(2, 1)], // One conv layer
565            vec![2],      // One pooling layer
566            2,            // FC layer params
567        )
568        .expect("Failed to create QCNN");
569
570        let input_state = vec![Complex::new(1.0, 0.0); 16]; // 2^4 = 16
571        let output = qcnn.forward(&input_state).expect("Failed to forward pass");
572
573        // Output should be for reduced qubits after pooling
574        assert!(!output.is_empty());
575    }
576
577    #[test]
578    fn test_parameter_management() {
579        let mut qcnn = QCNN::new(
580            4,            // 4 qubits
581            vec![(2, 1)], // One conv layer
582            vec![2],      // One pooling layer
583            2,            // FC layer params
584        )
585        .expect("Failed to create QCNN");
586
587        let params = qcnn.get_parameters();
588        let num_params = params.len();
589
590        // Modify parameters
591        let new_params: Vec<f64> = (0..num_params).map(|i| i as f64 * 0.1).collect();
592        qcnn.set_parameters(&new_params)
593            .expect("Failed to set parameters");
594
595        let retrieved_params = qcnn.get_parameters();
596        assert_eq!(retrieved_params, new_params);
597    }
598
599    #[test]
600    fn test_gradient_computation() {
601        let mut qcnn = QCNN::new(
602            4,            // 4 qubits
603            vec![(2, 1)], // One conv layer
604            vec![2],      // One pooling layer
605            2,            // FC layer params
606        )
607        .expect("Failed to create QCNN");
608
609        let input_state = vec![Complex::new(0.5, 0.0); 16];
610        let target_state = vec![Complex::new(0.707, 0.0); 2];
611
612        // Simple MSE loss
613        let loss_fn = |output: &DVector<Complex>, target: &DVector<Complex>| -> f64 {
614            output
615                .iter()
616                .zip(target.iter())
617                .map(|(o, t)| (o - t).norm_sqr())
618                .sum::<f64>()
619        };
620
621        let gradients = qcnn
622            .compute_gradients(&input_state, &target_state, loss_fn)
623            .expect("Failed to compute gradients");
624
625        // Should have gradients for all parameters
626        assert_eq!(gradients.len(), qcnn.get_parameters().len());
627    }
628
629    #[test]
630    fn test_invalid_layer_configuration() {
631        // Mismatched conv and pool layers
632        let result = QCNN::new(
633            8,
634            vec![(4, 2), (2, 1)], // Two conv layers
635            vec![2],              // Only one pooling layer
636            4,
637        );
638
639        assert!(result.is_err());
640    }
641
642    #[test]
643    fn test_stride_behavior() {
644        let filter = QuantumConvFilter::new(2, 2); // Filter size 2, stride 2
645        assert_eq!(filter.stride, 2);
646
647        let mut circuit = Circuit::<8>::new();
648
649        // Apply with stride - should skip positions
650        filter
651            .apply_filter(&mut circuit, 0)
652            .expect("Failed to apply filter at position 0");
653        filter
654            .apply_filter(&mut circuit, 2)
655            .expect("Failed to apply filter at position 2"); // Next position based on stride
656    }
657
658    #[test]
659    fn test_large_image_encoding() {
660        let encoder = QuantumImageEncoder::new(4, 4, 4); // 4x4 image, 4 qubits
661        let image = vec![vec![0.25; 4]; 4];
662
663        let encoded = encoder.encode(&image).expect("Failed to encode 4x4 image");
664        assert_eq!(encoded.len(), 16); // 2^4 = 16
665
666        // Verify partial encoding (16 pixels into 16 amplitudes)
667        let decoded = encoder.decode(&encoded);
668        assert_eq!(decoded.len(), 4);
669        assert_eq!(decoded[0].len(), 4);
670    }
671}