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
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 §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 §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 §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/// §Fase 51.e — the algebraic-backend **port** (D1). The OSS crate ships the
184/// [`ReferenceSimulator`]; the enterprise QuIDD / VRAM / QPU engine implements
185/// the same trait (§51.f–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 (§3.1).
191    fn encode(&self, x: &[f64], scheme: EncodingScheme) -> Result<StateVector, QuantError>;
192    /// Evolve a state under a parametric circuit `U(θ)` (§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⟩|²` (§3.4, fidelity kernel).
198    fn kernel(&self, a: &StateVector, b: &StateVector) -> Result<f64, QuantError>;
199}
200
201// ── The OSS reference simulator ─────────────────────────────────────────────
202
203/// The default capacity cap for the OSS reference simulator: n ≤ 10 ⇒ D ≤ 1024
204/// (the paper's `DensityMatrix[1024]` boundary). Above this, callers must use an
205/// enterprise [`QuantBackend`].
206pub const OSS_QUBIT_CAP: usize = 10;
207
208/// Tolerance for the unit-norm assertion on amplitude-encoding input.
209const NORM_TOL: f64 = 1e-9;
210
211/// §Fase 51.e — a usable dense-statevector simulator over `f64` complex
212/// amplitudes, capped at [`OSS_QUBIT_CAP`].
213#[derive(Debug, Clone)]
214pub struct ReferenceSimulator {
215    cap: usize,
216}
217
218impl Default for ReferenceSimulator {
219    fn default() -> Self {
220        ReferenceSimulator { cap: OSS_QUBIT_CAP }
221    }
222}
223
224impl ReferenceSimulator {
225    pub fn new() -> Self {
226        Self::default()
227    }
228
229    /// ⌈log₂ d⌉ via integer doubling (avoids float edge cases on exact powers).
230    fn amplitude_qubits(d: usize) -> usize {
231        let mut n = 0usize;
232        while (1usize << n) < d {
233            n += 1;
234        }
235        n
236    }
237
238    /// Apply a single-qubit gate `g` (row-major 2×2) to qubit `q`.
239    fn apply_1q(amps: &mut [C], q: usize, g: [[C; 2]; 2]) {
240        let bit = 1usize << q;
241        for i in 0..amps.len() {
242            if i & bit == 0 {
243                let j = i | bit;
244                let a0 = amps[i];
245                let a1 = amps[j];
246                amps[i] = g[0][0] * a0 + g[0][1] * a1;
247                amps[j] = g[1][0] * a0 + g[1][1] * a1;
248            }
249        }
250    }
251
252    /// §Fase 69.c — **data re-uploading**: interleave an angle-encoding of `x`
253    /// with a fixed entangling layer, `layers` times. For `layers ≥ 2` the data
254    /// `x` re-enters the circuit, so `⟨ψ(x)|ψ(y)⟩` is NO LONGER a quadratic form in
255    /// `x` (it becomes a Fourier series in the data — Schuld 2021,
256    /// `arXiv:2008.08605`). This is the ONLY provable escape from the amplitude+Pauli
257    /// quadratic bound (§69.b / the Havlíček route). `layers = 1` reduces to a
258    /// single angle layer (no re-uploading). HONEST: escaping the bound does NOT
259    /// guarantee advantage on classical text — the §69.a/b Advantage Witness still
260    /// gates it.
261    pub fn reupload_encode(&self, x: &[f64], layers: usize) -> Result<StateVector, QuantError> {
262        let n = x.len();
263        if n == 0 {
264            return Err(QuantError::DimensionMismatch {
265                detail: "empty input vector".to_string(),
266            });
267        }
268        if n > self.cap {
269            return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
270        }
271        // Start in |0…0⟩.
272        let mut amps = vec![C::ZERO; 1usize << n];
273        amps[0] = C::real(1.0);
274        for _ in 0..layers.max(1) {
275            // Data re-upload layer: Ry(xⱼ) on each qubit (the data enters here).
276            for (q, &angle) in x.iter().enumerate() {
277                let ry = [
278                    [C::real((angle / 2.0).cos()), C::real(-(angle / 2.0).sin())],
279                    [C::real((angle / 2.0).sin()), C::real((angle / 2.0).cos())],
280                ];
281                Self::apply_1q(&mut amps, q, ry);
282            }
283            // Fixed entangling layer (the data-independent "trainable" block; the
284            // reference uses a deterministic CNOT chain so the kernel is reproducible).
285            for q in 0..n.saturating_sub(1) {
286                Self::apply_cnot(&mut amps, q, q + 1);
287            }
288        }
289        Ok(StateVector { n, amps })
290    }
291
292    /// Apply CNOT(control `c`, target `t`).
293    fn apply_cnot(amps: &mut [C], c: usize, t: usize) {
294        let cb = 1usize << c;
295        let tb = 1usize << t;
296        for i in 0..amps.len() {
297            if i & cb != 0 && i & tb == 0 {
298                amps.swap(i, i | tb);
299            }
300        }
301    }
302
303    /// Apply one Pauli char to qubit `q` of `amps` in place.
304    fn apply_pauli(amps: &mut [C], q: usize, p: char) -> Result<(), char> {
305        let bit = 1usize << q;
306        match p {
307            'I' => {}
308            'X' => {
309                for i in 0..amps.len() {
310                    if i & bit == 0 {
311                        amps.swap(i, i | bit);
312                    }
313                }
314            }
315            'Z' => {
316                for amp in amps.iter_mut().enumerate().filter(|(i, _)| i & bit != 0).map(|(_, a)| a) {
317                    *amp = -*amp;
318                }
319            }
320            'Y' => {
321                // Y|0⟩ = i|1⟩, Y|1⟩ = −i|0⟩ ⇒ new[0] = −i·a1, new[1] = i·a0.
322                for i in 0..amps.len() {
323                    if i & bit == 0 {
324                        let j = i | bit;
325                        let a0 = amps[i];
326                        let a1 = amps[j];
327                        amps[i] = (-C::I) * a1;
328                        amps[j] = C::I * a0;
329                    }
330                }
331            }
332            other => return Err(other),
333        }
334        Ok(())
335    }
336
337    /// ⟨a|b⟩ — the complex inner product.
338    fn inner(a: &[C], b: &[C]) -> C {
339        a.iter()
340            .zip(b.iter())
341            .fold(C::ZERO, |acc, (x, y)| acc + x.conj() * *y)
342    }
343}
344
345impl QuantBackend for ReferenceSimulator {
346    fn capacity(&self) -> usize {
347        self.cap
348    }
349
350    fn encode(&self, x: &[f64], scheme: EncodingScheme) -> Result<StateVector, QuantError> {
351        if x.is_empty() {
352            return Err(QuantError::DimensionMismatch {
353                detail: "empty input vector".to_string(),
354            });
355        }
356        match scheme {
357            EncodingScheme::Amplitude => {
358                let n = Self::amplitude_qubits(x.len());
359                if n > self.cap {
360                    return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
361                }
362                // Norm invariant (D2): amplitude encoding requires ‖x‖₂ = 1.
363                let norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
364                if (norm - 1.0).abs() > NORM_TOL {
365                    return Err(QuantError::NotNormalized { norm });
366                }
367                let mut amps = vec![C::ZERO; 1usize << n];
368                for (i, &v) in x.iter().enumerate() {
369                    amps[i] = C::real(v);
370                }
371                Ok(StateVector { n, amps })
372            }
373            EncodingScheme::Angle => {
374                let n = x.len();
375                if n > self.cap {
376                    return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
377                }
378                // Product state ⊗ⱼ [cos(xⱼ/2), sin(xⱼ/2)] — inherently unit-norm.
379                let mut amps = vec![C::ZERO; 1usize << n];
380                for (idx, amp) in amps.iter_mut().enumerate() {
381                    let mut coeff = 1.0f64;
382                    for (q, &angle) in x.iter().enumerate() {
383                        let bit = (idx >> q) & 1;
384                        coeff *= if bit == 0 { (angle / 2.0).cos() } else { (angle / 2.0).sin() };
385                    }
386                    *amp = C::real(coeff);
387                }
388                Ok(StateVector { n, amps })
389            }
390        }
391    }
392
393    fn evolve(&self, mut state: StateVector, circuit: &VariationalCircuit) -> Result<StateVector, QuantError> {
394        let n = state.n;
395        for (li, layer) in circuit.layers.iter().enumerate() {
396            if layer.ry.len() != n || layer.rz.len() != n {
397                return Err(QuantError::DimensionMismatch {
398                    detail: format!(
399                        "layer {li} has {}/{} rotation angles but the register has {n} qubits",
400                        layer.ry.len(),
401                        layer.rz.len()
402                    ),
403                });
404            }
405            // Single-qubit Ry(θ)·Rz(φ) on each qubit.
406            for q in 0..n {
407                let ty = layer.ry[q];
408                let ry = [
409                    [C::real((ty / 2.0).cos()), C::real(-(ty / 2.0).sin())],
410                    [C::real((ty / 2.0).sin()), C::real((ty / 2.0).cos())],
411                ];
412                Self::apply_1q(&mut state.amps, q, ry);
413                let tz = layer.rz[q];
414                let rz = [
415                    [C::new((tz / 2.0).cos(), -(tz / 2.0).sin()), C::ZERO],
416                    [C::ZERO, C::new((tz / 2.0).cos(), (tz / 2.0).sin())],
417                ];
418                Self::apply_1q(&mut state.amps, q, rz);
419            }
420            // Linear CNOT entanglement chain (U_ent).
421            for q in 0..n.saturating_sub(1) {
422                Self::apply_cnot(&mut state.amps, q, q + 1);
423            }
424        }
425        Ok(state)
426    }
427
428    fn measure(&self, state: &StateVector, observable: &PauliSum) -> Result<f64, QuantError> {
429        let n = state.n;
430        let mut expectation = 0.0f64;
431        for (coeff, pauli) in &observable.terms {
432            if pauli.chars().count() != n {
433                return Err(QuantError::DimensionMismatch {
434                    detail: format!(
435                        "Pauli string '{pauli}' spans {} qubit(s) but the state has {n}",
436                        pauli.chars().count()
437                    ),
438                });
439            }
440            // φ = Pₖ|ψ⟩, then ⟨ψ|φ⟩ (real part — Pₖ is Hermitian).
441            let mut phi = state.amps.clone();
442            for (q, p) in pauli.chars().enumerate() {
443                Self::apply_pauli(&mut phi, q, p)
444                    .map_err(|bad| QuantError::BadPauli { pauli: pauli.clone(), bad })?;
445            }
446            expectation += coeff * Self::inner(&state.amps, &phi).re;
447        }
448        Ok(expectation)
449    }
450
451    fn kernel(&self, a: &StateVector, b: &StateVector) -> Result<f64, QuantError> {
452        if a.n != b.n {
453            return Err(QuantError::DimensionMismatch {
454                detail: format!("kernel operands span {} vs {} qubits", a.n, b.n),
455            });
456        }
457        Ok(Self::inner(&a.amps, &b.amps).norm_sqr())
458    }
459}
460
461impl ReferenceSimulator {
462    /// §Fase 69.d — multi-copy **polynomial kernel** `(xᵀy)^d`. Loading `d` copies
463    /// of the state gives `⟨ψ(x)|ψ(y)⟩^d = (xᵀy)^d` for amplitude encoding (Schuld
464    /// & Killoran). It reaches *beyond* cosine (degree 1) — but it is still a
465    /// CLASSICAL polynomial kernel (no quantum advantage), so like every fixed
466    /// amplitude map it is gated by the §69.a/b Advantage Witness. `degree = 0` is
467    /// the constant kernel `1`; `degree = 1` is the linear/cosine kernel `xᵀy`.
468    pub fn polynomial_kernel(a: &StateVector, b: &StateVector, degree: u32) -> Result<f64, QuantError> {
469        if a.n != b.n {
470            return Err(QuantError::DimensionMismatch {
471                detail: format!("kernel operands span {} vs {} qubits", a.n, b.n),
472            });
473        }
474        // Real amplitudes ⇒ the inner product is real (= xᵀy); raise to the degree.
475        Ok(Self::inner(&a.amps, &b.amps).re.powi(degree as i32))
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use std::f64::consts::PI;
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}