use crate::error::{QuantumError, QuantumResult};
use crate::gates::controlled::apply_cnot;
use crate::gates::parametric::{gate_ry, gate_rz};
use crate::pauli::expval::expectation_value;
use crate::pauli::hamiltonian::Hamiltonian;
use crate::pauli::pauli_string::PauliOp;
use crate::statevec::apply_1q::apply_1q_inplace;
use crate::statevec::state::StateVector;
pub const CONV_PARAMS: usize = 6;
pub const POOL_PARAMS: usize = 6;
#[derive(Debug, Clone)]
pub struct Qcnn {
n_qubits: usize,
n_stages: usize,
}
impl Qcnn {
pub fn new(n_qubits: usize) -> QuantumResult<Self> {
if !(2..=16).contains(&n_qubits) || !n_qubits.is_power_of_two() {
return Err(QuantumError::InvalidQubitCount { n: n_qubits });
}
let n_stages = n_qubits.trailing_zeros() as usize;
Ok(Self { n_qubits, n_stages })
}
#[must_use]
pub fn n_qubits(&self) -> usize {
self.n_qubits
}
#[must_use]
pub fn n_params(&self) -> usize {
self.n_stages * (CONV_PARAMS + POOL_PARAMS)
}
fn two_qubit_block(sv: &mut StateVector, a: usize, b: usize, p: &[f32]) -> QuantumResult<()> {
apply_1q_inplace(sv, a, &gate_ry(p[0]))?;
apply_1q_inplace(sv, a, &gate_rz(p[1]))?;
apply_1q_inplace(sv, b, &gate_ry(p[2]))?;
apply_1q_inplace(sv, b, &gate_rz(p[3]))?;
apply_cnot(sv, a, b)?;
apply_1q_inplace(sv, a, &gate_ry(p[4]))?;
apply_1q_inplace(sv, b, &gate_ry(p[5]))?;
Ok(())
}
pub fn forward(&self, input: &StateVector, params: &[f32]) -> QuantumResult<f32> {
if input.n_qubits != self.n_qubits {
return Err(QuantumError::DimensionMismatch {
expected: self.n_qubits,
got: input.n_qubits,
});
}
if params.len() != self.n_params() {
return Err(QuantumError::DimensionMismatch {
expected: self.n_params(),
got: params.len(),
});
}
let mut sv = input.clone();
let mut active: Vec<usize> = (0..self.n_qubits).collect();
let block = CONV_PARAMS + POOL_PARAMS;
for stage in 0..self.n_stages {
let conv_p = ¶ms[stage * block..stage * block + CONV_PARAMS];
let pool_p =
¶ms[stage * block + CONV_PARAMS..stage * block + CONV_PARAMS + POOL_PARAMS];
let mut i = 0;
while i + 1 < active.len() {
Self::two_qubit_block(&mut sv, active[i], active[i + 1], conv_p)?;
i += 2;
}
if active.len() > 2 {
let mut j = 1;
while j + 1 < active.len() {
Self::two_qubit_block(&mut sv, active[j], active[j + 1], conv_p)?;
j += 2;
}
}
let mut kept: Vec<usize> = Vec::with_capacity(active.len() / 2);
let mut k = 0;
while k + 1 < active.len() {
let keep = active[k];
let discard = active[k + 1];
Self::two_qubit_block(&mut sv, keep, discard, pool_p)?;
kept.push(keep);
k += 2;
}
if active.len() % 2 == 1 {
kept.push(active[active.len() - 1]);
}
active = kept;
}
let survivor = active[0];
let mut ops = vec![PauliOp::I; self.n_qubits];
ops[survivor] = PauliOp::Z;
let mut ham = Hamiltonian::new();
ham.add_term(1.0, ops);
expectation_value(&sv, &ham)
}
pub fn gradient(&self, input: &StateVector, params: &[f32]) -> QuantumResult<Vec<f32>> {
let shift = std::f32::consts::FRAC_PI_2;
let mut grad = vec![0.0_f32; params.len()];
for p in 0..params.len() {
let mut pp = params.to_vec();
let mut pm = params.to_vec();
pp[p] += shift;
pm[p] -= shift;
let fp = self.forward(input, &pp)?;
let fm = self.forward(input, &pm)?;
grad[p] = 0.5 * (fp - fm);
}
Ok(grad)
}
pub fn train(
&self,
inputs: &[StateVector],
labels: &[f32],
params: &mut [f32],
iters: usize,
lr: f32,
) -> QuantumResult<Vec<f32>> {
if inputs.is_empty() {
return Err(QuantumError::EmptyInput);
}
if labels.len() != inputs.len() {
return Err(QuantumError::DimensionMismatch {
expected: inputs.len(),
got: labels.len(),
});
}
if params.len() != self.n_params() {
return Err(QuantumError::DimensionMismatch {
expected: self.n_params(),
got: params.len(),
});
}
let mse = |p: &[f32], me: &Self| -> QuantumResult<f32> {
let mut acc = 0.0_f32;
for (x, &y) in inputs.iter().zip(labels.iter()) {
let s = me.forward(x, p)?;
acc += (s - y) * (s - y);
}
Ok(acc / inputs.len() as f32)
};
let mut history = Vec::with_capacity(iters + 1);
history.push(mse(params, self)?);
for _ in 0..iters {
let mut grad = vec![0.0_f32; params.len()];
for (x, &y) in inputs.iter().zip(labels.iter()) {
let s = self.forward(x, params)?;
let g = self.gradient(x, params)?;
let coeff = 2.0 * (s - y) / inputs.len() as f32;
for (gg, gi) in grad.iter_mut().zip(g.iter()) {
*gg += coeff * gi;
}
}
for (p, g) in params.iter_mut().zip(grad.iter()) {
*p -= lr * g;
}
history.push(mse(params, self)?);
}
Ok(history)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedding::angle::angle_embedding;
use crate::handle::LcgRng;
#[test]
fn rejects_non_power_of_two() {
assert!(Qcnn::new(3).is_err());
assert!(Qcnn::new(6).is_err());
assert!(Qcnn::new(1).is_err());
assert!(Qcnn::new(4).is_ok());
assert!(Qcnn::new(8).is_ok());
}
#[test]
fn param_count_matches_stages() {
let q = Qcnn::new(4).expect("valid");
assert_eq!(q.n_params(), 24);
let q8 = Qcnn::new(8).expect("valid");
assert_eq!(q8.n_params(), 36);
}
#[test]
fn forward_score_in_unit_interval() {
let q = Qcnn::new(4).expect("valid");
let mut rng = LcgRng::new(7);
let params: Vec<f32> = (0..q.n_params()).map(|_| rng.next_normal() * 0.3).collect();
let input = angle_embedding(&[0.5, 1.0, 0.2, 0.8]).expect("embed");
let s = q.forward(&input, ¶ms).expect("forward");
assert!((-1.0001..=1.0001).contains(&s), "score={s}");
}
#[test]
fn identity_params_pass_through_z_expectation() {
let q = Qcnn::new(4).expect("valid");
let params = vec![0.0_f32; q.n_params()];
let input = StateVector::new_zero_state(4).expect("zero");
let s = q.forward(&input, ¶ms).expect("forward");
assert!((s - 1.0).abs() < 1e-4, "score={s}");
}
#[test]
fn gradient_has_correct_length_and_is_finite() {
let q = Qcnn::new(4).expect("valid");
let mut rng = LcgRng::new(3);
let params: Vec<f32> = (0..q.n_params()).map(|_| rng.next_normal() * 0.5).collect();
let input = angle_embedding(&[0.1, 0.9, -0.3, 0.4]).expect("embed");
let g = q.gradient(&input, ¶ms).expect("grad");
assert_eq!(g.len(), q.n_params());
assert!(g.iter().all(|x| x.is_finite()));
}
#[test]
fn training_reduces_loss_on_separable_data() {
let q = Qcnn::new(4).expect("valid");
let class_a = angle_embedding(&[0.1, 0.1, 0.1, 0.1]).expect("a");
let class_b = angle_embedding(&[3.0, 3.0, 3.0, 3.0]).expect("b");
let inputs = vec![class_a.clone(), class_b.clone(), class_a, class_b];
let labels = vec![1.0_f32, -1.0, 1.0, -1.0];
let mut rng = LcgRng::new(2024);
let mut params: Vec<f32> = (0..q.n_params()).map(|_| rng.next_normal() * 0.2).collect();
let history = q
.train(&inputs, &labels, &mut params, 30, 0.25)
.expect("train");
let first = history[0];
let last = *history.last().expect("history");
assert!(last < first, "loss did not decrease: {first} → {last}");
}
}