Skip to main content

wai_quantum/
quantum_mps.rs

1//! Matrix Product State backend — the classical referee (`wai.quantum.mps`).
2//!
3//! The 2024–2026 literature's central lesson is that the method which actually
4//! beats quantum hardware on "utility-scale" circuits is the **tensor network**:
5//! for a state whose entanglement stays bounded, an MPS represents `2ⁿ` amplitudes
6//! in `O(n · χ²)` numbers and evolves them gate-by-gate, truncating the bond
7//! dimension `χ` at each two-qubit gate. This is the backend the dense simulator's
8//! cheapest-sufficient cascade hands large, low-entanglement circuits to.
9//!
10//! Contract, stated honestly and differently from the dense simulator: an MPS with
11//! bounded `χ` is **approximate** — the whole point is to discard small Schmidt
12//! weight — so byte-exact-vs-analytic is not the promise here. What IS preserved is
13//! **determinism**: every arithmetic step uses only IEEE-754 `+ − × ÷ √` (no FMA,
14//! no transcendentals), so the result is bit-reproducible on every machine, and the
15//! engine reports the **max bond dimension** it used and the **retained weight**
16//! (the standard truncation-fidelity heuristic). Verified against the byte-exact dense
17//! simulator: exact agreement when `χ` is unbounded, graceful, measured degradation
18//! when it is squeezed.
19
20// Numerical kernels: indexed loops and inherent add/mul/sub on the complex type are
21// deliberate for legibility of the matrix math.
22#![allow(clippy::should_implement_trait, clippy::needless_range_loop, clippy::manual_range_contains)]
23
24use crate::quantum::{BaseGate, Circuit, Gate, StateVector};
25
26// ===========================================================================
27// Complex f64 (deterministic: only + - * / sqrt, no FMA/transcendentals)
28// ===========================================================================
29
30#[derive(Clone, Copy, Debug)]
31pub struct C {
32    pub re: f64,
33    pub im: f64,
34}
35impl C {
36    const ZERO: C = C { re: 0.0, im: 0.0 };
37    const ONE: C = C { re: 1.0, im: 0.0 };
38    #[inline]
39    fn new(re: f64, im: f64) -> C {
40        C { re, im }
41    }
42    #[inline]
43    fn add(self, o: C) -> C {
44        C { re: self.re + o.re, im: self.im + o.im }
45    }
46    #[inline]
47    fn sub(self, o: C) -> C {
48        C { re: self.re - o.re, im: self.im - o.im }
49    }
50    #[inline]
51    fn mul(self, o: C) -> C {
52        C { re: self.re * o.re - self.im * o.im, im: self.re * o.im + self.im * o.re }
53    }
54    #[inline]
55    fn conj(self) -> C {
56        C { re: self.re, im: -self.im }
57    }
58    #[inline]
59    fn scale(self, s: f64) -> C {
60        C { re: self.re * s, im: self.im * s }
61    }
62    #[inline]
63    fn norm2(self) -> f64 {
64        self.re * self.re + self.im * self.im
65    }
66}
67
68// ===========================================================================
69// Complex one-sided Jacobi SVD  (M = U · diag(S) · V†)
70// ===========================================================================
71
72/// SVD of an `rows × cols` matrix given by its **columns** (`m[c]` is column `c`, a
73/// length-`rows` vector). Returns `(u_cols, s, v_cols)` where each `u_cols[k]` is a
74/// length-`rows` left singular vector, `s[k]` its singular value, `v_cols[k]` a
75/// length-`cols` right singular vector — sorted by `s` descending. Deterministic:
76/// fixed sweep order, fixed sweep cap, only `+ − × ÷ √`.
77fn jacobi_svd(mut m: Vec<Vec<C>>, rows: usize, cols: usize) -> (Vec<Vec<C>>, Vec<f64>, Vec<Vec<C>>) {
78    // V starts as identity (cols × cols), stored by columns.
79    let mut v: Vec<Vec<C>> = (0..cols)
80        .map(|j| (0..cols).map(|i| if i == j { C::ONE } else { C::ZERO }).collect())
81        .collect();
82    let eps = 1e-14;
83    for _sweep in 0..60 {
84        let mut off = 0.0f64;
85        for p in 0..cols {
86            for q in (p + 1)..cols {
87                // gram entries of columns p,q
88                let mut alpha = 0.0; // <c_p,c_p>
89                let mut beta = 0.0; // <c_q,c_q>
90                let mut gamma = C::ZERO; // <c_p,c_q> = sum conj(c_p)·c_q
91                for k in 0..rows {
92                    alpha += m[p][k].norm2();
93                    beta += m[q][k].norm2();
94                    gamma = gamma.add(m[p][k].conj().mul(m[q][k]));
95                }
96                let g2 = gamma.norm2();
97                off += g2;
98                if g2 <= eps * alpha * beta || g2 == 0.0 {
99                    continue;
100                }
101                let gabs = g2.sqrt();
102                // phase-rotate column q so <c_p,c_q> becomes real +gabs
103                let ph = C::new(gamma.re / gabs, gamma.im / gabs); // unit
104                let cph = ph.conj();
105                for k in 0..rows {
106                    m[q][k] = m[q][k].mul(cph);
107                }
108                for k in 0..cols {
109                    v[q][k] = v[q][k].mul(cph);
110                }
111                // real Jacobi rotation with a=alpha,b=beta,g=gabs
112                let tau = (beta - alpha) / (2.0 * gabs);
113                let t = if tau >= 0.0 {
114                    1.0 / (tau + (1.0 + tau * tau).sqrt())
115                } else {
116                    -1.0 / (-tau + (1.0 + tau * tau).sqrt())
117                };
118                let cs = 1.0 / (1.0 + t * t).sqrt();
119                let sn = t * cs;
120                for k in 0..rows {
121                    let x = m[p][k];
122                    let y = m[q][k];
123                    m[p][k] = x.scale(cs).sub(y.scale(sn));
124                    m[q][k] = x.scale(sn).add(y.scale(cs));
125                }
126                for k in 0..cols {
127                    let x = v[p][k];
128                    let y = v[q][k];
129                    v[p][k] = x.scale(cs).sub(y.scale(sn));
130                    v[q][k] = x.scale(sn).add(y.scale(cs));
131                }
132            }
133        }
134        if off <= eps {
135            break;
136        }
137    }
138    // singular values = column norms; left vectors = normalized columns
139    let mut s = vec![0.0; cols];
140    for j in 0..cols {
141        s[j] = (0..rows).map(|k| m[j][k].norm2()).sum::<f64>().sqrt();
142    }
143    let mut u: Vec<Vec<C>> = m;
144    for j in 0..cols {
145        if s[j] > 1e-300 {
146            let inv = 1.0 / s[j];
147            for k in 0..rows {
148                u[j][k] = u[j][k].scale(inv);
149            }
150        }
151    }
152    // sort by singular value descending (deterministic)
153    let mut order: Vec<usize> = (0..cols).collect();
154    order.sort_by(|&a, &b| s[b].partial_cmp(&s[a]).unwrap_or(std::cmp::Ordering::Equal));
155    let s2: Vec<f64> = order.iter().map(|&i| s[i]).collect();
156    let u2: Vec<Vec<C>> = order.iter().map(|&i| u[i].clone()).collect();
157    let v2: Vec<Vec<C>> = order.iter().map(|&i| v[i].clone()).collect();
158    (u2, s2, v2)
159}
160
161// ===========================================================================
162// MPS
163// ===========================================================================
164
165/// A matrix-product-state on `n` qubits. Site `i`'s tensor `a[i]` has shape
166/// `(dl[i], 2, dr[i])`, stored flat with index `(l*2 + s)*dr + r`.
167pub struct Mps {
168    pub n: u8,
169    a: Vec<Vec<C>>,
170    dl: Vec<usize>,
171    dr: Vec<usize>,
172    chi_max: usize,
173    max_bond: usize,
174    retained: f64, // running product of retained weight (fidelity floor)
175}
176
177#[inline]
178fn idx(l: usize, s: usize, r: usize, dr: usize) -> usize {
179    (l * 2 + s) * dr + r
180}
181
182impl Mps {
183    /// The product state `|0…0⟩` on `n` qubits.
184    pub fn zero(n: u8, chi_max: usize) -> Mps {
185        let a: Vec<Vec<C>> = (0..n).map(|_| vec![C::ONE, C::ZERO]).collect(); // (1,2,1): s=0→1
186        Mps {
187            n,
188            a,
189            dl: vec![1; n as usize],
190            dr: vec![1; n as usize],
191            chi_max: chi_max.max(1),
192            max_bond: 1,
193            retained: 1.0,
194        }
195    }
196
197    /// Apply a single-qubit gate (2×2, `g[out][in]`) on site `i`.
198    fn apply1(&mut self, i: usize, g: [[C; 2]; 2]) {
199        let (dl, dr) = (self.dl[i], self.dr[i]);
200        let ai = &mut self.a[i];
201        for l in 0..dl {
202            for r in 0..dr {
203                let a0 = ai[idx(l, 0, r, dr)];
204                let a1 = ai[idx(l, 1, r, dr)];
205                ai[idx(l, 0, r, dr)] = g[0][0].mul(a0).add(g[0][1].mul(a1));
206                ai[idx(l, 1, r, dr)] = g[1][0].mul(a0).add(g[1][1].mul(a1));
207            }
208        }
209    }
210
211    /// Apply a two-qubit gate (4×4, basis `2*s_lo + s_hi`) on **adjacent** sites
212    /// `i, i+1`, then SVD-truncate the bond to `chi_max`.
213    fn apply2_adjacent(&mut self, i: usize, g: [[C; 4]; 4]) {
214        let (dl, dc, dr) = (self.dl[i], self.dr[i], self.dr[i + 1]);
215        debug_assert_eq!(dc, self.dl[i + 1]);
216        // contract A[i]·A[i+1] over the shared bond → Theta[l, s1, s2, r]
217        // apply gate: Theta'[l,t1,t2,r] = sum G[(t1,t2)][(s1,s2)] Theta[l,s1,s2,r]
218        // reshape to M[row=(l,t1), col=(t2,r)]  (rows = dl*2, cols = 2*dr)
219        let rows = dl * 2;
220        let cols = 2 * dr;
221        let mut mcols: Vec<Vec<C>> = vec![vec![C::ZERO; rows]; cols];
222        for l in 0..dl {
223            for r in 0..dr {
224                // gather Theta[l, ., ., r] as a length-4 vector over (s1,s2)
225                let mut th = [C::ZERO; 4];
226                for s1 in 0..2 {
227                    for s2 in 0..2 {
228                        let mut acc = C::ZERO;
229                        for c in 0..dc {
230                            acc = acc.add(self.a[i][idx(l, s1, c, dc)].mul(self.a[i + 1][idx(c, s2, r, dr)]));
231                        }
232                        th[s1 * 2 + s2] = acc;
233                    }
234                }
235                for t1 in 0..2 {
236                    for t2 in 0..2 {
237                        let mut val = C::ZERO;
238                        for s in 0..4 {
239                            val = val.add(g[t1 * 2 + t2][s].mul(th[s]));
240                        }
241                        let row = l * 2 + t1;
242                        let col = t2 * dr + r;
243                        mcols[col][row] = val;
244                    }
245                }
246            }
247        }
248        let (u, s, v) = jacobi_svd(mcols, rows, cols);
249        // truncate
250        let total: f64 = s.iter().map(|x| x * x).sum();
251        let mut chi = 0;
252        for (k, sv) in s.iter().enumerate() {
253            if k >= self.chi_max || *sv <= 1e-12 * s[0].max(1e-300) {
254                break;
255            }
256            chi += 1;
257        }
258        let chi = chi.max(1).min(s.len());
259        let kept: f64 = s.iter().take(chi).map(|x| x * x).sum();
260        if total > 0.0 {
261            self.retained *= kept / total;
262        }
263        self.max_bond = self.max_bond.max(chi);
264        // new A[i] = U[:, :chi]  → shape (dl, 2, chi)
265        let mut na = vec![C::ZERO; dl * 2 * chi];
266        for l in 0..dl {
267            for t1 in 0..2 {
268                for k in 0..chi {
269                    na[idx(l, t1, k, chi)] = u[k][l * 2 + t1];
270                }
271            }
272        }
273        // new A[i+1] = diag(S)·V†  → shape (chi, 2, dr): [k,t2,r] = S[k]·conj(V_col[k][t2*dr+r])
274        let mut nb = vec![C::ZERO; chi * 2 * dr];
275        for k in 0..chi {
276            for t2 in 0..2 {
277                for r in 0..dr {
278                    nb[idx(k, t2, r, dr)] = v[k][t2 * dr + r].conj().scale(s[k]);
279                }
280            }
281        }
282        self.a[i] = na;
283        self.dr[i] = chi;
284        self.a[i + 1] = nb;
285        self.dl[i + 1] = chi;
286    }
287
288    /// Adjacent SWAP of sites `i, i+1` (its own two-qubit gate).
289    fn swap_adjacent(&mut self, i: usize) {
290        self.apply2_adjacent(i, swap_gate());
291    }
292
293    /// Apply a two-qubit gate on sites `a`,`b` (any distance) by SWAP-routing `b`
294    /// next to `a`, applying, and SWAPping back. `g4` is in `(min,max)`-site order.
295    fn apply2(&mut self, a: usize, b: usize, g4: [[C; 4]; 4]) {
296        let (lo, hi) = (a.min(b), a.max(b));
297        if hi == lo + 1 {
298            self.apply2_adjacent(lo, g4);
299            return;
300        }
301        // bring site `hi` down to `lo+1`
302        for k in (lo + 1..hi).rev() {
303            self.swap_adjacent(k);
304        }
305        self.apply2_adjacent(lo, g4);
306        for k in lo + 1..hi {
307            self.swap_adjacent(k);
308        }
309    }
310
311    /// The full `2ⁿ` statevector (for verification / small `n`), in the dense
312    /// simulator's index convention (bit `i` = qubit/site `i`), **normalized**.
313    pub fn to_statevector(&self) -> Vec<C> {
314        let dim = 1usize << self.n;
315        let mut out = vec![C::ZERO; dim];
316        for k in 0..dim {
317            // contract left→right, selecting s_i = (k>>i)&1
318            let mut vec = vec![C::ONE]; // 1×1 row
319            for i in 0..self.n as usize {
320                let s = (k >> i) & 1;
321                let (dl, dr) = (self.dl[i], self.dr[i]);
322                let mut nv = vec![C::ZERO; dr];
323                for r in 0..dr {
324                    let mut acc = C::ZERO;
325                    for l in 0..dl {
326                        acc = acc.add(vec[l].mul(self.a[i][idx(l, s, r, dr)]));
327                    }
328                    nv[r] = acc;
329                }
330                vec = nv;
331            }
332            out[k] = vec[0];
333        }
334        // normalize
335        let norm: f64 = out.iter().map(|c| c.norm2()).sum::<f64>().sqrt();
336        if norm > 0.0 {
337            for c in out.iter_mut() {
338                *c = c.scale(1.0 / norm);
339            }
340        }
341        out
342    }
343
344    pub fn max_bond(&self) -> usize {
345        self.max_bond
346    }
347    /// Retained Schmidt weight across all truncations, in `[0,1]` — the standard
348    /// truncation-fidelity heuristic (the fraction of squared singular weight kept).
349    /// It is an *indicator*, not a strict bound: because discarded components can be
350    /// amplified by later gates, the true fidelity can sit either side of it. `1.0`
351    /// means no weight was ever discarded (the run was exact).
352    pub fn retained_weight(&self) -> f64 {
353        self.retained
354    }
355}
356
357// ===========================================================================
358// Gate matrices (f64) + circuit runner
359// ===========================================================================
360
361fn m1(base: BaseGate, param: u16) -> [[C; 2]; 2] {
362    let s = std::f64::consts::FRAC_1_SQRT_2;
363    let z = C::ZERO;
364    let o = C::ONE;
365    let i = C::new(0.0, 1.0);
366    let ni = C::new(0.0, -1.0);
367    let no = C::new(-1.0, 0.0);
368    let phase = |k: u16| {
369        let th = 2.0 * std::f64::consts::PI / (1u64 << k) as f64;
370        C::new(th.cos(), th.sin())
371    };
372    match base {
373        BaseGate::I => [[o, z], [z, o]],
374        BaseGate::X => [[z, o], [o, z]],
375        BaseGate::Y => [[z, ni], [i, z]],
376        BaseGate::Z => [[o, z], [z, no]],
377        BaseGate::H => [[C::new(s, 0.0), C::new(s, 0.0)], [C::new(s, 0.0), C::new(-s, 0.0)]],
378        BaseGate::S => [[o, z], [z, i]],
379        BaseGate::Sdg => [[o, z], [z, ni]],
380        BaseGate::T => [[o, z], [z, phase(3)]],
381        BaseGate::Tdg => [[o, z], [z, phase(3).conj()]],
382        BaseGate::P => [[o, z], [z, phase(param)]],
383    }
384}
385
386/// The 4×4 for a controlled-`U` on sites `(lo,hi)` (basis `2*s_lo+s_hi`), given
387/// which of the pair is the control.
388fn controlled4(control_is_lo: bool, u: [[C; 2]; 2]) -> [[C; 4]; 4] {
389    let mut g = [[C::ZERO; 4]; 4];
390    for a in 0..2 {
391        for b in 0..2 {
392            let inb = a * 2 + b;
393            let (ctrl, tgt) = if control_is_lo { (a, b) } else { (b, a) };
394            if ctrl == 0 {
395                g[inb][inb] = C::ONE; // identity
396            } else {
397                // apply U to the target bit
398                for tt in 0..2 {
399                    let (oa, ob) = if control_is_lo { (a, tt) } else { (tt, b) };
400                    let out = oa * 2 + ob;
401                    g[out][inb] = u[tt][tgt];
402                }
403            }
404        }
405    }
406    g
407}
408
409fn swap_gate() -> [[C; 4]; 4] {
410    let mut g = [[C::ZERO; 4]; 4];
411    g[0][0] = C::ONE;
412    g[3][3] = C::ONE;
413    g[1][2] = C::ONE; // |01> <-> |10>
414    g[2][1] = C::ONE;
415    g
416}
417
418/// Expand a Toffoli-class gate (`X`/`Z` with two controls) into 1- and 2-qubit
419/// gates the MPS handles directly (standard Clifford+T Toffoli; CCZ = H·CCX·H).
420fn decompose_cc(g: &Gate) -> Option<Vec<Gate>> {
421    if g.controls.len() != 2 {
422        return None;
423    }
424    let (a, b, c) = (g.controls[0], g.controls[1], g.target);
425    let g1 = |base, t| Gate { base, controls: vec![], target: t, param: 0 };
426    let cx = |ctrl, t| Gate { base: BaseGate::X, controls: vec![ctrl], target: t, param: 0 };
427    let mut ops = Vec::new();
428    if g.base == BaseGate::Z {
429        ops.push(g1(BaseGate::H, c));
430    }
431    // CCX on (a,b;c)
432    ops.push(g1(BaseGate::H, c));
433    ops.push(cx(b, c));
434    ops.push(g1(BaseGate::Tdg, c));
435    ops.push(cx(a, c));
436    ops.push(g1(BaseGate::T, c));
437    ops.push(cx(b, c));
438    ops.push(g1(BaseGate::Tdg, c));
439    ops.push(cx(a, c));
440    ops.push(g1(BaseGate::T, b));
441    ops.push(g1(BaseGate::T, c));
442    ops.push(cx(a, b));
443    ops.push(g1(BaseGate::H, c));
444    ops.push(g1(BaseGate::T, a));
445    ops.push(g1(BaseGate::Tdg, b));
446    ops.push(cx(a, b));
447    if g.base == BaseGate::Z {
448        ops.push(g1(BaseGate::H, c));
449    }
450    Some(ops)
451}
452
453/// The result of an MPS run.
454pub struct MpsRun {
455    pub max_bond: usize,
456    pub retained_weight: f64,
457    /// Fidelity `|⟨mps|dense⟩|²` against the byte-exact dense statevector, when the
458    /// caller asks to verify (small `n`); `None` if not computed.
459    pub fidelity_vs_dense: Option<f64>,
460}
461
462/// Errors an MPS run can raise.
463#[derive(Debug)]
464pub enum MpsError {
465    /// A gate with more than two controls that isn't a decomposable Toffoli class.
466    Unsupported(String),
467}
468
469/// Run `circuit` as an MPS with bond cap `chi_max`. Returns the final [`Mps`] and a
470/// stats summary. Gates with >2 controls (other than CCX/CCZ) are unsupported.
471pub fn run(circuit: &Circuit, chi_max: usize) -> Result<(Mps, MpsRun), MpsError> {
472    let mut mps = Mps::zero(circuit.n_qubits, chi_max);
473    for g in &circuit.ops {
474        apply_gate(&mut mps, g)?;
475    }
476    let run = MpsRun {
477        max_bond: mps.max_bond(),
478        retained_weight: mps.retained_weight(),
479        fidelity_vs_dense: None,
480    };
481    Ok((mps, run))
482}
483
484fn apply_gate(mps: &mut Mps, g: &Gate) -> Result<(), MpsError> {
485    match g.controls.len() {
486        0 => mps.apply1(g.target as usize, m1(g.base, g.param)),
487        1 => {
488            let ctrl = g.controls[0] as usize;
489            let tgt = g.target as usize;
490            let control_is_lo = ctrl < tgt;
491            let g4 = controlled4(control_is_lo, m1(g.base, g.param));
492            mps.apply2(ctrl, tgt, g4);
493        }
494        2 => {
495            let ops = decompose_cc(g)
496                .ok_or_else(|| MpsError::Unsupported(format!("{:?} with 2 controls", g.base)))?;
497            for sub in &ops {
498                apply_gate(mps, sub)?;
499            }
500        }
501        k => return Err(MpsError::Unsupported(format!("{k} controls"))),
502    }
503    Ok(())
504}
505
506/// Fidelity `|⟨mps|dense⟩|²` between an MPS statevector and the byte-exact dense
507/// statevector — the verification metric.
508pub fn fidelity(mps: &Mps, dense: &StateVector) -> f64 {
509    let a = mps.to_statevector();
510    let scale = (1u64 << crate::quantum::FRAC) as f64;
511    let mut overlap = C::ZERO;
512    let mut nd = 0.0;
513    for (k, amp) in dense.amps.iter().enumerate() {
514        let d = C::new(amp.re as f64 / scale, amp.im as f64 / scale);
515        overlap = overlap.add(a[k].conj().mul(d));
516        nd += d.norm2();
517    }
518    if nd <= 0.0 {
519        return 0.0;
520    }
521    overlap.norm2() / nd // |<mps|dense>|^2 (mps already normalized)
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    fn approx(a: f64, b: f64, tol: f64) -> bool {
529        (a - b).abs() < tol
530    }
531    fn ghz(n: u8) -> Circuit {
532        let mut c = Circuit::new(n);
533        c.h(0);
534        for i in 1..n {
535            c.cx(i - 1, i);
536        }
537        c
538    }
539    fn bell() -> Circuit {
540        let mut c = Circuit::new(2);
541        c.h(0);
542        c.cx(0, 1);
543        c
544    }
545    /// A brickwork circuit that genuinely builds chain entanglement (bond → 2^(n/2)).
546    fn entangling(n: u8) -> Circuit {
547        let mut c = Circuit::new(n);
548        for _ in 0..3 {
549            for q in 0..n {
550                c.h(q);
551                c.t(q);
552            }
553            let mut q = 0;
554            while q + 1 < n {
555                c.cx(q, q + 1);
556                q += 2;
557            }
558            let mut q = 1;
559            while q + 1 < n {
560                c.cx(q, q + 1);
561                q += 2;
562            }
563        }
564        c
565    }
566
567    #[test]
568    fn product_state_stays_bond_one() {
569        // all-H on |0…0> is a product state: MPS bond never exceeds 1.
570        let mut c = Circuit::new(5);
571        for q in 0..5 {
572            c.h(q);
573        }
574        let (mps, run) = run(&c, 16).unwrap();
575        assert_eq!(run.max_bond, 1, "product state must stay bond-1");
576        assert!(approx(fidelity(&mps, &c.simulate().unwrap()), 1.0, 1e-9));
577    }
578
579    #[test]
580    fn ghz_is_exact_at_bond_two() {
581        let c = ghz(6);
582        let (mps, run) = run(&c, 8).unwrap();
583        assert_eq!(run.max_bond, 2, "GHZ is a bond-2 state");
584        assert!(approx(fidelity(&mps, &c.simulate().unwrap()), 1.0, 1e-9), "GHZ must be exact");
585    }
586
587    #[test]
588    fn bell_and_small_circuits_match_dense() {
589        for c in [bell(), ghz(4)] {
590            let (mps, _) = run(&c, 16).unwrap();
591            let f = fidelity(&mps, &c.simulate().unwrap());
592            assert!(approx(f, 1.0, 1e-8), "MPS should match dense, got {f}");
593        }
594    }
595
596    #[test]
597    fn qft_matches_dense_at_full_bond() {
598        // QFT builds entanglement; at full bond (2^(n/2)) MPS is exact.
599        let c = Circuit::qft(6);
600        let (mps, _) = run(&c, 64).unwrap();
601        let f = fidelity(&mps, &c.simulate().unwrap());
602        assert!(approx(f, 1.0, 1e-7), "QFT full-bond fidelity {f}");
603    }
604
605    #[test]
606    fn ccz_decomposition_matches_dense() {
607        // a 2-control CCZ over |+++>: dense applies it natively, MPS decomposes it
608        // into 1/2-qubit gates — they must agree.
609        let mut c = Circuit::new(3);
610        c.h(0);
611        c.h(1);
612        c.h(2);
613        c.ops.push(Gate { base: BaseGate::Z, controls: vec![0, 1], target: 2, param: 0 });
614        let (mps, _) = run(&c, 32).unwrap();
615        let f = fidelity(&mps, &c.simulate().unwrap());
616        assert!(approx(f, 1.0, 1e-6), "CCZ decomposition fidelity {f}");
617    }
618
619    #[test]
620    fn nonadjacent_two_qubit_gate() {
621        // CX between distant sites (0 and 4) must route via SWAPs and still match.
622        let mut c = Circuit::new(5);
623        c.h(0);
624        c.cx(0, 4);
625        let (mps, _) = run(&c, 8).unwrap();
626        assert!(approx(fidelity(&mps, &c.simulate().unwrap()), 1.0, 1e-8));
627    }
628
629    #[test]
630    fn truncation_is_measured_not_hidden() {
631        // Squeeze QFT's bond below what it needs → the engine REPORTS the lost
632        // weight (retained_weight < 1) and the loss is REAL (fidelity < 1). The
633        // truncation is neither hidden nor free.
634        let c = entangling(6);
635        let dense = c.simulate().unwrap();
636        let (mps, st) = run(&c, 2).unwrap();
637        assert!(st.retained_weight < 0.999, "squeezed bond must report lost weight: {}", st.retained_weight);
638        assert!(st.retained_weight > 0.0, "some weight is kept");
639        assert!(st.max_bond <= 2, "bond cap respected");
640        assert!(fidelity(&mps, &dense) < 0.999, "aggressive truncation must measurably reduce fidelity");
641        // and a generous bond (full = 2^(n/2) = 8 for n=6) recovers it exactly
642        let (mps2, st2) = run(&c, 8).unwrap();
643        assert!(approx(st2.retained_weight, 1.0, 1e-6), "full bond keeps all weight: {}", st2.retained_weight);
644        assert!(approx(fidelity(&mps2, &dense), 1.0, 1e-6));
645    }
646
647    #[test]
648    fn deterministic() {
649        let c = Circuit::qft(6);
650        let a = run(&c, 8).unwrap().0.to_statevector();
651        let b = run(&c, 8).unwrap().0.to_statevector();
652        assert!(a.iter().zip(&b).all(|(x, y)| x.re.to_bits() == y.re.to_bits() && x.im.to_bits() == y.im.to_bits()),
653            "MPS must be bit-reproducible");
654    }
655}