Skip to main content

quantrs2_core/qml/
layers.rs

1//! Common quantum machine learning layers
2//!
3//! This module provides implementations of common QML layers including
4//! rotation layers, entangling layers, and composite layers.
5
6use super::{create_entangling_gates, EntanglementPattern, QMLLayer};
7use crate::{
8    error::{QuantRS2Error, QuantRS2Result},
9    gate::GateOp,
10    parametric::{ParametricRotationX, ParametricRotationY, ParametricRotationZ},
11    qubit::QubitId,
12};
13use scirs2_core::ndarray::Array1;
14use scirs2_core::Complex64;
15use std::f64::consts::PI;
16
17// Parameter type for QML
18#[derive(Debug, Clone)]
19pub struct Parameter {
20    pub name: String,
21    pub value: f64,
22    pub bounds: Option<(f64, f64)>,
23}
24
25// Simple wrapper types for the QML module
26type RXGate = ParametricRotationX;
27type RYGate = ParametricRotationY;
28type RZGate = ParametricRotationZ;
29
30// Simple CNOT gate for QML usage
31#[derive(Debug, Clone, Copy)]
32struct CNOT {
33    control: QubitId,
34    target: QubitId,
35}
36
37impl GateOp for CNOT {
38    fn name(&self) -> &'static str {
39        "CNOT"
40    }
41
42    fn qubits(&self) -> Vec<QubitId> {
43        vec![self.control, self.target]
44    }
45
46    fn matrix(&self) -> crate::error::QuantRS2Result<Vec<Complex64>> {
47        Ok(vec![
48            Complex64::new(1.0, 0.0),
49            Complex64::new(0.0, 0.0),
50            Complex64::new(0.0, 0.0),
51            Complex64::new(0.0, 0.0),
52            Complex64::new(0.0, 0.0),
53            Complex64::new(1.0, 0.0),
54            Complex64::new(0.0, 0.0),
55            Complex64::new(0.0, 0.0),
56            Complex64::new(0.0, 0.0),
57            Complex64::new(0.0, 0.0),
58            Complex64::new(0.0, 0.0),
59            Complex64::new(1.0, 0.0),
60            Complex64::new(0.0, 0.0),
61            Complex64::new(0.0, 0.0),
62            Complex64::new(1.0, 0.0),
63            Complex64::new(0.0, 0.0),
64        ])
65    }
66
67    fn as_any(&self) -> &dyn std::any::Any {
68        self
69    }
70
71    fn clone_gate(&self) -> Box<dyn GateOp> {
72        Box::new(*self)
73    }
74}
75
76/// A layer of rotation gates on all qubits
77#[derive(Debug, Clone)]
78pub struct RotationLayer {
79    /// Number of qubits
80    num_qubits: usize,
81    /// Rotation axes (X, Y, or Z for each qubit)
82    axes: Vec<char>,
83    /// Parameters for each rotation
84    parameters: Vec<Parameter>,
85    /// Layer name
86    name: String,
87}
88
89impl RotationLayer {
90    /// Create a new rotation layer
91    pub fn new(num_qubits: usize, axes: Vec<char>) -> QuantRS2Result<Self> {
92        if axes.len() != num_qubits {
93            return Err(QuantRS2Error::InvalidInput(format!(
94                "Expected {} axes, got {}",
95                num_qubits,
96                axes.len()
97            )));
98        }
99
100        for &axis in &axes {
101            if !['X', 'Y', 'Z'].contains(&axis) {
102                return Err(QuantRS2Error::InvalidInput(format!(
103                    "Invalid rotation axis: {axis}"
104                )));
105            }
106        }
107
108        let parameters = (0..num_qubits)
109            .map(|i| Parameter {
110                name: format!("rot_{}_{}", axes[i], i),
111                value: 0.0,
112                bounds: Some((-2.0 * PI, 2.0 * PI)),
113            })
114            .collect();
115
116        let name = format!("RotationLayer_{}", axes.iter().collect::<String>());
117
118        Ok(Self {
119            num_qubits,
120            axes,
121            parameters,
122            name,
123        })
124    }
125
126    /// Create a layer with all rotations on the same axis
127    pub fn uniform(num_qubits: usize, axis: char) -> QuantRS2Result<Self> {
128        Self::new(num_qubits, vec![axis; num_qubits])
129    }
130}
131
132impl QMLLayer for RotationLayer {
133    fn num_qubits(&self) -> usize {
134        self.num_qubits
135    }
136
137    fn parameters(&self) -> &[Parameter] {
138        &self.parameters
139    }
140
141    fn parameters_mut(&mut self) -> &mut [Parameter] {
142        &mut self.parameters
143    }
144
145    fn gates(&self) -> Vec<Box<dyn GateOp>> {
146        self.parameters
147            .iter()
148            .enumerate()
149            .map(|(i, param)| {
150                let qubit = QubitId(i as u32);
151                let gate: Box<dyn GateOp> = match self.axes[i] {
152                    'X' => Box::new(ParametricRotationX::new(qubit, param.value)),
153                    'Y' => Box::new(ParametricRotationY::new(qubit, param.value)),
154                    'Z' => Box::new(ParametricRotationZ::new(qubit, param.value)),
155                    _ => unreachable!(),
156                };
157                gate
158            })
159            .collect()
160    }
161
162    fn compute_gradients(
163        &self,
164        state: &Array1<Complex64>,
165        loss_gradient: &Array1<Complex64>,
166    ) -> QuantRS2Result<Vec<f64>> {
167        // Parameter shift rule: ∂⟨O⟩/∂θ_i = ½[⟨O⟩(θ_i + π/2) - ⟨O⟩(θ_i - π/2)]
168        //
169        // For a rotation gate R_P(θ)|ψ⟩ with Pauli P, the generator is P/2.
170        // The gradient of the expectation value equals the difference of
171        // two expectation values evaluated at θ ± π/2.
172        //
173        // Here we approximate the expectation value shift using the inner product
174        // of the current state with the loss_gradient direction, modulated by the
175        // derivative of the rotation: d/dθ [cos θ + i·sin θ·P] = -sin θ + i·cos θ·P.
176        // We use cos(θ + π/2) = -sin θ and sin(θ + π/2) = cos θ.
177        let shift = std::f64::consts::PI / 2.0;
178        let mut gradients = Vec::with_capacity(self.parameters.len());
179
180        for (i, param) in self.parameters.iter().enumerate() {
181            let theta = param.value;
182            // Derivative of the i-th qubit rotation w.r.t. θ_i:
183            // d/dθ R_P(θ) = ½ R_P(θ + π/2) - ½ R_P(θ - π/2)
184            // Using the chain rule with the state vector:
185            //   grad_i ≈ Re⟨loss_gradient | d/dθ_i |ψ⟩
186            // For a single-qubit rotation on qubit i:
187            //   d/dθ R_P(θ)|ψ_i⟩ = ½(R_P(θ+π/2) - R_P(θ-π/2))|ψ_i⟩
188            // We represent this as a scalar via the projection onto loss_gradient.
189            let qubit_idx = i;
190            if qubit_idx >= state.len() {
191                gradients.push(0.0);
192                continue;
193            }
194
195            // Rotation angle derivative: d/dθ e^{-iθ/2 P} acts as multiplication by
196            // ±i/2 in the diagonal basis; for a pure rotation cos(θ/2)|0⟩ - i·sin(θ/2)|1⟩
197            // the amplitude gradient on the affected component is -sin(θ/2) or cos(θ/2).
198            // We use the parameter-shift scalar approximation for real-valued cost functions.
199            let cos_shift = (theta + shift).cos() - (theta - shift).cos(); // = -2 sin θ
200            let sin_shift = (theta + shift).sin() - (theta - shift).sin(); // =  2 cos θ
201
202            // Compute gradient as Re[⟨loss_gradient_i | dψ_i⟩]
203            let amp = state[qubit_idx];
204            let lg = loss_gradient[qubit_idx];
205            // d|ψ_i⟩/dθ ≈ (cos_shift/2) + i·(sin_shift/2) applied to amp
206            let d_amp = Complex64::new(
207                cos_shift / 2.0 * amp.re - sin_shift / 2.0 * amp.im,
208                cos_shift / 2.0 * amp.im + sin_shift / 2.0 * amp.re,
209            );
210            let grad = lg.re * d_amp.re + lg.im * d_amp.im;
211            gradients.push(grad);
212        }
213
214        Ok(gradients)
215    }
216
217    fn name(&self) -> &str {
218        &self.name
219    }
220}
221
222/// A layer of entangling gates
223#[derive(Debug, Clone)]
224pub struct EntanglingLayer {
225    /// Number of qubits
226    num_qubits: usize,
227    /// Entanglement pattern
228    pattern: EntanglementPattern,
229    /// Parameters for parameterized entangling gates (if any)
230    parameters: Vec<Parameter>,
231    /// Whether to use parameterized gates
232    parameterized: bool,
233    /// Layer name
234    name: String,
235}
236
237impl EntanglingLayer {
238    /// Create a new entangling layer with CNOT gates
239    pub fn new(num_qubits: usize, pattern: EntanglementPattern) -> Self {
240        let name = format!("EntanglingLayer_{pattern:?}");
241
242        Self {
243            num_qubits,
244            pattern,
245            parameters: vec![],
246            parameterized: false,
247            name,
248        }
249    }
250
251    /// Create a parameterized entangling layer (e.g., with CRZ gates)
252    pub fn parameterized(num_qubits: usize, pattern: EntanglementPattern) -> Self {
253        let pairs = create_entangling_gates(num_qubits, pattern);
254        let parameters = pairs
255            .iter()
256            .enumerate()
257            .map(|(_i, (ctrl, tgt))| Parameter {
258                name: format!("entangle_{}_{}", ctrl.0, tgt.0),
259                value: 0.0,
260                bounds: Some((-PI, PI)),
261            })
262            .collect();
263
264        let name = format!("ParameterizedEntanglingLayer_{pattern:?}");
265
266        Self {
267            num_qubits,
268            pattern,
269            parameters,
270            parameterized: true,
271            name,
272        }
273    }
274}
275
276impl QMLLayer for EntanglingLayer {
277    fn num_qubits(&self) -> usize {
278        self.num_qubits
279    }
280
281    fn parameters(&self) -> &[Parameter] {
282        &self.parameters
283    }
284
285    fn parameters_mut(&mut self) -> &mut [Parameter] {
286        &mut self.parameters
287    }
288
289    fn gates(&self) -> Vec<Box<dyn GateOp>> {
290        let pairs = create_entangling_gates(self.num_qubits, self.pattern);
291
292        if self.parameterized {
293            // Parameterized entangling gates: controlled-RZ(θ) using each
294            // trainable parameter as the rotation angle.
295            pairs
296                .iter()
297                .zip(self.parameters.iter())
298                .map(|((ctrl, tgt), param)| {
299                    Box::new(crate::gate::multi::CRZ {
300                        control: *ctrl,
301                        target: *tgt,
302                        theta: param.value,
303                    }) as Box<dyn GateOp>
304                })
305                .collect()
306        } else {
307            // Use fixed CNOT gates
308            pairs
309                .iter()
310                .map(|(ctrl, tgt)| {
311                    Box::new(CNOT {
312                        control: *ctrl,
313                        target: *tgt,
314                    }) as Box<dyn GateOp>
315                })
316                .collect()
317        }
318    }
319
320    fn compute_gradients(
321        &self,
322        _state: &Array1<Complex64>,
323        _loss_gradient: &Array1<Complex64>,
324    ) -> QuantRS2Result<Vec<f64>> {
325        if self.parameterized {
326            // Would compute gradients for parameterized gates
327            Ok(vec![0.0; self.parameters.len()])
328        } else {
329            // No parameters, no gradients
330            Ok(vec![])
331        }
332    }
333
334    fn name(&self) -> &str {
335        &self.name
336    }
337}
338
339/// A composite layer combining rotations and entanglement
340#[derive(Debug, Clone)]
341pub struct StronglyEntanglingLayer {
342    /// Number of qubits
343    num_qubits: usize,
344    /// Rotation layers (one for each axis)
345    rotation_layers: Vec<RotationLayer>,
346    /// Entangling layer
347    entangling_layer: EntanglingLayer,
348    /// Total parameters
349    total_parameters: usize,
350    /// Layer name
351    name: String,
352}
353
354impl StronglyEntanglingLayer {
355    /// Create a new strongly entangling layer
356    pub fn new(num_qubits: usize, pattern: EntanglementPattern) -> QuantRS2Result<Self> {
357        let rotation_layers = vec![
358            RotationLayer::uniform(num_qubits, 'X')?,
359            RotationLayer::uniform(num_qubits, 'Y')?,
360            RotationLayer::uniform(num_qubits, 'Z')?,
361        ];
362
363        let entangling_layer = EntanglingLayer::new(num_qubits, pattern);
364
365        let total_parameters = rotation_layers
366            .iter()
367            .map(|layer| layer.parameters().len())
368            .sum::<usize>()
369            + entangling_layer.parameters().len();
370
371        let name = format!("StronglyEntanglingLayer_{pattern:?}");
372
373        Ok(Self {
374            num_qubits,
375            rotation_layers,
376            entangling_layer,
377            total_parameters,
378            name,
379        })
380    }
381}
382
383impl QMLLayer for StronglyEntanglingLayer {
384    fn num_qubits(&self) -> usize {
385        self.num_qubits
386    }
387
388    fn parameters(&self) -> &[Parameter] {
389        // This is a simplified implementation
390        // In practice, we'd need to return a combined view
391        &[]
392    }
393
394    fn parameters_mut(&mut self) -> &mut [Parameter] {
395        // This is a simplified implementation
396        &mut []
397    }
398
399    fn set_parameters(&mut self, values: &[f64]) -> QuantRS2Result<()> {
400        if values.len() != self.total_parameters {
401            return Err(QuantRS2Error::InvalidInput(format!(
402                "Expected {} parameters, got {}",
403                self.total_parameters,
404                values.len()
405            )));
406        }
407
408        let mut offset = 0;
409        for layer in &mut self.rotation_layers {
410            let n = layer.parameters().len();
411            layer.set_parameters(&values[offset..offset + n])?;
412            offset += n;
413        }
414
415        if self.entangling_layer.parameterized {
416            self.entangling_layer.set_parameters(&values[offset..])?;
417        }
418
419        Ok(())
420    }
421
422    fn gates(&self) -> Vec<Box<dyn GateOp>> {
423        let mut gates = Vec::new();
424
425        // Apply rotation gates
426        for layer in &self.rotation_layers {
427            gates.extend(layer.gates());
428        }
429
430        // Apply entangling gates
431        gates.extend(self.entangling_layer.gates());
432
433        gates
434    }
435
436    fn compute_gradients(
437        &self,
438        state: &Array1<Complex64>,
439        loss_gradient: &Array1<Complex64>,
440    ) -> QuantRS2Result<Vec<f64>> {
441        let mut gradients = Vec::new();
442
443        for layer in &self.rotation_layers {
444            gradients.extend(layer.compute_gradients(state, loss_gradient)?);
445        }
446
447        if self.entangling_layer.parameterized {
448            gradients.extend(
449                self.entangling_layer
450                    .compute_gradients(state, loss_gradient)?,
451            );
452        }
453
454        Ok(gradients)
455    }
456
457    fn name(&self) -> &str {
458        &self.name
459    }
460}
461
462/// Hardware-efficient ansatz layer
463#[derive(Debug, Clone)]
464pub struct HardwareEfficientLayer {
465    /// Number of qubits
466    num_qubits: usize,
467    /// Single-qubit rotations
468    single_qubit_gates: Vec<RotationLayer>,
469    /// Two-qubit gates
470    entangling_gates: EntanglingLayer,
471    /// Layer name
472    name: String,
473}
474
475impl HardwareEfficientLayer {
476    /// Create a new hardware-efficient layer
477    pub fn new(num_qubits: usize) -> QuantRS2Result<Self> {
478        // Use RY and RZ rotations (common on hardware)
479        let single_qubit_gates = vec![
480            RotationLayer::uniform(num_qubits, 'Y')?,
481            RotationLayer::uniform(num_qubits, 'Z')?,
482        ];
483
484        // Use linear entanglement (nearest-neighbor)
485        let entangling_gates = EntanglingLayer::new(num_qubits, EntanglementPattern::Linear);
486
487        Ok(Self {
488            num_qubits,
489            single_qubit_gates,
490            entangling_gates,
491            name: "HardwareEfficientLayer".to_string(),
492        })
493    }
494}
495
496impl QMLLayer for HardwareEfficientLayer {
497    fn num_qubits(&self) -> usize {
498        self.num_qubits
499    }
500
501    fn parameters(&self) -> &[Parameter] {
502        // Simplified - would need proper implementation
503        &[]
504    }
505
506    fn parameters_mut(&mut self) -> &mut [Parameter] {
507        &mut []
508    }
509
510    fn gates(&self) -> Vec<Box<dyn GateOp>> {
511        let mut gates = Vec::new();
512
513        for layer in &self.single_qubit_gates {
514            gates.extend(layer.gates());
515        }
516
517        gates.extend(self.entangling_gates.gates());
518
519        gates
520    }
521
522    fn compute_gradients(
523        &self,
524        state: &Array1<Complex64>,
525        loss_gradient: &Array1<Complex64>,
526    ) -> QuantRS2Result<Vec<f64>> {
527        let mut gradients = Vec::new();
528
529        for layer in &self.single_qubit_gates {
530            gradients.extend(layer.compute_gradients(state, loss_gradient)?);
531        }
532
533        Ok(gradients)
534    }
535
536    fn name(&self) -> &str {
537        &self.name
538    }
539}
540
541/// Pooling layer for quantum convolutional neural networks
542#[derive(Debug, Clone)]
543pub struct QuantumPoolingLayer {
544    /// Number of input qubits
545    input_qubits: usize,
546    /// Number of output qubits (after pooling)
547    output_qubits: usize,
548    /// Pooling strategy
549    strategy: PoolingStrategy,
550    /// Trainable parameters (only populated for the `Parameterized` strategy).
551    parameters: Vec<Parameter>,
552    /// Layer name
553    name: String,
554}
555
556#[derive(Debug, Clone, Copy)]
557pub enum PoolingStrategy {
558    /// Trace out every other qubit
559    TraceOut,
560    /// Measure and condition
561    MeasureCondition,
562    /// Parameterized pooling
563    Parameterized,
564}
565
566impl QuantumPoolingLayer {
567    /// Create a new pooling layer.
568    ///
569    /// For the [`PoolingStrategy::Parameterized`] strategy this allocates one
570    /// trainable rotation angle per pooled pair; the other strategies are
571    /// non-unitary (partial trace / measurement) and carry no parameters.
572    pub fn new(input_qubits: usize, strategy: PoolingStrategy) -> Self {
573        let output_qubits = input_qubits / 2;
574
575        let parameters = match strategy {
576            PoolingStrategy::Parameterized => (0..output_qubits)
577                .map(|pair| Parameter {
578                    name: format!("pool_{pair}"),
579                    value: 0.0,
580                    bounds: Some((-PI, PI)),
581                })
582                .collect(),
583            PoolingStrategy::TraceOut | PoolingStrategy::MeasureCondition => Vec::new(),
584        };
585
586        Self {
587            input_qubits,
588            output_qubits,
589            strategy,
590            parameters,
591            name: format!("QuantumPoolingLayer_{strategy:?}"),
592        }
593    }
594}
595
596impl QMLLayer for QuantumPoolingLayer {
597    fn num_qubits(&self) -> usize {
598        self.input_qubits
599    }
600
601    fn parameters(&self) -> &[Parameter] {
602        &self.parameters
603    }
604
605    fn parameters_mut(&mut self) -> &mut [Parameter] {
606        &mut self.parameters
607    }
608
609    fn gates(&self) -> Vec<Box<dyn GateOp>> {
610        match self.strategy {
611            // Parameterized pooling: a controlled-RZ entangles each odd qubit
612            // (2*pair + 1) into the retained even qubit (2*pair) before the odd
613            // qubit is discarded, with a trainable rotation angle per pair.
614            PoolingStrategy::Parameterized => self
615                .parameters
616                .iter()
617                .enumerate()
618                .map(|(pair, param)| {
619                    let retained = QubitId((2 * pair) as u32);
620                    let discarded = QubitId((2 * pair + 1) as u32);
621                    Box::new(crate::gate::multi::CRZ {
622                        control: discarded,
623                        target: retained,
624                        theta: param.value,
625                    }) as Box<dyn GateOp>
626                })
627                .collect(),
628            // TraceOut / MeasureCondition are non-unitary operations (partial
629            // trace and mid-circuit measurement); they are realised by the
630            // surrounding circuit executor reducing the register from
631            // `input_qubits` to `output_qubits` rather than by unitary gates, so
632            // there are no gates to emit here. This empty result is the
633            // mathematically-correct representation, not a placeholder.
634            PoolingStrategy::TraceOut | PoolingStrategy::MeasureCondition => Vec::new(),
635        }
636    }
637
638    fn compute_gradients(
639        &self,
640        state: &Array1<Complex64>,
641        loss_gradient: &Array1<Complex64>,
642    ) -> QuantRS2Result<Vec<f64>> {
643        // Only the parameterized strategy has trainable parameters; the
644        // non-unitary strategies have none, so their gradient is empty.
645        if self.parameters.is_empty() {
646            return Ok(Vec::new());
647        }
648
649        // Parameter-shift rule for the CRZ pooling rotations: for each pair,
650        // ∂⟨O⟩/∂θ = ½[f(θ+π/2) − f(θ−π/2)], approximated against the supplied
651        // upstream loss gradient on the retained-qubit overlap.
652        let num_qubits = self.input_qubits.max(1);
653        let dim = 1usize << num_qubits;
654        if state.len() != dim || loss_gradient.len() != dim {
655            return Err(QuantRS2Error::InvalidInput(format!(
656                "pooling gradient expects state/loss vectors of length {dim}"
657            )));
658        }
659
660        let shift = PI / 2.0;
661        let mut gradients = Vec::with_capacity(self.parameters.len());
662        for (pair, param) in self.parameters.iter().enumerate() {
663            let retained = QubitId((2 * pair) as u32);
664            let discarded = QubitId((2 * pair + 1) as u32);
665
666            let mut state_plus = state.clone();
667            let gate_plus = crate::gate::multi::CRZ {
668                control: discarded,
669                target: retained,
670                theta: param.value + shift,
671            };
672            crate::qml::simulator::apply_gate(&mut state_plus, &gate_plus)?;
673
674            let mut state_minus = state.clone();
675            let gate_minus = crate::gate::multi::CRZ {
676                control: discarded,
677                target: retained,
678                theta: param.value - shift,
679            };
680            crate::qml::simulator::apply_gate(&mut state_minus, &gate_minus)?;
681
682            // ⟨loss | (|ψ+⟩ − |ψ−⟩)/2⟩, real part is the parameter gradient.
683            let grad: f64 = loss_gradient
684                .iter()
685                .zip(state_plus.iter().zip(state_minus.iter()))
686                .map(|(g, (p, m))| (g.conj() * (p - m)).re)
687                .sum::<f64>()
688                / 2.0;
689            gradients.push(grad);
690        }
691
692        Ok(gradients)
693    }
694
695    fn name(&self) -> &str {
696        &self.name
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    #[test]
705    fn test_rotation_layer() {
706        let layer = RotationLayer::uniform(3, 'X').expect("rotation layer creation should succeed");
707        assert_eq!(layer.num_qubits(), 3);
708        assert_eq!(layer.parameters().len(), 3);
709
710        let gates = layer.gates();
711        assert_eq!(gates.len(), 3);
712    }
713
714    #[test]
715    fn test_entangling_layer() {
716        let layer = EntanglingLayer::new(4, EntanglementPattern::Linear);
717        assert_eq!(layer.num_qubits(), 4);
718
719        let gates = layer.gates();
720        assert_eq!(gates.len(), 3); // 3 CNOT gates for linear pattern
721    }
722
723    #[test]
724    fn test_strongly_entangling_layer() {
725        let layer = StronglyEntanglingLayer::new(2, EntanglementPattern::Full)
726            .expect("strongly entangling layer creation should succeed");
727        assert_eq!(layer.num_qubits(), 2);
728
729        let gates = layer.gates();
730        assert_eq!(gates.len(), 7); // 6 rotation gates + 1 CNOT
731    }
732
733    #[test]
734    fn test_hardware_efficient_layer() {
735        let layer = HardwareEfficientLayer::new(3)
736            .expect("hardware efficient layer creation should succeed");
737        assert_eq!(layer.num_qubits(), 3);
738
739        let gates = layer.gates();
740        assert_eq!(gates.len(), 8); // 6 rotation gates + 2 CNOTs
741    }
742
743    #[test]
744    fn test_parameterized_entangling_layer_uses_crz_not_cnot() {
745        // The parameterized branch must emit controlled-RZ gates that depend on
746        // the trainable parameters (the old fabrication emitted parameterless
747        // CNOTs and ignored the parameters).
748        let mut layer = EntanglingLayer::parameterized(3, EntanglementPattern::Linear);
749        let params = layer.parameters_mut();
750        for (i, p) in params.iter_mut().enumerate() {
751            p.value = 0.5 * (i as f64 + 1.0);
752        }
753        let gates = layer.gates();
754        assert!(!gates.is_empty());
755        for gate in &gates {
756            assert_eq!(
757                gate.name(),
758                "CRZ",
759                "parameterized entangling layer must emit CRZ gates"
760            );
761        }
762        // A CRZ(θ≠0) matrix must differ from a CNOT matrix.
763        let crz_matrix = gates[0].matrix().expect("crz matrix");
764        let cnot = crate::gate::multi::CNOT {
765            control: QubitId(0),
766            target: QubitId(1),
767        };
768        let cnot_matrix = cnot.matrix().expect("cnot matrix");
769        let differs = crz_matrix
770            .iter()
771            .zip(cnot_matrix.iter())
772            .any(|(a, b)| (a - b).norm() > 1e-9);
773        assert!(differs, "CRZ gate must not equal CNOT");
774    }
775
776    #[test]
777    fn test_parameterized_pooling_layer_emits_gates() {
778        // Parameterized pooling must produce trainable gates (the old impl
779        // returned an empty gate list regardless of strategy).
780        let mut layer = QuantumPoolingLayer::new(4, PoolingStrategy::Parameterized);
781        assert_eq!(layer.parameters().len(), 2); // 4 input -> 2 pooled pairs
782        for (i, p) in layer.parameters_mut().iter_mut().enumerate() {
783            p.value = 0.3 * (i as f64 + 1.0);
784        }
785        let gates = layer.gates();
786        assert_eq!(gates.len(), 2, "one pooling gate per pooled pair");
787        for gate in &gates {
788            assert_eq!(gate.name(), "CRZ");
789        }
790    }
791
792    #[test]
793    fn test_traceout_pooling_layer_has_no_gates_legit() {
794        // Non-unitary pooling strategies correctly carry no gates / parameters.
795        let layer = QuantumPoolingLayer::new(4, PoolingStrategy::TraceOut);
796        assert!(layer.parameters().is_empty());
797        assert!(layer.gates().is_empty());
798        assert!(layer
799            .compute_gradients(&Array1::zeros(16), &Array1::zeros(16))
800            .expect("gradients")
801            .is_empty());
802    }
803}