Skip to main content

axon/
quant.rs

1//! v2.4.0 — the `QuantBackend` port + the OSS reference simulator.
2//!
3//! This is the OSS half of the `quant` cognitive primitive's RUNTIME (the type
4//! discipline shipped in v2.4.0–d.2 on the frontend). It defines:
5//!
6//!   - [`QuantBackend`] — the **port** (D1). Enterprise mounts the production
7//! QuIDD / VRAM / QPU engine behind this same trait (v2.4.0–i); the OSS crate
8//!     ships only the reference implementation below.
9//!   - [`ReferenceSimulator`] — a **genuinely usable** dense-statevector
10//!     simulator over `f64` complex amplitudes, hard-capped at **n ≤ 10 qubits**
11//!     (D = 2¹⁰ = 1024 amplitudes — the paper's `DensityMatrix[1024]` boundary).
12//!     It actually executes small `quant` blocks on the CPU and serves as the
13//! differential-test ORACLE for the v2.4.0 enterprise engine. Above the cap
14//!     it returns [`QuantError::CapacityExceeded`] (`axon-E0783`) — never a
15//!     silent OOM or a degraded result (D1, Option A).
16//!
17//! The reference simulator uses exact `f64` (NOT the enterprise Q32.32 /
18//! purification arithmetic — that is v2.4.0). It is the oracle, not the
19//! production path.
20//!
21//! **Norm invariant (D2, deferred from v2.4.0):** amplitude encoding asserts
22//! the input carrier has unit L2 norm `‖x‖₂ = 1` ([`QuantError::NotNormalized`])
23//! — the numeric realization the type system could not prove statically.
24
25
26// ── Minimal complex scalar (no external dep) ────────────────────────────────
27
28/// A complex number `re + im·i` over `f64`. Just enough algebra for
29/// statevector simulation (no `num-complex` dependency).
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct C {
32    pub re: f64,
33    pub im: f64,
34}
35
36impl C {
37    pub const ZERO: C = C { re: 0.0, im: 0.0 };
38    pub const ONE: C = C { re: 1.0, im: 0.0 };
39    /// The imaginary unit `i`.
40    pub const I: C = C { re: 0.0, im: 1.0 };
41
42    pub fn new(re: f64, im: f64) -> C {
43        C { re, im }
44    }
45    pub fn real(re: f64) -> C {
46        C { re, im: 0.0 }
47    }
48    pub fn conj(self) -> C {
49        C { re: self.re, im: -self.im }
50    }
51    /// `|z|²` — the squared modulus.
52    pub fn norm_sqr(self) -> f64 {
53        self.re * self.re + self.im * self.im
54    }
55}
56
57impl std::ops::Add for C {
58    type Output = C;
59    fn add(self, o: C) -> C {
60        C { re: self.re + o.re, im: self.im + o.im }
61    }
62}
63impl std::ops::Mul for C {
64    type Output = C;
65    fn mul(self, o: C) -> C {
66        // (a+bi)(c+di) = (ac − bd) + (ad + bc)i
67        C {
68            re: self.re * o.re - self.im * o.im,
69            im: self.re * o.im + self.im * o.re,
70        }
71    }
72}
73impl std::ops::Neg for C {
74    type Output = C;
75    fn neg(self) -> C {
76        C { re: -self.re, im: -self.im }
77    }
78}
79
80// ── Public surface types ────────────────────────────────────────────────────
81
82/// The encoding scheme that maps a classical real vector into a Hilbert-space
83/// state (paper section 3.1; plan D2).
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum EncodingScheme {
86    /// d features → n = ⌈log₂ d⌉ qubits (exponential compression); requires a
87    /// unit-norm input.
88    Amplitude,
89    /// d features → n = d qubits (one Ry rotation per feature); O(1) depth,
90    /// robust to scale noise — no normalization requirement.
91    Angle,
92}
93
94/// A pure quantum state — a dense statevector of `2ⁿ` complex amplitudes.
95#[derive(Debug, Clone, PartialEq)]
96pub struct StateVector {
97    pub n: usize,
98    pub amps: Vec<C>,
99}
100
101impl StateVector {
102    /// The squared L2 norm `⟨ψ|ψ⟩` (should be ≈ 1 for a valid pure state).
103    pub fn norm_sqr(&self) -> f64 {
104        self.amps.iter().map(|a| a.norm_sqr()).sum()
105    }
106}
107
108/// One layer of the hardware-efficient variational ansatz (paper section 3.2):
109/// a single-qubit `Ry(θ)·Rz(φ)` rotation per qubit, followed by a linear CNOT
110/// entanglement chain (`U_ent`). `ry`/`rz` carry one angle per qubit.
111#[derive(Debug, Clone)]
112pub struct RotationLayer {
113    pub ry: Vec<f64>,
114    pub rz: Vec<f64>,
115}
116
117/// A parametric circuit `U(θ) = ∏ₗ (⊗ₖ Ry·Rz) · U_ent` (paper section 3.2).
118#[derive(Debug, Clone, Default)]
119pub struct VariationalCircuit {
120    pub layers: Vec<RotationLayer>,
121}
122
123/// A Pauli-sum observable `M = Σ cₖ Pₖ` (the runtime mirror of the frontend
124/// `observable` declaration). Hermitian by construction (real coefficients).
125#[derive(Debug, Clone, Default)]
126pub struct PauliSum {
127    /// `(coefficient, pauli_string)` — the string is over `{I, X, Y, Z}`, one
128    /// char per qubit (char j ↦ qubit j).
129    pub terms: Vec<(f64, String)>,
130}
131
132/// The closed catalogue of runtime errors. `code()` returns the stable
133/// machine-readable diagnostic id.
134#[derive(Debug, Clone, PartialEq)]
135pub enum QuantError {
136    /// `axon-E0783` — the requested register exceeds the backend capacity.
137    CapacityExceeded { requested: usize, cap: usize },
138    /// Amplitude encoding requires a unit-norm input (‖x‖₂ = 1).
139    NotNormalized { norm: f64 },
140    /// A shape mismatch (empty input, wrong rotation-vector / Pauli-string
141    /// length for the register).
142    DimensionMismatch { detail: String },
143    /// A Pauli string carries a char outside `{I, X, Y, Z}`.
144    BadPauli { pauli: String, bad: char },
145}
146
147impl QuantError {
148    pub fn code(&self) -> &'static str {
149        match self {
150            QuantError::CapacityExceeded { .. } => "axon-E0783",
151            QuantError::NotNormalized { .. } => "axon-E0788",
152            QuantError::DimensionMismatch { .. } => "axon-E0789",
153            QuantError::BadPauli { .. } => "axon-E0785",
154        }
155    }
156}
157
158impl std::fmt::Display for QuantError {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            QuantError::CapacityExceeded { requested, cap } => write!(
162                f,
163                "axon-E0783 quant: capacity exceeded — requested {requested} qubits (D = 2^{requested}), \
164                 the OSS reference simulator caps n ≤ {cap}; use an enterprise QuantBackend for larger registers."
165            ),
166            QuantError::NotNormalized { norm } => write!(
167                f,
168                "axon-E0788 quant: amplitude encoding requires a unit-norm input (‖x‖₂ = 1), got ‖x‖₂ = {norm:.6}."
169            ),
170            QuantError::DimensionMismatch { detail } => {
171                write!(f, "axon-E0789 quant: dimension mismatch — {detail}")
172            }
173            QuantError::BadPauli { pauli, bad } => write!(
174                f,
175                "axon-E0785 quant: Pauli string '{pauli}' contains '{bad}' — the closed alphabet is {{I, X, Y, Z}}."
176            ),
177        }
178    }
179}
180
181// ── The port ────────────────────────────────────────────────────────────────
182
183/// v2.4.0 — the algebraic-backend **port** (D1). The OSS crate ships the
184/// [`ReferenceSimulator`]; the enterprise QuIDD / VRAM / QPU engine implements
185/// the same trait (v2.4.0–i). A `quant` block's pipeline is `encode → evolve →
186/// {measure | kernel}`.
187pub trait QuantBackend {
188    /// Maximum register width (qubits) this backend can realise.
189    fn capacity(&self) -> usize;
190    /// Project a classical real vector into a Hilbert-space state (section 3.1).
191    fn encode(&self, x: &[f64], scheme: EncodingScheme) -> Result<StateVector, QuantError>;
192    /// Evolve a state under a parametric circuit `U(θ)` (section 3.2).
193    fn evolve(&self, state: StateVector, circuit: &VariationalCircuit) -> Result<StateVector, QuantError>;
194    /// Expectation `E(θ) = ⟨ψ| M |ψ⟩` of a Pauli-sum observable (real, since M is
195    /// Hermitian).
196    fn measure(&self, state: &StateVector, observable: &PauliSum) -> Result<f64, QuantError>;
197    /// Quantum-kernel overlap `K = |⟨ψ_a|ψ_b⟩|²` (section 3.4, fidelity kernel).
198    fn kernel(&self, a: &StateVector, b: &StateVector) -> Result<f64, QuantError>;
199}
200
201/// v2.89.0 — **the deploy gate.** Which declared observables this backend
202/// cannot realise.
203///
204/// The sibling of [`crate::warden::unsupported_scope_depths`], and it exists
205/// for the same reason: v2.67.0 already refuses an over-capacity register with
206/// `axon-E0783` rather than truncating it silently — but it refuses at the
207/// `yield`, after the block's body and every step before it has run.
208///
209/// `capacity()` is on the trait and the observable catalog is compiled, so both
210/// halves are available at deploy. An `observable` declaring more qubits than
211/// the mounted simulator can hold is a program that cannot run here, and saying
212/// so before it starts costs nothing.
213///
214/// Empty ⇒ every declared observable fits.
215pub fn unsupported_observables(
216    backend: &dyn QuantBackend,
217    observables: &[crate::ir_nodes::IRObservable],
218) -> Vec<String> {
219    let cap = backend.capacity();
220    observables
221        .iter()
222        .filter_map(|o| {
223            // `qubits` is optional in the grammar — an observable that declares
224            // none is inferred at dispatch and is not this gate's business.
225            let n = usize::try_from(o.qubits?).ok()?;
226            (n > cap).then(|| {
227                format!(
228                    "observable `{}` declares {n} qubits, above the mounted simulator's \
229                     capacity of {cap}. Refusing at DEPLOY rather than at the `yield`: a \
230                     register that cannot be realised is not a late failure to discover. \
231                     The OSS reference simulator is capped at {} qubits (D = 2^{} \
232                     amplitudes); larger registers require the enterprise backend.",
233                    o.name, OSS_QUBIT_CAP, OSS_QUBIT_CAP
234                )
235            })
236        })
237        .collect()
238}
239
240// ── The OSS reference simulator ─────────────────────────────────────────────
241
242/// The default capacity cap for the OSS reference simulator: n ≤ 10 ⇒ D ≤ 1024
243/// (the paper's `DensityMatrix[1024]` boundary). Above this, callers must use an
244/// enterprise [`QuantBackend`].
245pub const OSS_QUBIT_CAP: usize = 10;
246
247/// Tolerance for the unit-norm assertion on amplitude-encoding input.
248const NORM_TOL: f64 = 1e-9;
249
250/// v2.4.0 — a usable dense-statevector simulator over `f64` complex
251/// amplitudes, capped at [`OSS_QUBIT_CAP`].
252#[derive(Debug, Clone)]
253pub struct ReferenceSimulator {
254    cap: usize,
255}
256
257impl Default for ReferenceSimulator {
258    fn default() -> Self {
259        ReferenceSimulator { cap: OSS_QUBIT_CAP }
260    }
261}
262
263impl ReferenceSimulator {
264    pub fn new() -> Self {
265        Self::default()
266    }
267
268    /// ⌈log₂ d⌉ via integer doubling (avoids float edge cases on exact powers).
269    fn amplitude_qubits(d: usize) -> usize {
270        let mut n = 0usize;
271        while (1usize << n) < d {
272            n += 1;
273        }
274        n
275    }
276
277    /// Apply a single-qubit gate `g` (row-major 2×2) to qubit `q`.
278    fn apply_1q(amps: &mut [C], q: usize, g: [[C; 2]; 2]) {
279        let bit = 1usize << q;
280        for i in 0..amps.len() {
281            if i & bit == 0 {
282                let j = i | bit;
283                let a0 = amps[i];
284                let a1 = amps[j];
285                amps[i] = g[0][0] * a0 + g[0][1] * a1;
286                amps[j] = g[1][0] * a0 + g[1][1] * a1;
287            }
288        }
289    }
290
291    /// v2.23.0 — **data re-uploading**: interleave an angle-encoding of `x`
292    /// with a fixed entangling layer, `layers` times. For `layers ≥ 2` the data
293    /// `x` re-enters the circuit, so `⟨ψ(x)|ψ(y)⟩` is NO LONGER a quadratic form in
294    /// `x` (it becomes a Fourier series in the data — Schuld 2021,
295    /// `arXiv:2008.08605`). This is the ONLY provable escape from the amplitude+Pauli
296    /// quadratic bound (v2.23.0 / the Havlíček route). `layers = 1` reduces to a
297    /// single angle layer (no re-uploading). HONEST: escaping the bound does NOT
298    /// guarantee advantage on classical text — the v2.23.0 Advantage Witness still
299    /// gates it.
300    pub fn reupload_encode(&self, x: &[f64], layers: usize) -> Result<StateVector, QuantError> {
301        let n = x.len();
302        if n == 0 {
303            return Err(QuantError::DimensionMismatch {
304                detail: "empty input vector".to_string(),
305            });
306        }
307        if n > self.cap {
308            return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
309        }
310        // Start in |0…0⟩.
311        let mut amps = vec![C::ZERO; 1usize << n];
312        amps[0] = C::real(1.0);
313        for _ in 0..layers.max(1) {
314            // Data re-upload layer: Ry(xⱼ) on each qubit (the data enters here).
315            for (q, &angle) in x.iter().enumerate() {
316                let ry = [
317                    [C::real((angle / 2.0).cos()), C::real(-(angle / 2.0).sin())],
318                    [C::real((angle / 2.0).sin()), C::real((angle / 2.0).cos())],
319                ];
320                Self::apply_1q(&mut amps, q, ry);
321            }
322            // Fixed entangling layer (the data-independent "trainable" block; the
323            // reference uses a deterministic CNOT chain so the kernel is reproducible).
324            for q in 0..n.saturating_sub(1) {
325                Self::apply_cnot(&mut amps, q, q + 1);
326            }
327        }
328        Ok(StateVector { n, amps })
329    }
330
331    /// Apply CNOT(control `c`, target `t`).
332    fn apply_cnot(amps: &mut [C], c: usize, t: usize) {
333        let cb = 1usize << c;
334        let tb = 1usize << t;
335        for i in 0..amps.len() {
336            if i & cb != 0 && i & tb == 0 {
337                amps.swap(i, i | tb);
338            }
339        }
340    }
341
342    /// Apply one Pauli char to qubit `q` of `amps` in place.
343    fn apply_pauli(amps: &mut [C], q: usize, p: char) -> Result<(), char> {
344        let bit = 1usize << q;
345        match p {
346            'I' => {}
347            'X' => {
348                for i in 0..amps.len() {
349                    if i & bit == 0 {
350                        amps.swap(i, i | bit);
351                    }
352                }
353            }
354            'Z' => {
355                for amp in amps.iter_mut().enumerate().filter(|(i, _)| i & bit != 0).map(|(_, a)| a) {
356                    *amp = -*amp;
357                }
358            }
359            'Y' => {
360                // Y|0⟩ = i|1⟩, Y|1⟩ = −i|0⟩ ⇒ new[0] = −i·a1, new[1] = i·a0.
361                for i in 0..amps.len() {
362                    if i & bit == 0 {
363                        let j = i | bit;
364                        let a0 = amps[i];
365                        let a1 = amps[j];
366                        amps[i] = (-C::I) * a1;
367                        amps[j] = C::I * a0;
368                    }
369                }
370            }
371            other => return Err(other),
372        }
373        Ok(())
374    }
375
376    /// ⟨a|b⟩ — the complex inner product.
377    fn inner(a: &[C], b: &[C]) -> C {
378        a.iter()
379            .zip(b.iter())
380            .fold(C::ZERO, |acc, (x, y)| acc + x.conj() * *y)
381    }
382}
383
384impl QuantBackend for ReferenceSimulator {
385    fn capacity(&self) -> usize {
386        self.cap
387    }
388
389    fn encode(&self, x: &[f64], scheme: EncodingScheme) -> Result<StateVector, QuantError> {
390        if x.is_empty() {
391            return Err(QuantError::DimensionMismatch {
392                detail: "empty input vector".to_string(),
393            });
394        }
395        match scheme {
396            EncodingScheme::Amplitude => {
397                let n = Self::amplitude_qubits(x.len());
398                if n > self.cap {
399                    return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
400                }
401                // Norm invariant (D2): amplitude encoding requires ‖x‖₂ = 1.
402                let norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
403                if (norm - 1.0).abs() > NORM_TOL {
404                    return Err(QuantError::NotNormalized { norm });
405                }
406                let mut amps = vec![C::ZERO; 1usize << n];
407                for (i, &v) in x.iter().enumerate() {
408                    amps[i] = C::real(v);
409                }
410                Ok(StateVector { n, amps })
411            }
412            EncodingScheme::Angle => {
413                let n = x.len();
414                if n > self.cap {
415                    return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
416                }
417                // Product state ⊗ⱼ [cos(xⱼ/2), sin(xⱼ/2)] — inherently unit-norm.
418                let mut amps = vec![C::ZERO; 1usize << n];
419                for (idx, amp) in amps.iter_mut().enumerate() {
420                    let mut coeff = 1.0f64;
421                    for (q, &angle) in x.iter().enumerate() {
422                        let bit = (idx >> q) & 1;
423                        coeff *= if bit == 0 { (angle / 2.0).cos() } else { (angle / 2.0).sin() };
424                    }
425                    *amp = C::real(coeff);
426                }
427                Ok(StateVector { n, amps })
428            }
429        }
430    }
431
432    fn evolve(&self, mut state: StateVector, circuit: &VariationalCircuit) -> Result<StateVector, QuantError> {
433        let n = state.n;
434        for (li, layer) in circuit.layers.iter().enumerate() {
435            if layer.ry.len() != n || layer.rz.len() != n {
436                return Err(QuantError::DimensionMismatch {
437                    detail: format!(
438                        "layer {li} has {}/{} rotation angles but the register has {n} qubits",
439                        layer.ry.len(),
440                        layer.rz.len()
441                    ),
442                });
443            }
444            // Single-qubit Ry(θ)·Rz(φ) on each qubit.
445            for q in 0..n {
446                let ty = layer.ry[q];
447                let ry = [
448                    [C::real((ty / 2.0).cos()), C::real(-(ty / 2.0).sin())],
449                    [C::real((ty / 2.0).sin()), C::real((ty / 2.0).cos())],
450                ];
451                Self::apply_1q(&mut state.amps, q, ry);
452                let tz = layer.rz[q];
453                let rz = [
454                    [C::new((tz / 2.0).cos(), -(tz / 2.0).sin()), C::ZERO],
455                    [C::ZERO, C::new((tz / 2.0).cos(), (tz / 2.0).sin())],
456                ];
457                Self::apply_1q(&mut state.amps, q, rz);
458            }
459            // Linear CNOT entanglement chain (U_ent).
460            for q in 0..n.saturating_sub(1) {
461                Self::apply_cnot(&mut state.amps, q, q + 1);
462            }
463        }
464        Ok(state)
465    }
466
467    fn measure(&self, state: &StateVector, observable: &PauliSum) -> Result<f64, QuantError> {
468        let n = state.n;
469        let mut expectation = 0.0f64;
470        for (coeff, pauli) in &observable.terms {
471            if pauli.chars().count() != n {
472                return Err(QuantError::DimensionMismatch {
473                    detail: format!(
474                        "Pauli string '{pauli}' spans {} qubit(s) but the state has {n}",
475                        pauli.chars().count()
476                    ),
477                });
478            }
479            // φ = Pₖ|ψ⟩, then ⟨ψ|φ⟩ (real part — Pₖ is Hermitian).
480            let mut phi = state.amps.clone();
481            for (q, p) in pauli.chars().enumerate() {
482                Self::apply_pauli(&mut phi, q, p)
483                    .map_err(|bad| QuantError::BadPauli { pauli: pauli.clone(), bad })?;
484            }
485            expectation += coeff * Self::inner(&state.amps, &phi).re;
486        }
487        Ok(expectation)
488    }
489
490    fn kernel(&self, a: &StateVector, b: &StateVector) -> Result<f64, QuantError> {
491        if a.n != b.n {
492            return Err(QuantError::DimensionMismatch {
493                detail: format!("kernel operands span {} vs {} qubits", a.n, b.n),
494            });
495        }
496        Ok(Self::inner(&a.amps, &b.amps).norm_sqr())
497    }
498}
499
500impl ReferenceSimulator {
501    /// v2.23.0 — multi-copy **polynomial kernel** `(xᵀy)^d`. Loading `d` copies
502    /// of the state gives `⟨ψ(x)|ψ(y)⟩^d = (xᵀy)^d` for amplitude encoding (Schuld
503    /// & Killoran). It reaches *beyond* cosine (degree 1) — but it is still a
504    /// CLASSICAL polynomial kernel (no quantum advantage), so like every fixed
505    /// amplitude map it is gated by the v2.23.0 Advantage Witness. `degree = 0` is
506    /// the constant kernel `1`; `degree = 1` is the linear/cosine kernel `xᵀy`.
507    pub fn polynomial_kernel(a: &StateVector, b: &StateVector, degree: u32) -> Result<f64, QuantError> {
508        if a.n != b.n {
509            return Err(QuantError::DimensionMismatch {
510                detail: format!("kernel operands span {} vs {} qubits", a.n, b.n),
511            });
512        }
513        // Real amplitudes ⇒ the inner product is real (= xᵀy); raise to the degree.
514        Ok(Self::inner(&a.amps, &b.amps).re.powi(degree as i32))
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use std::f64::consts::PI;
522
523    fn approx(a: f64, b: f64) -> bool {
524        (a - b).abs() < 1e-9
525    }
526
527    #[test]
528    fn amplitude_qubits_is_ceil_log2() {
529        assert_eq!(ReferenceSimulator::amplitude_qubits(1), 0);
530        assert_eq!(ReferenceSimulator::amplitude_qubits(2), 1);
531        assert_eq!(ReferenceSimulator::amplitude_qubits(3), 2);
532        assert_eq!(ReferenceSimulator::amplitude_qubits(4), 2);
533        assert_eq!(ReferenceSimulator::amplitude_qubits(1024), 10);
534        assert_eq!(ReferenceSimulator::amplitude_qubits(1025), 11);
535    }
536
537    #[test]
538    fn capacity_cap_is_enforced_with_e0783() {
539        let sim = ReferenceSimulator::new();
540        // 1025 features ⇒ n = 11 > 10.
541        let x = vec![0.0; 1025];
542        let err = sim.encode(&x, EncodingScheme::Amplitude).unwrap_err();
543        assert!(matches!(err, QuantError::CapacityExceeded { requested: 11, cap: 10 }));
544        assert_eq!(err.code(), "axon-E0783");
545    }
546
547    #[test]
548    fn amplitude_encode_requires_unit_norm() {
549        let sim = ReferenceSimulator::new();
550        // ‖[0.6, 0.8]‖ = 1 → ok.
551        let ok = sim.encode(&[0.6, 0.8], EncodingScheme::Amplitude).unwrap();
552        assert_eq!(ok.n, 1);
553        assert!(approx(ok.norm_sqr(), 1.0));
554        // ‖[1, 1]‖ = √2 ≠ 1 → NotNormalized.
555        let err = sim.encode(&[1.0, 1.0], EncodingScheme::Amplitude).unwrap_err();
556        assert!(matches!(err, QuantError::NotNormalized { .. }));
557        assert_eq!(err.code(), "axon-E0788");
558    }
559
560    #[test]
561    fn angle_encode_is_unit_norm_product_state() {
562        let sim = ReferenceSimulator::new();
563        // x = [0] ⇒ |0⟩ : amps [1, 0].
564        let s0 = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
565        assert!(approx(s0.amps[0].re, 1.0) && approx(s0.amps[1].re, 0.0));
566        // x = [π] ⇒ Ry(π)|0⟩ = |1⟩ : amps [0, 1].
567        let s1 = sim.encode(&[PI], EncodingScheme::Angle).unwrap();
568        assert!(approx(s1.amps[0].re, 0.0) && approx(s1.amps[1].re, 1.0));
569        assert!(approx(s1.norm_sqr(), 1.0));
570    }
571
572    #[test]
573    fn ry_pi_flips_zero_to_one() {
574        let sim = ReferenceSimulator::new();
575        // |0⟩ on one qubit (angle-encode x=[0]).
576        let s = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
577        let circuit = VariationalCircuit {
578            layers: vec![RotationLayer { ry: vec![PI], rz: vec![0.0] }],
579        };
580        let out = sim.evolve(s, &circuit).unwrap();
581        // |0⟩ —Ry(π)→ |1⟩ (up to global phase from Rz(0)=I).
582        assert!(approx(out.amps[1].norm_sqr(), 1.0));
583        assert!(approx(out.amps[0].norm_sqr(), 0.0));
584    }
585
586    #[test]
587    fn measure_pauli_z_eigenvalues() {
588        let sim = ReferenceSimulator::new();
589        let z = PauliSum { terms: vec![(1.0, "Z".to_string())] };
590        // ⟨Z⟩ on |0⟩ = +1.
591        let s0 = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
592        assert!(approx(sim.measure(&s0, &z).unwrap(), 1.0));
593        // ⟨Z⟩ on |1⟩ = −1.
594        let s1 = sim.encode(&[PI], EncodingScheme::Angle).unwrap();
595        assert!(approx(sim.measure(&s1, &z).unwrap(), -1.0));
596    }
597
598    #[test]
599    fn measure_zz_on_two_qubits() {
600        let sim = ReferenceSimulator::new();
601        let zz = PauliSum { terms: vec![(1.0, "ZZ".to_string())] };
602        // |00⟩ ⇒ ⟨ZZ⟩ = (+1)(+1) = +1.
603        let s00 = sim.encode(&[0.0, 0.0], EncodingScheme::Angle).unwrap();
604        assert!(approx(sim.measure(&s00, &zz).unwrap(), 1.0));
605        // |01⟩ (qubit0=1, qubit1=0) ⇒ ⟨ZZ⟩ = (−1)(+1) = −1.
606        let s01 = sim.encode(&[PI, 0.0], EncodingScheme::Angle).unwrap();
607        assert!(approx(sim.measure(&s01, &zz).unwrap(), -1.0));
608    }
609
610    #[test]
611    fn measure_rejects_wrong_length_pauli() {
612        let sim = ReferenceSimulator::new();
613        let s = sim.encode(&[0.0, 0.0], EncodingScheme::Angle).unwrap(); // 2 qubits
614        let bad = PauliSum { terms: vec![(1.0, "Z".to_string())] }; // 1 char
615        assert!(matches!(sim.measure(&s, &bad), Err(QuantError::DimensionMismatch { .. })));
616    }
617
618    #[test]
619    fn measure_rejects_bad_pauli_alphabet() {
620        let sim = ReferenceSimulator::new();
621        let s = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
622        let bad = PauliSum { terms: vec![(1.0, "K".to_string())] };
623        let err = sim.measure(&s, &bad).unwrap_err();
624        assert!(matches!(err, QuantError::BadPauli { bad: 'K', .. }));
625    }
626
627    #[test]
628    fn kernel_fidelity_identical_and_orthogonal() {
629        let sim = ReferenceSimulator::new();
630        let a = sim.encode(&[0.6, 0.8], EncodingScheme::Amplitude).unwrap();
631        // |⟨ψ|ψ⟩|² = 1.
632        assert!(approx(sim.kernel(&a, &a).unwrap(), 1.0));
633        // Orthogonal: [1,0] vs [0,1] ⇒ 0.
634        let e0 = sim.encode(&[1.0, 0.0], EncodingScheme::Amplitude).unwrap();
635        let e1 = sim.encode(&[0.0, 1.0], EncodingScheme::Amplitude).unwrap();
636        assert!(approx(sim.kernel(&e0, &e1).unwrap(), 0.0));
637    }
638
639    #[test]
640    fn reupload_changes_the_feature_map_and_stays_a_valid_kernel() {
641        // v2.23.0 — data re-uploading escapes the single-layer (quadratic)
642        // feature map: L=2 produces a DIFFERENT, higher-frequency kernel than L=1
643        // (the Fourier-feature gain). Both remain valid fidelity kernels.
644        let sim = ReferenceSimulator::new();
645        let x = [0.5, 1.2, 0.3];
646        let y = [1.0, 0.2, 0.9];
647        let k1 = sim
648            .kernel(
649                &sim.reupload_encode(&x, 1).unwrap(),
650                &sim.reupload_encode(&y, 1).unwrap(),
651            )
652            .unwrap();
653        let k2 = sim
654            .kernel(
655                &sim.reupload_encode(&x, 2).unwrap(),
656                &sim.reupload_encode(&y, 2).unwrap(),
657            )
658            .unwrap();
659        assert!(
660            (k1 - k2).abs() > 1e-3,
661            "re-uploading must change the kernel (escape the single-layer bound): k1={k1}, k2={k2}"
662        );
663        // Valid fidelity kernel: self-overlap = 1, range [0, 1].
664        let sx = sim.reupload_encode(&x, 2).unwrap();
665        assert!(approx(sim.kernel(&sx, &sx).unwrap(), 1.0));
666        assert!(k2 >= -1e-9 && k2 <= 1.0 + 1e-9);
667    }
668
669    #[test]
670    fn polynomial_kernel_is_dot_to_the_degree() {
671        // v2.23.0 — `(xᵀy)^d`. x=[0.6,0.8], y=[1,0] ⇒ xᵀy = 0.6.
672        let sim = ReferenceSimulator::new();
673        let a = sim.encode(&[0.6, 0.8], EncodingScheme::Amplitude).unwrap();
674        let b = sim.encode(&[1.0, 0.0], EncodingScheme::Amplitude).unwrap();
675        assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 0).unwrap(), 1.0));
676        assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 1).unwrap(), 0.6));
677        assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 2).unwrap(), 0.36));
678        assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 3).unwrap(), 0.216));
679    }
680
681    #[test]
682    fn cnot_entangles_for_bell_correlation() {
683        // |00⟩ —Ry(π) on q0→ |10⟩ —CNOT(0,1)→ |11⟩. Then ⟨ZZ⟩ = (−1)(−1) = +1.
684        let sim = ReferenceSimulator::new();
685        let s = sim.encode(&[0.0, 0.0], EncodingScheme::Angle).unwrap();
686        let circuit = VariationalCircuit {
687            layers: vec![RotationLayer { ry: vec![PI, 0.0], rz: vec![0.0, 0.0] }],
688        };
689        let out = sim.evolve(s, &circuit).unwrap();
690        let zz = PauliSum { terms: vec![(1.0, "ZZ".to_string())] };
691        assert!(approx(sim.measure(&out, &zz).unwrap(), 1.0), "post-CNOT |11⟩ ⇒ ⟨ZZ⟩ = +1");
692    }
693}