Skip to main content

wai_quantum/
quantum_mps.rs

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