Skip to main content

oxicuda_quantum/mps/
simulator.rs

1//! Matrix Product State (MPS) simulator for low-entanglement circuits.
2//!
3//! An MPS represents an `n`-qubit pure state as a chain of rank-3 site tensors
4//!
5//! ```text
6//!   A^[0]  A^[1]  …  A^[n-1]
7//!     │      │          │
8//!    p_0    p_1        p_{n-1}
9//! ```
10//!
11//! where each site tensor `A^[k]` has shape `(left_bond, 2, right_bond)`, the
12//! middle index `p_k ∈ {0,1}` is the physical (qubit) index, and adjacent
13//! tensors share a virtual *bond* index. The two boundary bonds (left of site 0
14//! and right of site n-1) are fixed to dimension 1, so the chain contracts down
15//! to a single `2^n` amplitude vector.
16//!
17//! ## Storage layout
18//!
19//! Every site tensor is held as a flat row-major `Vec<Complex32>` with
20//! dimensions `(left_bond L, 2, right_bond R)`. The element `A[l, p, r]` lives at
21//!
22//! ```text
23//!   offset = (l * 2 + p) * R + r
24//! ```
25//!
26//! i.e. strides `[2 * R, R, 1]` for the `(left, physical, right)` axes.
27//!
28//! ## Qubit / bit ordering
29//!
30//! Site `k` corresponds to qubit `k`. To stay compatible with [`StateVector`],
31//! where qubit `k` is bit `k` of the amplitude index (qubit 0 = least
32//! significant bit), [`MatrixProductState::to_statevector`] places site 0's
33//! physical index in the least-significant position.
34//!
35//! ## Truncation
36//!
37//! Two-qubit gates increase the shared bond. After applying a gate we perform a
38//! singular-value decomposition of the merged two-site block and keep at most
39//! `max_bond_dim` (χ) singular values, additionally discarding any singular
40//! value below `svd_cutoff`. The retained Σ is folded into the left tensor,
41//! yielding a left-canonical split.
42
43use num_complex::Complex;
44
45use crate::error::{QuantumError, QuantumResult};
46use crate::statevec::state::StateVector;
47
48type Complex32 = Complex<f32>;
49
50#[inline]
51fn c0() -> Complex32 {
52    Complex32::new(0.0, 0.0)
53}
54
55/// Configuration for an [`MatrixProductState`] simulation.
56#[derive(Debug, Clone, Copy, PartialEq)]
57pub struct MpsConfig {
58    /// Number of qubits (sites) in the chain.
59    pub n_qubits: usize,
60    /// Maximum retained bond dimension χ after a truncated SVD.
61    pub max_bond_dim: usize,
62    /// Singular values strictly below this threshold are dropped.
63    pub svd_cutoff: f32,
64}
65
66impl MpsConfig {
67    /// Construct a config, validating the qubit count and bond dimension.
68    pub fn new(n_qubits: usize, max_bond_dim: usize, svd_cutoff: f32) -> QuantumResult<Self> {
69        if n_qubits == 0 || n_qubits > 30 {
70            return Err(QuantumError::InvalidQubitCount { n: n_qubits });
71        }
72        if max_bond_dim == 0 {
73            return Err(QuantumError::InvalidParameter {
74                name: "max_bond_dim must be >= 1".into(),
75            });
76        }
77        if !(svd_cutoff.is_finite() && svd_cutoff >= 0.0) {
78            return Err(QuantumError::InvalidParameter {
79                name: "svd_cutoff must be finite and >= 0".into(),
80            });
81        }
82        Ok(Self {
83            n_qubits,
84            max_bond_dim,
85            svd_cutoff,
86        })
87    }
88}
89
90/// A single rank-3 site tensor with shape `(left, 2, right)`.
91#[derive(Debug, Clone)]
92struct SiteTensor {
93    left: usize,
94    right: usize,
95    /// Flat row-major data, length `left * 2 * right`.
96    data: Vec<Complex32>,
97}
98
99impl SiteTensor {
100    fn zeros(left: usize, right: usize) -> Self {
101        Self {
102            left,
103            right,
104            data: vec![c0(); left * 2 * right],
105        }
106    }
107
108    #[inline]
109    fn idx(&self, l: usize, p: usize, r: usize) -> usize {
110        (l * 2 + p) * self.right + r
111    }
112
113    #[inline]
114    fn get(&self, l: usize, p: usize, r: usize) -> Complex32 {
115        self.data[self.idx(l, p, r)]
116    }
117
118    #[inline]
119    fn set(&mut self, l: usize, p: usize, r: usize, v: Complex32) {
120        let i = self.idx(l, p, r);
121        self.data[i] = v;
122    }
123}
124
125/// Matrix Product State representation of an `n`-qubit pure state.
126#[derive(Debug, Clone)]
127pub struct MatrixProductState {
128    config: MpsConfig,
129    sites: Vec<SiteTensor>,
130}
131
132impl MatrixProductState {
133    /// Build the product state |0…0⟩ with all bond dimensions equal to 1.
134    pub fn new_zero_state(config: MpsConfig) -> QuantumResult<Self> {
135        let n = config.n_qubits;
136        let mut sites = Vec::with_capacity(n);
137        for _ in 0..n {
138            // Bonds are all 1; physical 0 amplitude = 1, physical 1 = 0.
139            let mut t = SiteTensor::zeros(1, 1);
140            t.set(0, 0, 0, Complex32::new(1.0, 0.0));
141            sites.push(t);
142        }
143        Ok(Self { config, sites })
144    }
145
146    /// Number of qubits / sites.
147    #[must_use]
148    pub fn n_qubits(&self) -> usize {
149        self.config.n_qubits
150    }
151
152    /// The active configuration.
153    #[must_use]
154    pub fn config(&self) -> MpsConfig {
155        self.config
156    }
157
158    /// The right-bond dimension of each site (one entry per site).
159    ///
160    /// `bond_dims()[k]` is the dimension of the bond between site `k` and
161    /// site `k+1`; the final entry is the right boundary bond (always 1).
162    #[must_use]
163    pub fn bond_dims(&self) -> Vec<usize> {
164        self.sites.iter().map(|s| s.right).collect()
165    }
166
167    /// Apply a 1-qubit gate to `qubit`, contracting it into the physical index.
168    ///
169    /// This leaves both bond dimensions unchanged.
170    pub fn apply_1q(&mut self, gate: &[[Complex32; 2]; 2], qubit: usize) -> QuantumResult<()> {
171        if qubit >= self.config.n_qubits {
172            return Err(QuantumError::QubitIndexOutOfRange {
173                index: qubit,
174                n_qubits: self.config.n_qubits,
175            });
176        }
177        let site = &mut self.sites[qubit];
178        let left = site.left;
179        let right = site.right;
180        for l in 0..left {
181            for r in 0..right {
182                let a0 = site.get(l, 0, r);
183                let a1 = site.get(l, 1, r);
184                let n0 = gate[0][0] * a0 + gate[0][1] * a1;
185                let n1 = gate[1][0] * a0 + gate[1][1] * a1;
186                site.set(l, 0, r, n0);
187                site.set(l, 1, r, n1);
188            }
189        }
190        Ok(())
191    }
192
193    /// Apply a 4×4 two-qubit gate to **adjacent** qubits `q` and `q+1`.
194    ///
195    /// The gate is indexed in the basis `|q, q+1⟩` with `q` the more-significant
196    /// of the two physical indices, i.e. the matrix row/column index is
197    /// `2 * bit(q) + bit(q+1)` — matching
198    /// [`apply_2q_inplace`](crate::statevec::apply_2q::apply_2q_inplace).
199    ///
200    /// # Errors
201    /// Returns an error if the qubits are not adjacent with `q_plus_1 == q + 1`,
202    /// or if either index is out of range.
203    pub fn apply_2q(
204        &mut self,
205        gate: &[[Complex32; 4]; 4],
206        q: usize,
207        q_plus_1: usize,
208    ) -> QuantumResult<()> {
209        let n = self.config.n_qubits;
210        if q >= n {
211            return Err(QuantumError::QubitIndexOutOfRange {
212                index: q,
213                n_qubits: n,
214            });
215        }
216        if q_plus_1 >= n {
217            return Err(QuantumError::QubitIndexOutOfRange {
218                index: q_plus_1,
219                n_qubits: n,
220            });
221        }
222        if q_plus_1 != q + 1 {
223            return Err(QuantumError::InvalidParameter {
224                name: "apply_2q requires adjacent qubits with q_plus_1 == q + 1".into(),
225            });
226        }
227
228        // Merge sites q and q+1 into a single block tensor with shape
229        // (left, 2, 2, right), held as a matrix of rows = left*2 and
230        // cols = 2*right after applying the gate.
231        let left = self.sites[q].left;
232        let mid = self.sites[q].right; // == self.sites[q+1].left
233        let right = self.sites[q_plus_1].right;
234        debug_assert_eq!(mid, self.sites[q_plus_1].left);
235
236        // theta[l, p, p2, r] = Σ_m A_q[l, p, m] * A_{q+1}[m, p2, r]
237        // then apply gate over (p, p2).
238        // Store theta as a matrix `mat` of shape (left*2, 2*right):
239        //   row = l * 2 + p,  col = p2 * right + r.
240        let rows = left * 2;
241        let cols = 2 * right;
242        let mut theta = vec![c0(); rows * cols];
243        {
244            let sq = &self.sites[q];
245            let sq1 = &self.sites[q_plus_1];
246            for l in 0..left {
247                for p in 0..2 {
248                    for p2 in 0..2 {
249                        for r in 0..right {
250                            let mut acc = c0();
251                            for m in 0..mid {
252                                acc += sq.get(l, p, m) * sq1.get(m, p2, r);
253                            }
254                            let row = l * 2 + p;
255                            let col = p2 * right + r;
256                            theta[row * cols + col] = acc;
257                        }
258                    }
259                }
260            }
261        }
262
263        // Apply the 4×4 gate over the combined physical pair (p, p2).
264        // gate index = 2 * p + p2 (p == bit(q), p2 == bit(q+1)).
265        // new_theta[l, (P,P2), r] = Σ_{p,p2} gate[2P+P2, 2p+p2] * theta[l,(p,p2),r]
266        let mut gated = vec![c0(); rows * cols];
267        for l in 0..left {
268            for r in 0..right {
269                // gather the 2×2 physical block for this (l, r)
270                let v00 = theta[(l * 2) * cols + r];
271                let v01 = theta[(l * 2) * cols + (right + r)];
272                let v10 = theta[(l * 2 + 1) * cols + r];
273                let v11 = theta[(l * 2 + 1) * cols + (right + r)];
274                // input vector ordered as [00, 01, 10, 11] = [v00, v01, v10, v11]
275                let input = [v00, v01, v10, v11];
276                for (big, grow) in gate.iter().enumerate() {
277                    let mut acc = c0();
278                    for (small, &iv) in input.iter().enumerate() {
279                        acc += grow[small] * iv;
280                    }
281                    let big_p = big >> 1; // bit(q)
282                    let big_p2 = big & 1; // bit(q+1)
283                    let row = l * 2 + big_p;
284                    let col = if big_p2 == 1 { right + r } else { r };
285                    gated[row * cols + col] = acc;
286                }
287            }
288        }
289
290        // SVD of `gated` (rows × cols): gated = U Σ V†.
291        let svd = svd_dense(&gated, rows, cols)?;
292        let full_rank = svd.singular_values.len();
293
294        // Determine retained rank: drop σ < cutoff, then cap at χ.
295        let mut keep = 0usize;
296        for &s in &svd.singular_values {
297            if s >= self.config.svd_cutoff && keep < self.config.max_bond_dim {
298                keep += 1;
299            } else {
300                break;
301            }
302        }
303        if keep == 0 {
304            // Degenerate (e.g. the block became ~0). Keep one bond to stay valid.
305            keep = if full_rank == 0 { 0 } else { 1 };
306        }
307        let new_bond = keep;
308
309        // Left tensor A_q'[l, p, k] = U[(l*2+p), k] * Σ_k  (fold Σ into left).
310        // U has shape (rows × full_rank); we use its first `keep` columns.
311        let mut left_tensor = SiteTensor::zeros(left, new_bond);
312        for l in 0..left {
313            for p in 0..2 {
314                let row = l * 2 + p;
315                for k in 0..new_bond {
316                    let u = svd.u[row * full_rank + k];
317                    let val = u * svd.singular_values[k];
318                    left_tensor.set(l, p, k, val);
319                }
320            }
321        }
322
323        // Right tensor A_{q+1}'[k, p2, r] = V†[k, (p2*right+r)] = conj(V[(p2*right+r), k]).
324        // V has shape (cols × full_rank).
325        let mut right_tensor = SiteTensor::zeros(new_bond, right);
326        for k in 0..new_bond {
327            for p2 in 0..2 {
328                for r in 0..right {
329                    let col = p2 * right + r;
330                    let v = svd.v[col * full_rank + k];
331                    right_tensor.set(k, p2, r, v.conj());
332                }
333            }
334        }
335
336        self.sites[q] = left_tensor;
337        self.sites[q_plus_1] = right_tensor;
338        Ok(())
339    }
340
341    /// Contract the whole chain into a dense `2^n` [`StateVector`].
342    ///
343    /// Intended for verification on small systems; cost is exponential in `n`.
344    pub fn to_statevector(&self) -> QuantumResult<StateVector> {
345        let n = self.config.n_qubits;
346        // Running tensor as a matrix of shape (acc_phys_dim, cur_right_bond),
347        // where row encodes the physical indices contracted so far with qubit 0
348        // in the least-significant position.
349        //
350        // Start with site 0: shape (left=1, 2, right) -> matrix (2, right).
351        let s0 = &self.sites[0];
352        if s0.left != 1 {
353            return Err(QuantumError::Internal {
354                msg: "left boundary bond must be 1".into(),
355            });
356        }
357        let mut acc_dim = 2usize;
358        let mut acc_right = s0.right;
359        let mut acc = vec![c0(); acc_dim * acc_right];
360        for p in 0..2 {
361            for r in 0..acc_right {
362                acc[p * acc_right + r] = s0.get(0, p, r);
363            }
364        }
365
366        for site in self.sites.iter().skip(1) {
367            if site.left != acc_right {
368                return Err(QuantumError::Internal {
369                    msg: "bond mismatch during contraction".into(),
370                });
371            }
372            let new_right = site.right;
373            let new_dim = acc_dim * 2;
374            let mut next = vec![c0(); new_dim * new_right];
375            // next[(phys_old + p_new * acc_dim), r2] =
376            //     Σ_m acc[phys_old, m] * site[m, p_new, r2]
377            // Placing the new physical index in the *higher* bits keeps qubit 0
378            // (the first contracted site) in the least-significant position.
379            for phys_old in 0..acc_dim {
380                for p_new in 0..2 {
381                    let new_phys = phys_old + p_new * acc_dim;
382                    for r2 in 0..new_right {
383                        let mut val = c0();
384                        for m in 0..acc_right {
385                            val += acc[phys_old * acc_right + m] * site.get(m, p_new, r2);
386                        }
387                        next[new_phys * new_right + r2] = val;
388                    }
389                }
390            }
391            acc = next;
392            acc_dim = new_dim;
393            acc_right = new_right;
394        }
395
396        if acc_right != 1 {
397            return Err(QuantumError::Internal {
398                msg: "right boundary bond must be 1".into(),
399            });
400        }
401        // acc is now (2^n, 1); flatten to amplitudes.
402        let dim = 1usize << n;
403        if acc_dim != dim {
404            return Err(QuantumError::DimensionMismatch {
405                expected: dim,
406                got: acc_dim,
407            });
408        }
409        let mut amps = vec![c0(); dim];
410        for (i, slot) in amps.iter_mut().enumerate() {
411            *slot = acc[i];
412        }
413        Ok(StateVector { amps, n_qubits: n })
414    }
415
416    /// Squared norm ⟨ψ|ψ⟩ of the represented state.
417    #[must_use]
418    pub fn norm_sq(&self) -> f32 {
419        // Contract ⟨ψ|ψ⟩ via the transfer-matrix sweep:
420        //   E_{a,b} accumulates Σ over contracted physical legs of
421        //   conj(A)·A. Start with the 1×1 left boundary E = [1].
422        let mut env = vec![Complex32::new(1.0, 0.0); 1]; // shape (1, 1) flattened
423        let mut dim_a = 1usize; // bra bond dim
424        let mut dim_b = 1usize; // ket bond dim
425        for site in &self.sites {
426            let l = site.left;
427            let r = site.right;
428            // new_env[a', b'] = Σ_{a,b,p} conj(A[a,p,a']) * env[a,b] * A[b,p,b']
429            let mut new_env = vec![c0(); r * r];
430            for ap in 0..r {
431                for bp in 0..r {
432                    let mut acc = c0();
433                    for p in 0..2 {
434                        for a in 0..l.min(dim_a) {
435                            let bra = site.get(a, p, ap).conj();
436                            for b in 0..l.min(dim_b) {
437                                acc += bra * env[a * dim_b + b] * site.get(b, p, bp);
438                            }
439                        }
440                    }
441                    new_env[ap * r + bp] = acc;
442                }
443            }
444            env = new_env;
445            dim_a = r;
446            dim_b = r;
447        }
448        // Final env is (1,1).
449        if env.is_empty() {
450            return 0.0;
451        }
452        env[0].re.max(0.0)
453    }
454
455    /// Euclidean norm ‖ψ‖.
456    #[must_use]
457    pub fn norm(&self) -> f32 {
458        self.norm_sq().sqrt()
459    }
460
461    /// Normalize the state in place (scales the first site tensor).
462    pub fn normalize(&mut self) {
463        let nrm = self.norm();
464        if nrm > 1e-12 {
465            let inv = 1.0 / nrm;
466            if let Some(first) = self.sites.first_mut() {
467                for v in &mut first.data {
468                    *v *= inv;
469                }
470            }
471        }
472    }
473
474    /// Expectation value ⟨ψ| Z_qubit |ψ⟩.
475    pub fn expectation_z(&self, qubit: usize) -> QuantumResult<f32> {
476        if qubit >= self.config.n_qubits {
477            return Err(QuantumError::QubitIndexOutOfRange {
478                index: qubit,
479                n_qubits: self.config.n_qubits,
480            });
481        }
482        // Transfer-matrix sweep with a Z insertion (phase +1 on p=0, -1 on p=1)
483        // at the target site; divide by ⟨ψ|ψ⟩ for an unnormalized MPS.
484        let mut env = vec![Complex32::new(1.0, 0.0); 1];
485        let mut dim_a = 1usize;
486        let mut dim_b = 1usize;
487        for (idx, site) in self.sites.iter().enumerate() {
488            let l = site.left;
489            let r = site.right;
490            let mut new_env = vec![c0(); r * r];
491            for ap in 0..r {
492                for bp in 0..r {
493                    let mut acc = c0();
494                    for p in 0..2 {
495                        let z_phase = if idx == qubit && p == 1 { -1.0 } else { 1.0 };
496                        for a in 0..l.min(dim_a) {
497                            let bra = site.get(a, p, ap).conj() * z_phase;
498                            for b in 0..l.min(dim_b) {
499                                acc += bra * env[a * dim_b + b] * site.get(b, p, bp);
500                            }
501                        }
502                    }
503                    new_env[ap * r + bp] = acc;
504                }
505            }
506            env = new_env;
507            dim_a = r;
508            dim_b = r;
509        }
510        let numer = if env.is_empty() { 0.0 } else { env[0].re };
511        let denom = self.norm_sq();
512        if denom <= 1e-12 {
513            return Err(QuantumError::MeasurementFailed);
514        }
515        Ok(numer / denom)
516    }
517}
518
519// ─── Self-contained dense complex SVD ────────────────────────────────────────
520
521/// Result of [`svd_dense`]: `M = U Σ V†` with σ in descending order.
522struct SvdResult {
523    /// Left singular vectors, shape `(m × k)` row-major (`k = min(m,n)` rank).
524    u: Vec<Complex32>,
525    /// Right singular vectors `V` (NOT V†), shape `(n × k)` row-major.
526    v: Vec<Complex32>,
527    /// Singular values, length `k`, descending.
528    singular_values: Vec<f32>,
529}
530
531/// Compute a singular value decomposition of an `m × n` complex matrix.
532///
533/// Strategy: build the smaller Hermitian Gram matrix and diagonalise it with a
534/// complex cyclic Jacobi sweep (eigenvalues = σ²). When `m >= n` we use
535/// `G = MᴴM` (n×n) to obtain `V`, then `U = M V Σ⁻¹`; otherwise we use
536/// `G = MMᴴ` (m×m) to obtain `U`, then `V = Mᴴ U Σ⁻¹`. A guard handles σ ≈ 0.
537fn svd_dense(mat: &[Complex32], m: usize, n: usize) -> QuantumResult<SvdResult> {
538    if m == 0 || n == 0 || mat.len() != m * n {
539        return Err(QuantumError::DimensionMismatch {
540            expected: m * n,
541            got: mat.len(),
542        });
543    }
544    let k = m.min(n);
545
546    if m >= n {
547        // G = Mᴴ M  (n × n)
548        let g = gram_ata(mat, m, n);
549        let (evals, evecs) = jacobi_hermitian(&g, n)?;
550        // Sort eigen-pairs descending by eigenvalue.
551        let order = sorted_desc_indices(&evals);
552        let mut singular_values = Vec::with_capacity(k);
553        let mut v = vec![c0(); n * k]; // (n × k)
554        let mut u = vec![c0(); m * k]; // (m × k)
555        for (col, &ei) in order.iter().take(k).enumerate() {
556            let sigma = evals[ei].max(0.0).sqrt();
557            singular_values.push(sigma);
558            // V column = eigenvector ei.
559            for row in 0..n {
560                v[row * k + col] = evecs[row * n + ei];
561            }
562            // U column = M v / σ (guard σ ≈ 0).
563            if sigma > 1e-12 {
564                let inv = 1.0 / sigma;
565                for i in 0..m {
566                    let mut acc = c0();
567                    for j in 0..n {
568                        acc += mat[i * n + j] * v[j * k + col];
569                    }
570                    u[i * k + col] = acc * inv;
571                }
572            }
573        }
574        fill_orthonormal_u(&mut u, &singular_values, m, k);
575        Ok(SvdResult {
576            u,
577            v,
578            singular_values,
579        })
580    } else {
581        // G = M Mᴴ  (m × m)
582        let g = gram_aat(mat, m, n);
583        let (evals, evecs) = jacobi_hermitian(&g, m)?;
584        let order = sorted_desc_indices(&evals);
585        let mut singular_values = Vec::with_capacity(k);
586        let mut u = vec![c0(); m * k]; // (m × k)
587        let mut v = vec![c0(); n * k]; // (n × k)
588        for (col, &ei) in order.iter().take(k).enumerate() {
589            let sigma = evals[ei].max(0.0).sqrt();
590            singular_values.push(sigma);
591            for row in 0..m {
592                u[row * k + col] = evecs[row * m + ei];
593            }
594            // V column = Mᴴ u / σ  (guard σ ≈ 0).
595            if sigma > 1e-12 {
596                let inv = 1.0 / sigma;
597                for j in 0..n {
598                    let mut acc = c0();
599                    for i in 0..m {
600                        acc += mat[i * n + j].conj() * u[i * k + col];
601                    }
602                    v[j * k + col] = acc * inv;
603                }
604            }
605        }
606        Ok(SvdResult {
607            u,
608            v,
609            singular_values,
610        })
611    }
612}
613
614/// For columns with σ ≈ 0, replace the (left-undetermined) U column with an
615/// orthonormal complement so that U has orthonormal columns. This keeps the
616/// reconstruction `U Σ Vᴴ` correct (those columns are multiplied by σ ≈ 0) and
617/// makes the left tensor well-formed.
618fn fill_orthonormal_u(u: &mut [Complex32], sigma: &[f32], m: usize, k: usize) {
619    for col in 0..k.min(sigma.len()) {
620        if sigma[col] > 1e-12 {
621            continue;
622        }
623        // Build a candidate from standard basis vectors, orthogonalised against
624        // the already-fixed columns (Gram-Schmidt).
625        let mut placed = false;
626        for seed in 0..m {
627            let mut cand = vec![c0(); m];
628            cand[seed] = Complex32::new(1.0, 0.0);
629            for prev in 0..col {
630                // proj = <prev|cand> ; cand -= proj * prev
631                let mut dot = c0();
632                for i in 0..m {
633                    dot += u[i * k + prev].conj() * cand[i];
634                }
635                for i in 0..m {
636                    cand[i] -= dot * u[i * k + prev];
637                }
638            }
639            let nrm: f32 = cand.iter().map(|x| x.norm_sqr()).sum::<f32>().sqrt();
640            if nrm > 1e-6 {
641                let inv = 1.0 / nrm;
642                for i in 0..m {
643                    u[i * k + col] = cand[i] * inv;
644                }
645                placed = true;
646                break;
647            }
648        }
649        if !placed {
650            // Fall back to a zero column (only reachable for a fully rank-0 M).
651            for i in 0..m {
652                u[i * k + col] = c0();
653            }
654        }
655    }
656}
657
658/// Gram matrix `Mᴴ M` (n × n), Hermitian PSD.
659fn gram_ata(mat: &[Complex32], m: usize, n: usize) -> Vec<Complex32> {
660    let mut g = vec![c0(); n * n];
661    for a in 0..n {
662        for b in 0..n {
663            let mut acc = c0();
664            for i in 0..m {
665                acc += mat[i * n + a].conj() * mat[i * n + b];
666            }
667            g[a * n + b] = acc;
668        }
669    }
670    g
671}
672
673/// Gram matrix `M Mᴴ` (m × m), Hermitian PSD.
674fn gram_aat(mat: &[Complex32], m: usize, n: usize) -> Vec<Complex32> {
675    let mut g = vec![c0(); m * m];
676    for a in 0..m {
677        for b in 0..m {
678            let mut acc = c0();
679            for j in 0..n {
680                acc += mat[a * n + j] * mat[b * n + j].conj();
681            }
682            g[a * m + b] = acc;
683        }
684    }
685    g
686}
687
688/// Indices that sort `vals` in descending order.
689fn sorted_desc_indices(vals: &[f32]) -> Vec<usize> {
690    let mut idx: Vec<usize> = (0..vals.len()).collect();
691    idx.sort_by(|&i, &j| {
692        vals[j]
693            .partial_cmp(&vals[i])
694            .unwrap_or(std::cmp::Ordering::Equal)
695    });
696    idx
697}
698
699/// Cyclic Jacobi eigenvalue iteration for a complex Hermitian `n × n` matrix.
700///
701/// Returns `(eigenvalues, eigenvectors)` where eigenvectors are stored column-
702/// major-by-column in a row-major `(n × n)` buffer: column `j` of the result is
703/// `evecs[row * n + j]`, and `A · v_j = λ_j v_j`.
704///
705/// The complex Hermitian Jacobi rotation zeroes the off-diagonal pair `(p, q)`
706/// using a phase rotation followed by a real Jacobi (Givens) rotation; see
707/// Golub & Van Loan, *Matrix Computations*, §8.5. This mirrors the small-matrix
708/// eigensolver style used in [`crate::density::metrics`].
709fn jacobi_hermitian(a_in: &[Complex32], n: usize) -> QuantumResult<(Vec<f32>, Vec<Complex32>)> {
710    if a_in.len() != n * n {
711        return Err(QuantumError::DimensionMismatch {
712            expected: n * n,
713            got: a_in.len(),
714        });
715    }
716    let mut a = a_in.to_vec();
717    // Eigenvector accumulator, initialised to identity.
718    let mut v = vec![c0(); n * n];
719    for i in 0..n {
720        v[i * n + i] = Complex32::new(1.0, 0.0);
721    }
722    if n == 1 {
723        return Ok((vec![a[0].re], v));
724    }
725
726    let max_sweeps = 100usize;
727    for _sweep in 0..max_sweeps {
728        // Off-diagonal Frobenius norm.
729        let mut off = 0.0f32;
730        for p in 0..n {
731            for q in (p + 1)..n {
732                off += a[p * n + q].norm_sqr();
733            }
734        }
735        if off.sqrt() < 1e-12 {
736            break;
737        }
738
739        for p in 0..n {
740            for q in (p + 1)..n {
741                let apq = a[p * n + q];
742                if apq.norm() < 1e-18 {
743                    continue;
744                }
745                let app = a[p * n + p].re;
746                let aqq = a[q * n + q].re;
747
748                // Phase φ so that e^{-iφ} a_pq is real-positive.
749                let phi = apq.im.atan2(apq.re);
750                let (sin_phi, cos_phi) = phi.sin_cos();
751                let e_neg = Complex32::new(cos_phi, -sin_phi); // e^{-iφ}
752                let e_pos = Complex32::new(cos_phi, sin_phi); // e^{+iφ}
753                let abs_apq = apq.norm();
754
755                // Real Jacobi angle θ for the real symmetric problem
756                //   [[app, |a_pq|], [|a_pq|, aqq]].
757                let tau = (aqq - app) / (2.0 * abs_apq);
758                let t = if tau >= 0.0 {
759                    1.0 / (tau + (1.0 + tau * tau).sqrt())
760                } else {
761                    -1.0 / (-tau + (1.0 + tau * tau).sqrt())
762                };
763                let cos_t = 1.0 / (1.0 + t * t).sqrt();
764                let sin_t = t * cos_t;
765
766                // Combined rotation columns: the unitary acting on (p, q) is
767                //   J = [[ c,        s·e^{-iφ} ],
768                //        [ -s·e^{+iφ}, c        ]].
769                let c = cos_t;
770                let s_em = sin_phi_combine(sin_t, e_neg);
771                let s_ep = sin_phi_combine(sin_t, e_pos);
772
773                // Apply J on the left (rows p, q) and Jᴴ on the right
774                // (cols p, q) to A.
775                apply_jacobi_rotation(&mut a, n, p, q, c, s_em, s_ep);
776                // Accumulate eigenvectors: V = V · Jᴴ (rotate columns p, q).
777                accumulate_eigenvectors(&mut v, n, p, q, c, s_em, s_ep);
778            }
779        }
780    }
781
782    let eigenvalues: Vec<f32> = (0..n).map(|i| a[i * n + i].re).collect();
783    Ok((eigenvalues, v))
784}
785
786#[inline]
787fn sin_phi_combine(sin_t: f32, e: Complex32) -> Complex32 {
788    Complex32::new(sin_t * e.re, sin_t * e.im)
789}
790
791/// Apply the Hermitian Jacobi similarity `A ← J A Jᴴ` for the rotation acting on
792/// the `(p, q)` 2×2 subspace with
793///   J = [[c, s_em], [-s_ep, c]]   (s_em = s·e^{-iφ}, s_ep = s·e^{+iφ}).
794fn apply_jacobi_rotation(
795    a: &mut [Complex32],
796    n: usize,
797    p: usize,
798    q: usize,
799    c: f32,
800    s_em: Complex32,
801    s_ep: Complex32,
802) {
803    let cc = Complex32::new(c, 0.0);
804    // Left multiply: rows p and q. row_p' = c·row_p + s_em·row_q
805    //                               row_q' = -s_ep·row_p + c·row_q
806    for j in 0..n {
807        let rp = a[p * n + j];
808        let rq = a[q * n + j];
809        a[p * n + j] = cc * rp + s_em * rq;
810        a[q * n + j] = -s_ep * rp + cc * rq;
811    }
812    // Right multiply by Jᴴ: cols p and q.
813    // Jᴴ = [[c, -conj(s_ep)],[conj(s_em), c]]  acting on columns.
814    // col_p' = c·col_p + conj(s_em)·col_q
815    // col_q' = -conj(s_ep)·col_p + c·col_q
816    let s_em_c = s_em.conj();
817    let s_ep_c = s_ep.conj();
818    for i in 0..n {
819        let cp = a[i * n + p];
820        let cq = a[i * n + q];
821        a[i * n + p] = cc * cp + s_em_c * cq;
822        a[i * n + q] = -s_ep_c * cp + cc * cq;
823    }
824}
825
826/// Accumulate `V ← V · Jᴴ` (rotate eigenvector columns p and q).
827fn accumulate_eigenvectors(
828    v: &mut [Complex32],
829    n: usize,
830    p: usize,
831    q: usize,
832    c: f32,
833    s_em: Complex32,
834    s_ep: Complex32,
835) {
836    let cc = Complex32::new(c, 0.0);
837    let s_em_c = s_em.conj();
838    let s_ep_c = s_ep.conj();
839    for i in 0..n {
840        let cp = v[i * n + p];
841        let cq = v[i * n + q];
842        v[i * n + p] = cc * cp + s_em_c * cq;
843        v[i * n + q] = -s_ep_c * cp + cc * cq;
844    }
845}
846
847#[cfg(test)]
848mod tests {
849    use super::*;
850    use crate::gates::hadamard::gate_h;
851    use crate::gates::parametric::gate_ry;
852    use crate::gates::pauli::{gate_x, gate_z};
853    use crate::handle::LcgRng;
854    use crate::statevec::apply_1q::{apply_1q_controlled, apply_1q_inplace};
855    use crate::statevec::state::StateVector;
856
857    fn cfg(n: usize, chi: usize) -> MpsConfig {
858        MpsConfig::new(n, chi, 1e-12).expect("test config parameters are valid")
859    }
860
861    fn c(re: f32, im: f32) -> Complex32 {
862        Complex32::new(re, im)
863    }
864
865    /// 4×4 CNOT with control = q (more significant), target = q+1.
866    /// Row/col index = 2*bit(ctrl) + bit(tgt).
867    fn cnot_4x4() -> [[Complex32; 4]; 4] {
868        let o = c(1.0, 0.0);
869        let z = c0();
870        // |00>->|00>, |01>->|01>, |10>->|11>, |11>->|10>
871        [[o, z, z, z], [z, o, z, z], [z, z, z, o], [z, z, o, z]]
872    }
873
874    fn max_abs_diff(a: &[Complex32], b: &[Complex32]) -> f32 {
875        a.iter()
876            .zip(b.iter())
877            .map(|(x, y)| (x - y).norm())
878            .fold(0.0f32, f32::max)
879    }
880
881    #[test]
882    fn t01_product_state_is_zero_ket() {
883        let mps = MatrixProductState::new_zero_state(cfg(3, 4))
884            .expect("valid config produces zero state");
885        let sv = mps.to_statevector().expect("MPS converts to statevector");
886        assert!((sv.amps[0].re - 1.0).abs() < 1e-6);
887        for a in &sv.amps[1..] {
888            assert!(a.norm() < 1e-6);
889        }
890    }
891
892    #[test]
893    fn t02_pauli_x_flips_amplitude() {
894        let mut mps = MatrixProductState::new_zero_state(cfg(1, 2))
895            .expect("valid config produces zero state");
896        mps.apply_1q(&gate_x(), 0)
897            .expect("qubit 0 is in range for 1-qubit MPS");
898        let sv = mps.to_statevector().expect("MPS converts to statevector");
899        assert!(sv.amps[0].norm() < 1e-6);
900        assert!((sv.amps[1].re - 1.0).abs() < 1e-6);
901    }
902
903    #[test]
904    fn t03_hadamard_uniform_superposition() {
905        let mut mps = MatrixProductState::new_zero_state(cfg(1, 2))
906            .expect("valid config produces zero state");
907        mps.apply_1q(&gate_h(), 0)
908            .expect("qubit 0 is in range for 1-qubit MPS");
909        let sv = mps.to_statevector().expect("MPS converts to statevector");
910        let inv = std::f32::consts::FRAC_1_SQRT_2;
911        assert!((sv.amps[0].re - inv).abs() < 1e-5);
912        assert!((sv.amps[1].re - inv).abs() < 1e-5);
913    }
914
915    #[test]
916    fn t04_cnot_on_plus_zero_makes_bell_bond2() {
917        // |+0>: H on qubit 0, then CNOT(0->1). With qubit 0 the control and the
918        // less-significant index in the state vector, build the gate in the
919        // (q=0 control MSB-of-pair, q+1=1 target) convention.
920        let mut mps = MatrixProductState::new_zero_state(cfg(2, 4))
921            .expect("valid config produces zero state");
922        mps.apply_1q(&gate_h(), 0)
923            .expect("qubit 0 is in range for 2-qubit MPS");
924        mps.apply_2q(&cnot_4x4(), 0, 1)
925            .expect("qubits 0 and 1 are adjacent and in range");
926        let sv = mps.to_statevector().expect("MPS converts to statevector");
927        let inv = std::f32::consts::FRAC_1_SQRT_2;
928        // Bell |00> + |11>.
929        assert!((sv.amps[0].re - inv).abs() < 1e-5, "amps={:?}", sv.amps);
930        assert!((sv.amps[3].re - inv).abs() < 1e-5, "amps={:?}", sv.amps);
931        assert!(sv.amps[1].norm() < 1e-5);
932        assert!(sv.amps[2].norm() < 1e-5);
933        // Bond between the two sites must be 2.
934        assert_eq!(mps.bond_dims()[0], 2);
935    }
936
937    #[test]
938    fn t05_full_chi_matches_statevector_random_circuit() {
939        // Build the same Clifford+Ry circuit on both an MPS (full χ) and a
940        // dense StateVector, and compare amplitudes to ~1e-4.
941        let n = 5usize;
942        let chi = 1usize << n; // full bond, no truncation
943        let mut mps = MatrixProductState::new_zero_state(cfg(n, chi))
944            .expect("valid config produces zero state");
945        let mut sv =
946            StateVector::new_zero_state(n).expect("valid qubit count produces zero statevector");
947
948        let mut rng = LcgRng::new(12345);
949        let angles = [0.3f32, 1.1, -0.7, 2.0, 0.45, -1.3];
950        // Layer of Ry + H on each qubit.
951        for (q, &ang) in angles.iter().take(n).enumerate() {
952            let ry = gate_ry(ang);
953            mps.apply_1q(&ry, q)
954                .expect("qubit index q is within the n-qubit MPS");
955            apply_1q_inplace(&mut sv, q, &ry)
956                .expect("qubit index q is within the n-qubit statevector");
957            mps.apply_1q(&gate_h(), q)
958                .expect("qubit index q is within the n-qubit MPS");
959            apply_1q_inplace(&mut sv, q, &gate_h())
960                .expect("qubit index q is within the n-qubit statevector");
961        }
962        // Ladder of CNOTs on adjacent pairs.
963        for q in 0..(n - 1) {
964            mps.apply_2q(&cnot_4x4(), q, q + 1)
965                .expect("adjacent qubits q and q+1 are in range");
966            // Equivalent dense CNOT(control=q, target=q+1).
967            apply_1q_controlled(&mut sv, q, q + 1, &gate_x())
968                .expect("adjacent qubits q and q+1 are valid for controlled gate");
969        }
970        // A second Ry layer to entangle further.
971        for (q, &ang) in angles.iter().take(n).enumerate() {
972            let ry = gate_ry(ang * 0.5 + 0.2);
973            mps.apply_1q(&ry, q)
974                .expect("qubit index q is within the n-qubit MPS");
975            apply_1q_inplace(&mut sv, q, &ry)
976                .expect("qubit index q is within the n-qubit statevector");
977        }
978        let _ = &mut rng;
979
980        let mps_sv = mps.to_statevector().expect("MPS converts to statevector");
981        let diff = max_abs_diff(&mps_sv.amps, &sv.amps);
982        assert!(diff < 1e-4, "max amplitude diff {diff}");
983    }
984
985    #[test]
986    fn t06_truncation_reduces_bond() {
987        // Create a maximally entangling pair, then re-split with χ = 1 to force
988        // truncation of the bond from 2 down to 1.
989        let mut mps = MatrixProductState::new_zero_state(cfg(2, 1))
990            .expect("valid config produces zero state");
991        mps.apply_1q(&gate_h(), 0)
992            .expect("qubit 0 is in range for 2-qubit MPS");
993        // χ = 1 ⇒ the Bell bond (rank 2) is truncated to 1.
994        mps.apply_2q(&cnot_4x4(), 0, 1)
995            .expect("qubits 0 and 1 are adjacent and in range");
996        assert_eq!(mps.bond_dims()[0], 1, "bond should be truncated to χ=1");
997    }
998
999    #[test]
1000    fn t07_expectation_z_matches_statevector() {
1001        let n = 3usize;
1002        let chi = 1usize << n;
1003        let mut mps = MatrixProductState::new_zero_state(cfg(n, chi))
1004            .expect("valid config produces zero state");
1005        let mut sv =
1006            StateVector::new_zero_state(n).expect("valid qubit count produces zero statevector");
1007        let angs = [0.6f32, -1.2, 0.9];
1008        for (q, &a) in angs.iter().enumerate() {
1009            let g = gate_ry(a);
1010            mps.apply_1q(&g, q)
1011                .expect("qubit index q is within the n-qubit MPS");
1012            apply_1q_inplace(&mut sv, q, &g)
1013                .expect("qubit index q is within the n-qubit statevector");
1014        }
1015        for q in 0..n {
1016            let z_mps = mps
1017                .expectation_z(q)
1018                .expect("qubit index q is within the n-qubit MPS");
1019            // ⟨Z_q⟩ from statevector = Σ |amp|² (−1)^{bit q}.
1020            let mask = 1usize << q;
1021            let z_sv: f32 = sv
1022                .amps
1023                .iter()
1024                .enumerate()
1025                .map(|(i, a)| {
1026                    let sign = if i & mask != 0 { -1.0 } else { 1.0 };
1027                    sign * a.norm_sqr()
1028                })
1029                .sum();
1030            assert!((z_mps - z_sv).abs() < 1e-4, "q={q} mps={z_mps} sv={z_sv}");
1031        }
1032    }
1033
1034    #[test]
1035    fn t08_unitary_gates_preserve_norm() {
1036        let mut mps = MatrixProductState::new_zero_state(cfg(3, 8))
1037            .expect("n=3 and chi=8 are valid MPS configuration parameters");
1038        mps.apply_1q(&gate_h(), 0)
1039            .expect("qubit 0 is within the 3-qubit MPS range");
1040        mps.apply_2q(&cnot_4x4(), 0, 1)
1041            .expect("qubits 0 and 1 are adjacent and both within the 3-qubit MPS range");
1042        mps.apply_1q(&gate_ry(0.8), 2)
1043            .expect("qubit 2 is within the 3-qubit MPS range");
1044        mps.apply_2q(&cnot_4x4(), 1, 2)
1045            .expect("qubits 1 and 2 are adjacent and both within the 3-qubit MPS range");
1046        let nrm = mps.norm();
1047        assert!((nrm - 1.0).abs() < 1e-4, "norm={nrm}");
1048    }
1049
1050    #[test]
1051    fn t09_svd_reconstruction() {
1052        // Random-ish 4×3 complex matrix; check U Σ Vᴴ ≈ M.
1053        let m = 4usize;
1054        let n = 3usize;
1055        let mut mat = vec![c0(); m * n];
1056        let mut rng = LcgRng::new(99);
1057        for v in &mut mat {
1058            *v = c(rng.next_f32() - 0.5, rng.next_f32() - 0.5);
1059        }
1060        let svd = svd_dense(&mat, m, n)
1061            .expect("mat has exactly m*n=12 elements and m=4, n=3 are both positive");
1062        let k = svd.singular_values.len();
1063        // Reconstruct.
1064        let mut recon = vec![c0(); m * n];
1065        for i in 0..m {
1066            for j in 0..n {
1067                let mut acc = c0();
1068                for t in 0..k {
1069                    acc += svd.u[i * k + t]
1070                        * Complex32::new(svd.singular_values[t], 0.0)
1071                        * svd.v[j * k + t].conj();
1072                }
1073                recon[i * n + j] = acc;
1074            }
1075        }
1076        let diff = max_abs_diff(&recon, &mat);
1077        assert!(diff < 1e-4, "reconstruction diff {diff}");
1078        // Singular values descending and non-negative.
1079        for w in svd.singular_values.windows(2) {
1080            assert!(w[0] >= w[1] - 1e-6);
1081            assert!(w[1] >= -1e-6);
1082        }
1083    }
1084
1085    #[test]
1086    fn t10_svd_reconstruction_wide() {
1087        // Wide matrix m < n exercises the MMᴴ branch.
1088        let m = 2usize;
1089        let n = 5usize;
1090        let mut mat = vec![c0(); m * n];
1091        let mut rng = LcgRng::new(7);
1092        for v in &mut mat {
1093            *v = c(rng.next_f32() - 0.5, rng.next_f32() - 0.5);
1094        }
1095        let svd = svd_dense(&mat, m, n)
1096            .expect("mat has exactly m*n=10 elements and m=2, n=5 are both positive");
1097        let k = svd.singular_values.len();
1098        let mut recon = vec![c0(); m * n];
1099        for i in 0..m {
1100            for j in 0..n {
1101                let mut acc = c0();
1102                for t in 0..k {
1103                    acc += svd.u[i * k + t]
1104                        * Complex32::new(svd.singular_values[t], 0.0)
1105                        * svd.v[j * k + t].conj();
1106                }
1107                recon[i * n + j] = acc;
1108            }
1109        }
1110        let diff = max_abs_diff(&recon, &mat);
1111        assert!(diff < 1e-4, "wide reconstruction diff {diff}");
1112    }
1113
1114    #[test]
1115    fn t11_non_adjacent_apply_2q_errors() {
1116        let mut mps = MatrixProductState::new_zero_state(cfg(3, 4))
1117            .expect("n=3 and chi=4 are valid MPS configuration parameters");
1118        assert!(mps.apply_2q(&cnot_4x4(), 0, 2).is_err());
1119    }
1120
1121    #[test]
1122    fn t12_bond_never_exceeds_chi() {
1123        let n = 6usize;
1124        let chi = 3usize;
1125        let mut mps = MatrixProductState::new_zero_state(cfg(n, chi))
1126            .expect("n=6 and chi=3 are valid MPS configuration parameters");
1127        for q in 0..n {
1128            mps.apply_1q(&gate_h(), q)
1129                .expect("q iterates over 0..n so it is always within the n-qubit MPS range");
1130        }
1131        for q in 0..(n - 1) {
1132            mps.apply_2q(&cnot_4x4(), q, q + 1)
1133                .expect("q < n-1 so both q and q+1 are within range and adjacent");
1134            mps.apply_2q(&cnot_4x4(), q, q + 1)
1135                .expect("q < n-1 so both q and q+1 are within range and adjacent");
1136        }
1137        for b in mps.bond_dims() {
1138            assert!(b <= chi, "bond {b} exceeds χ={chi}");
1139        }
1140    }
1141
1142    #[test]
1143    fn t13_product_state_bonds_all_one() {
1144        let mps = MatrixProductState::new_zero_state(cfg(5, 4))
1145            .expect("n=5 and chi=4 are valid MPS configuration parameters");
1146        for b in mps.bond_dims() {
1147            assert_eq!(b, 1);
1148        }
1149    }
1150
1151    #[test]
1152    fn t14_out_of_range_qubit_errors() {
1153        let mut mps = MatrixProductState::new_zero_state(cfg(2, 4))
1154            .expect("n=2 and chi=4 are valid MPS configuration parameters");
1155        assert!(mps.apply_1q(&gate_x(), 5).is_err());
1156        assert!(mps.expectation_z(9).is_err());
1157    }
1158
1159    #[test]
1160    fn t15_zero_qubits_config_errors() {
1161        assert!(MpsConfig::new(0, 4, 1e-12).is_err());
1162        assert!(MpsConfig::new(2, 0, 1e-12).is_err());
1163    }
1164
1165    #[test]
1166    fn t16_normalize_restores_unit_norm() {
1167        let mut mps = MatrixProductState::new_zero_state(cfg(2, 4))
1168            .expect("n=2 and chi=4 are valid MPS configuration parameters");
1169        mps.apply_1q(&gate_h(), 0)
1170            .expect("qubit 0 is within the 2-qubit MPS range");
1171        // Scale the first site to break normalization.
1172        if let Some(first) = mps.sites.first_mut() {
1173            for v in &mut first.data {
1174                *v *= 3.0;
1175            }
1176        }
1177        assert!((mps.norm() - 3.0).abs() < 1e-4);
1178        mps.normalize();
1179        assert!((mps.norm() - 1.0).abs() < 1e-4, "norm={}", mps.norm());
1180    }
1181
1182    #[test]
1183    fn t17_expectation_z_on_basis_states() {
1184        // |0> ⇒ +1, |1> ⇒ −1.
1185        let mut mps = MatrixProductState::new_zero_state(cfg(1, 2))
1186            .expect("n=1 and chi=2 are valid MPS configuration parameters");
1187        assert!(
1188            (mps.expectation_z(0)
1189                .expect("qubit 0 is the only qubit in this 1-qubit MPS so it is always in range")
1190                - 1.0)
1191                .abs()
1192                < 1e-5
1193        );
1194        mps.apply_1q(&gate_x(), 0)
1195            .expect("qubit 0 is the only qubit in this 1-qubit MPS so it is always in range");
1196        assert!(
1197            (mps.expectation_z(0)
1198                .expect("qubit 0 is the only qubit in this 1-qubit MPS so it is always in range")
1199                + 1.0)
1200                .abs()
1201                < 1e-5
1202        );
1203    }
1204
1205    #[test]
1206    fn t18_gate_z_phase_then_statevector() {
1207        // Apply H then Z on qubit 0 of a 2-qubit register; compare to dense.
1208        let mut mps = MatrixProductState::new_zero_state(cfg(2, 4))
1209            .expect("n=2 and chi=4 are valid MPS configuration parameters");
1210        let mut sv = StateVector::new_zero_state(2)
1211            .expect("n=2 is a valid positive qubit count for a statevector");
1212        mps.apply_1q(&gate_h(), 1)
1213            .expect("qubit 1 is within the 2-qubit MPS range");
1214        apply_1q_inplace(&mut sv, 1, &gate_h())
1215            .expect("qubit 1 is within the 2-qubit statevector range");
1216        mps.apply_1q(&gate_z(), 1)
1217            .expect("qubit 1 is within the 2-qubit MPS range");
1218        apply_1q_inplace(&mut sv, 1, &gate_z())
1219            .expect("qubit 1 is within the 2-qubit statevector range");
1220        let mps_sv = mps
1221            .to_statevector()
1222            .expect("MPS was constructed with valid boundary bonds so contraction must succeed");
1223        let diff = max_abs_diff(&mps_sv.amps, &sv.amps);
1224        assert!(diff < 1e-5, "diff={diff}");
1225    }
1226}