Skip to main content

toroidal_noise/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use num_complex::Complex64;
4use std::f64::consts::{FRAC_1_SQRT_2, PI};
5
6/// A 2×2 complex matrix stored in row-major order.
7pub type Matrix2x2 = [[Complex64; 2]; 2];
8
9/// Identity matrix.
10pub const IDENTITY: Matrix2x2 = [
11    [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
12    [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
13];
14
15/// A Kraus operator: a 2×2 complex matrix K such that the channel
16/// maps ρ → Σᵢ Kᵢ ρ Kᵢ†.
17#[derive(Debug, Clone, Copy)]
18pub struct KrausOperator(pub Matrix2x2);
19
20impl KrausOperator {
21    /// Conjugate transpose K†.
22    #[must_use]
23    pub fn adjoint(&self) -> Self {
24        let m = &self.0;
25        Self([
26            [m[0][0].conj(), m[1][0].conj()],
27            [m[0][1].conj(), m[1][1].conj()],
28        ])
29    }
30
31    /// Matrix of the operator.
32    #[must_use]
33    pub const fn matrix(&self) -> &Matrix2x2 {
34        &self.0
35    }
36}
37
38/// Apply a channel (list of Kraus operators) to a 2×2 density matrix:
39/// ρ → Σᵢ Kᵢ ρ Kᵢ†
40#[must_use]
41pub fn apply_channel(rho: &Matrix2x2, kraus: &[KrausOperator]) -> Matrix2x2 {
42    let mut result = [[Complex64::new(0.0, 0.0); 2]; 2];
43    for k in kraus {
44        let kd = k.adjoint();
45        let k_rho = mul2x2(&k.0, rho);
46        let k_rho_kd = mul2x2(&k_rho, &kd.0);
47        for i in 0..2 {
48            for j in 0..2 {
49                result[i][j] += k_rho_kd[i][j];
50            }
51        }
52    }
53    result
54}
55
56/// Apply a unitary gate to a density matrix: ρ → U ρ U†
57#[must_use]
58pub fn apply_unitary(rho: &Matrix2x2, u: &Matrix2x2) -> Matrix2x2 {
59    let ud = adjoint2x2(u);
60    let u_rho = mul2x2(u, rho);
61    mul2x2(&u_rho, &ud)
62}
63
64/// Trace of a 2×2 matrix.
65#[must_use]
66pub fn trace(m: &Matrix2x2) -> Complex64 {
67    m[0][0] + m[1][1]
68}
69
70/// Check if Kraus operators satisfy trace preservation: Σᵢ Kᵢ† Kᵢ = I.
71#[must_use]
72pub fn is_trace_preserving(kraus: &[KrausOperator], tol: f64) -> bool {
73    let mut sum = [[Complex64::new(0.0, 0.0); 2]; 2];
74    for k in kraus {
75        let kd = k.adjoint();
76        let kdk = mul2x2(&kd.0, &k.0);
77        for i in 0..2 {
78            for j in 0..2 {
79                sum[i][j] += kdk[i][j];
80            }
81        }
82    }
83    (sum[0][0] - Complex64::new(1.0, 0.0)).norm() < tol
84        && (sum[0][1]).norm() < tol
85        && (sum[1][0]).norm() < tol
86        && (sum[1][1] - Complex64::new(1.0, 0.0)).norm() < tol
87}
88
89// ─────────────────────────────────────────────────────────────────────────────
90// Noise Channels
91// ─────────────────────────────────────────────────────────────────────────────
92
93/// Single-qubit phase damping channel.
94///
95/// Kraus operators:
96/// ```text
97/// K₀ = [[1, 0], [0, √(1-γ)]]
98/// K₁ = [[0, 0], [0, √γ]]
99/// ```
100///
101/// Off-diagonal elements decay as ρ₀₁ → ρ₀₁ · √(1-γ).
102///
103/// # Panics
104///
105/// Panics if `gamma` is not in `[0, 1]`.
106#[must_use]
107pub fn dephasing(gamma: f64) -> Vec<KrausOperator> {
108    assert!((0.0..=1.0).contains(&gamma), "gamma must be in [0, 1], got {gamma}");
109    let zero = Complex64::new(0.0, 0.0);
110    let one = Complex64::new(1.0, 0.0);
111    let k0 = KrausOperator([
112        [one, zero],
113        [zero, Complex64::new((1.0 - gamma).sqrt(), 0.0)],
114    ]);
115    let k1 = KrausOperator([
116        [zero, zero],
117        [zero, Complex64::new(gamma.sqrt(), 0.0)],
118    ]);
119    vec![k0, k1]
120}
121
122/// Single-qubit amplitude damping channel.
123///
124/// Kraus operators:
125/// ```text
126/// K₀ = [[1, 0], [0, √(1-γ)]]
127/// K₁ = [[0, √γ], [0, 0]]
128/// ```
129///
130/// The excited state |1⟩ decays to |0⟩ with probability γ.
131///
132/// # Panics
133///
134/// Panics if `gamma` is not in `[0, 1]`.
135#[must_use]
136pub fn amplitude_damping(gamma: f64) -> Vec<KrausOperator> {
137    assert!((0.0..=1.0).contains(&gamma), "gamma must be in [0, 1], got {gamma}");
138    let zero = Complex64::new(0.0, 0.0);
139    let one = Complex64::new(1.0, 0.0);
140    let k0 = KrausOperator([
141        [one, zero],
142        [zero, Complex64::new((1.0 - gamma).sqrt(), 0.0)],
143    ]);
144    let k1 = KrausOperator([
145        [zero, Complex64::new(gamma.sqrt(), 0.0)],
146        [zero, zero],
147    ]);
148    vec![k0, k1]
149}
150
151/// Single-qubit symmetric depolarizing channel.
152///
153/// Each Pauli error (X, Y, Z) applied with probability p/3.
154///
155/// Kraus operators:
156/// ```text
157/// K₀ = √(1-p) · I
158/// K₁ = √(p/3) · X
159/// K₂ = √(p/3) · Y
160/// K₃ = √(p/3) · Z
161/// ```
162///
163/// # Panics
164///
165/// Panics if `p` is not in `[0, 1]`.
166#[must_use]
167pub fn depolarizing(p: f64) -> Vec<KrausOperator> {
168    assert!((0.0..=1.0).contains(&p), "p must be in [0, 1], got {p}");
169    let zero = Complex64::new(0.0, 0.0);
170    let s0 = Complex64::new((1.0 - p).sqrt(), 0.0);
171    let sp = Complex64::new((p / 3.0).sqrt(), 0.0);
172    let im = Complex64::new(0.0, 1.0);
173
174    let k0 = KrausOperator([[s0, zero], [zero, s0]]);                    // √(1-p) I
175    let k1 = KrausOperator([[zero, sp], [sp, zero]]);                    // √(p/3) X
176    let k2 = KrausOperator([[zero, -sp * im], [sp * im, zero]]);         // √(p/3) Y
177    let k3 = KrausOperator([[sp, zero], [zero, -sp]]);                   // √(p/3) Z
178    vec![k0, k1, k2, k3]
179}
180
181/// Spectral gap of the cycle graph Cₙ Laplacian.
182///
183/// λ₁ = 2 − 2cos(2π/n)
184///
185/// This is also the smallest non-zero eigenvalue of the n×n discrete torus
186/// Laplacian Cₙ □ Cₙ, since product-graph eigenvalues are pairwise sums.
187///
188/// # Panics
189///
190/// Panics if `n < 2`.
191#[must_use]
192pub fn spectral_gap(n: usize) -> f64 {
193    assert!(n >= 2, "n must be >= 2, got {n}");
194    2.0 - 2.0 * (2.0 * PI / n as f64).cos()
195}
196
197/// Phenomenological effective dephasing rate from a toroidal lattice
198/// spectral gap.
199///
200/// ```text
201/// γ_eff = γ · λ₁(n) / (λ₁(n) + α)
202/// ```
203///
204/// where λ₁(n) is the cycle-graph spectral gap. The mapping has the property
205/// γ_eff → 0 as λ₁ → 0 and γ_eff → γ as λ₁ → ∞.
206///
207/// # Status
208///
209/// This mapping is **phenomenological**. It is offered as a parameterization
210/// for studying lattice-geometry-dependent dephasing, not as a derivation
211/// of dephasing rates from physical first principles. Use `alpha` as a
212/// tunable knob, not as a physically calibrated coupling.
213///
214/// # Panics
215///
216/// Panics if `gamma` is not in `[0, 1]`, `grid_n < 2`, or `alpha <= 0.0`.
217#[must_use]
218pub fn effective_gamma(gamma: f64, grid_n: usize, alpha: f64) -> f64 {
219    assert!((0.0..=1.0).contains(&gamma), "gamma must be in [0, 1], got {gamma}");
220    assert!(grid_n >= 2, "grid_n must be >= 2, got {grid_n}");
221    assert!(alpha > 0.0, "alpha must be positive, got {alpha}");
222    let lambda1 = spectral_gap(grid_n);
223    gamma * lambda1 / (lambda1 + alpha)
224}
225
226/// Single-qubit dephasing parameterized by a toroidal lattice spectral gap.
227///
228/// Returns the same Kraus operators as [`dephasing`] but with `gamma`
229/// replaced by [`effective_gamma`]`(gamma, grid_n, alpha)`. This function
230/// is a thin wrapper: it delegates the Kraus construction to [`dephasing`].
231///
232/// | grid_n | λ₁(n)   | γ_eff/γ (α=1) |
233/// |--------|---------|----------------|
234/// | 4      | 2.000   | 0.667          |
235/// | 6      | 1.000   | 0.500          |
236/// | 8      | 0.586   | 0.369          |
237/// | 12     | 0.268   | 0.211          |
238/// | 32     | 0.0383  | 0.0369         |
239///
240/// # Status
241///
242/// Phenomenological — see [`effective_gamma`] for the caveat. The
243/// spectral-gap-to-dephasing mapping does not correspond to any particular
244/// standard physical mechanism and should not be interpreted as a hardware
245/// prediction without independent justification.
246///
247/// # Panics
248///
249/// Panics if `gamma` is not in `[0, 1]`, `grid_n < 2`, or `alpha <= 0.0`.
250#[must_use]
251pub fn toroidal_dephasing(gamma: f64, grid_n: usize, alpha: f64) -> Vec<KrausOperator> {
252    dephasing(effective_gamma(gamma, grid_n, alpha))
253}
254
255// ─────────────────────────────────────────────────────────────────────────────
256// Common gates (for density matrix simulation)
257// ─────────────────────────────────────────────────────────────────────────────
258
259/// Hadamard gate.
260pub const HADAMARD: Matrix2x2 = {
261    let s = FRAC_1_SQRT_2;
262    [
263        [Complex64::new(s, 0.0), Complex64::new(s, 0.0)],
264        [Complex64::new(s, 0.0), Complex64::new(-s, 0.0)],
265    ]
266};
267
268/// Pauli X gate.
269pub const SIGMA_X: Matrix2x2 = [
270    [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
271    [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
272];
273
274/// |0⟩⟨0| density matrix.
275pub const RHO_ZERO: Matrix2x2 = [
276    [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
277    [Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0)],
278];
279
280// ─────────────────────────────────────────────────────────────────────────────
281// Internal 2×2 matrix arithmetic
282// ─────────────────────────────────────────────────────────────────────────────
283
284fn mul2x2(a: &Matrix2x2, b: &Matrix2x2) -> Matrix2x2 {
285    [
286        [
287            a[0][0] * b[0][0] + a[0][1] * b[1][0],
288            a[0][0] * b[0][1] + a[0][1] * b[1][1],
289        ],
290        [
291            a[1][0] * b[0][0] + a[1][1] * b[1][0],
292            a[1][0] * b[0][1] + a[1][1] * b[1][1],
293        ],
294    ]
295}
296
297fn adjoint2x2(m: &Matrix2x2) -> Matrix2x2 {
298    [
299        [m[0][0].conj(), m[1][0].conj()],
300        [m[0][1].conj(), m[1][1].conj()],
301    ]
302}
303
304// ─────────────────────────────────────────────────────────────────────────────
305// Tests
306// ─────────────────────────────────────────────────────────────────────────────
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    const TOL: f64 = 1e-12;
313
314    fn approx_eq(a: Complex64, b: Complex64) -> bool {
315        (a - b).norm() < TOL
316    }
317
318    fn approx_eq_f64(a: f64, b: f64) -> bool {
319        (a - b).abs() < TOL
320    }
321
322    // --- Dephasing ---
323
324    #[test]
325    fn dephasing_gamma0_is_identity() {
326        let k = dephasing(0.0);
327        assert!(approx_eq(k[0].0[1][1], Complex64::new(1.0, 0.0)));
328        assert!(approx_eq(k[1].0[1][1], Complex64::new(0.0, 0.0)));
329    }
330
331    #[test]
332    fn dephasing_gamma1_full() {
333        let k = dephasing(1.0);
334        assert!(approx_eq(k[0].0[1][1], Complex64::new(0.0, 0.0)));
335        assert!(approx_eq(k[1].0[1][1], Complex64::new(1.0, 0.0)));
336    }
337
338    #[test]
339    fn dephasing_trace_preserving() {
340        for &g in &[0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0] {
341            assert!(is_trace_preserving(&dephasing(g), TOL));
342        }
343    }
344
345    #[test]
346    fn dephasing_off_diagonal_decay() {
347        for &g in &[0.1, 0.3, 0.5, 0.9] {
348            let rho = apply_unitary(&RHO_ZERO, &HADAMARD); // |+⟩⟨+|
349            let rho_noisy = apply_channel(&rho, &dephasing(g));
350            let expected = 0.5 * (1.0 - g).sqrt();
351            assert!(approx_eq_f64(rho_noisy[0][1].re, expected));
352        }
353    }
354
355    #[test]
356    #[should_panic]
357    fn dephasing_invalid_negative() {
358        let _ = dephasing(-0.1);
359    }
360
361    #[test]
362    #[should_panic]
363    fn dephasing_invalid_above_one() {
364        let _ = dephasing(1.5);
365    }
366
367    // --- Amplitude Damping ---
368
369    #[test]
370    fn amplitude_damping_gamma0_identity() {
371        let k = amplitude_damping(0.0);
372        assert!(is_trace_preserving(&k, TOL));
373        assert!(approx_eq(k[0].0[1][1], Complex64::new(1.0, 0.0)));
374    }
375
376    #[test]
377    fn amplitude_damping_full_decay() {
378        let rho = apply_unitary(&RHO_ZERO, &SIGMA_X); // |1⟩⟨1|
379        let rho_out = apply_channel(&rho, &amplitude_damping(1.0));
380        assert!(approx_eq_f64(rho_out[0][0].re, 1.0)); // fully decayed to |0⟩
381        assert!(approx_eq_f64(rho_out[1][1].re, 0.0));
382    }
383
384    #[test]
385    fn amplitude_damping_partial() {
386        let rho = apply_unitary(&RHO_ZERO, &SIGMA_X); // |1⟩⟨1|
387        let rho_out = apply_channel(&rho, &amplitude_damping(0.3));
388        assert!(approx_eq_f64(rho_out[0][0].re, 0.3));
389        assert!(approx_eq_f64(rho_out[1][1].re, 0.7));
390    }
391
392    #[test]
393    fn amplitude_damping_trace_preserving() {
394        for &g in &[0.0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0] {
395            assert!(is_trace_preserving(&amplitude_damping(g), TOL));
396        }
397    }
398
399    // --- Depolarizing ---
400
401    #[test]
402    fn depolarizing_p0_identity() {
403        let k = depolarizing(0.0);
404        assert!(approx_eq(k[0].0[0][0], Complex64::new(1.0, 0.0)));
405    }
406
407    #[test]
408    fn depolarizing_trace_preserving() {
409        for &p in &[0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0] {
410            assert!(is_trace_preserving(&depolarizing(p), TOL));
411        }
412    }
413
414    #[test]
415    fn depolarizing_full_on_ground() {
416        let rho_out = apply_channel(&RHO_ZERO, &depolarizing(1.0));
417        assert!(approx_eq_f64(rho_out[0][0].re, 1.0 / 3.0));
418        assert!(approx_eq_f64(rho_out[1][1].re, 2.0 / 3.0));
419    }
420
421    #[test]
422    fn depolarizing_returns_four_kraus() {
423        assert_eq!(depolarizing(0.5).len(), 4);
424    }
425
426    // --- Spectral Gap ---
427
428    #[test]
429    fn spectral_gap_known_values() {
430        assert!(approx_eq_f64(spectral_gap(2), 4.0));
431        assert!(approx_eq_f64(spectral_gap(3), 3.0));
432        assert!(approx_eq_f64(spectral_gap(4), 2.0));
433        assert!(approx_eq_f64(spectral_gap(6), 1.0));
434    }
435
436    #[test]
437    fn spectral_gap_monotone_decreasing() {
438        let mut prev = spectral_gap(2);
439        for n in [3, 4, 6, 8, 16, 32, 64] {
440            let sg = spectral_gap(n);
441            assert!(sg < prev, "spectral_gap({n}) = {sg} >= {prev}");
442            prev = sg;
443        }
444    }
445
446    #[test]
447    fn spectral_gap_always_positive() {
448        for n in [2, 3, 4, 8, 16, 64, 128] {
449            assert!(spectral_gap(n) > 0.0);
450        }
451    }
452
453    // --- Effective gamma ---
454
455    #[test]
456    fn effective_gamma_explicit_formula() {
457        let gamma = 0.5_f64;
458        let n = 8;
459        let alpha = 1.5_f64;
460        let lam = spectral_gap(n);
461        assert!(approx_eq_f64(
462            effective_gamma(gamma, n, alpha),
463            gamma * lam / (lam + alpha),
464        ));
465    }
466
467    #[test]
468    fn effective_gamma_zero_in_zero_out() {
469        assert_eq!(effective_gamma(0.0, 12, 1.0), 0.0);
470    }
471
472    #[test]
473    fn effective_gamma_alpha_to_zero_recovers_gamma() {
474        let gamma = 0.7_f64;
475        let eg = effective_gamma(gamma, 12, 1e-12);
476        assert!((eg - gamma).abs() < 1e-6);
477    }
478
479    #[test]
480    #[should_panic]
481    fn effective_gamma_alpha_zero_panics() {
482        let _ = effective_gamma(0.5, 12, 0.0);
483    }
484
485    // --- Toroidal Dephasing ---
486
487    #[test]
488    fn toroidal_gamma0_identity() {
489        let k = toroidal_dephasing(0.0, 12, 1.0);
490        assert!(approx_eq(k[0].0[1][1], Complex64::new(1.0, 0.0)));
491        assert!(approx_eq(k[1].0[1][1], Complex64::new(0.0, 0.0)));
492    }
493
494    #[test]
495    fn toroidal_matches_dephasing_with_effective_gamma() {
496        // Delegation contract: toroidal_dephasing(g, n, a) ≡ dephasing(effective_gamma(g, n, a))
497        for &g in &[0.0, 0.25, 0.5, 0.75, 1.0] {
498            for n in [4, 8, 12, 32] {
499                for &a in &[0.5, 1.0, 2.0] {
500                    let k_t = toroidal_dephasing(g, n, a);
501                    let k_d = dephasing(effective_gamma(g, n, a));
502                    assert_eq!(k_t.len(), k_d.len());
503                    for (kt, kd) in k_t.iter().zip(k_d.iter()) {
504                        for i in 0..2 {
505                            for j in 0..2 {
506                                assert!(approx_eq(kt.0[i][j], kd.0[i][j]));
507                            }
508                        }
509                    }
510                }
511            }
512        }
513    }
514
515    #[test]
516    fn toroidal_smaller_gap_means_smaller_eff_gamma() {
517        // With this phenomenological mapping, smaller spectral gap (larger n)
518        // produces smaller γ_eff. Note this is a property of the formula, not a
519        // physical claim — see effective_gamma docstring.
520        let rho = apply_unitary(&RHO_ZERO, &HADAMARD);
521        let rho_plain = apply_channel(&rho, &dephasing(0.5));
522        let rho_torus = apply_channel(&rho, &toroidal_dephasing(0.5, 12, 1.0));
523        assert!(rho_torus[0][1].norm() > rho_plain[0][1].norm());
524    }
525
526    #[test]
527    fn toroidal_larger_grid_smaller_eff_gamma() {
528        let k_small = toroidal_dephasing(0.5, 4, 1.0);
529        let k_large = toroidal_dephasing(0.5, 32, 1.0);
530        let g_small = k_small[1].0[1][1].norm().powi(2);
531        let g_large = k_large[1].0[1][1].norm().powi(2);
532        assert!(g_large < g_small);
533    }
534
535    #[test]
536    fn toroidal_monotonic_in_grid_n() {
537        let mut prev_g = 1.0;
538        for n in [4, 6, 8, 12, 16, 32, 64] {
539            let k = toroidal_dephasing(1.0, n, 1.0);
540            let g_eff = k[1].0[1][1].norm().powi(2);
541            assert!(g_eff < prev_g);
542            prev_g = g_eff;
543        }
544    }
545
546    #[test]
547    fn toroidal_known_value() {
548        let k = toroidal_dephasing(1.0, 4, 1.0);
549        let g_eff = k[1].0[1][1].norm().powi(2);
550        let l1 = spectral_gap(4);
551        let expected = l1 / (l1 + 1.0);
552        assert!(approx_eq_f64(g_eff, expected));
553    }
554
555    #[test]
556    fn toroidal_trace_preserving() {
557        for &g in &[0.0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0] {
558            for n in [2, 4, 6, 8, 12, 32] {
559                assert!(is_trace_preserving(&toroidal_dephasing(g, n, 1.0), TOL));
560            }
561        }
562    }
563
564    #[test]
565    fn toroidal_analytical_value() {
566        let gamma = 0.5;
567        let n = 12;
568        let alpha = 1.0;
569        let l1 = spectral_gap(n);
570        let g_eff = gamma * l1 / (l1 + alpha);
571        let rho = apply_unitary(&RHO_ZERO, &HADAMARD);
572        let rho_out = apply_channel(&rho, &toroidal_dephasing(gamma, n, alpha));
573        let expected_off_diag = 0.5 * (1.0 - g_eff).sqrt();
574        assert!(approx_eq_f64(rho_out[0][1].re, expected_off_diag));
575    }
576
577    #[test]
578    #[should_panic]
579    fn toroidal_invalid_grid() {
580        let _ = toroidal_dephasing(0.5, 1, 1.0);
581    }
582
583    #[test]
584    #[should_panic]
585    fn toroidal_invalid_alpha() {
586        let _ = toroidal_dephasing(0.5, 12, 0.0);
587    }
588
589    // --- Density matrix properties ---
590
591    #[test]
592    fn density_matrix_hermitian_after_noise() {
593        let rho = apply_unitary(&RHO_ZERO, &HADAMARD);
594        let rho_out = apply_channel(&rho, &dephasing(0.3));
595        assert!(approx_eq(rho_out[0][1], rho_out[1][0].conj()));
596    }
597
598    #[test]
599    fn density_matrix_unit_trace_after_noise() {
600        for &g in &[0.0, 0.1, 0.5, 1.0] {
601            let rho = apply_unitary(&RHO_ZERO, &HADAMARD);
602            let rho_out = apply_channel(&rho, &dephasing(g));
603            assert!(approx_eq_f64(trace(&rho_out).re, 1.0));
604        }
605    }
606
607    #[test]
608    fn composed_noise() {
609        let rho = apply_unitary(&RHO_ZERO, &HADAMARD);
610        let rho = apply_channel(&rho, &dephasing(0.3));
611        let rho = apply_channel(&rho, &amplitude_damping(0.2));
612        assert!(approx_eq_f64(trace(&rho).re, 1.0));
613    }
614
615    #[test]
616    fn full_dephasing_destroys_coherence() {
617        let rho = apply_unitary(&RHO_ZERO, &HADAMARD);
618        let rho_out = apply_channel(&rho, &dephasing(1.0));
619        assert!(rho_out[0][1].norm() < TOL);
620        assert!(rho_out[1][0].norm() < TOL);
621        assert!(approx_eq_f64(rho_out[0][0].re, 0.5));
622        assert!(approx_eq_f64(rho_out[1][1].re, 0.5));
623    }
624}