Skip to main content

axon/
quant.rs

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