1#![doc = include_str!("../README.md")]
2
3use num_complex::Complex64;
4use std::f64::consts::{FRAC_1_SQRT_2, PI};
5
6pub type Matrix2x2 = [[Complex64; 2]; 2];
8
9pub 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#[derive(Debug, Clone, Copy)]
18pub struct KrausOperator(pub Matrix2x2);
19
20impl KrausOperator {
21 #[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 #[must_use]
33 pub const fn matrix(&self) -> &Matrix2x2 {
34 &self.0
35 }
36}
37
38#[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#[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#[must_use]
66pub fn trace(m: &Matrix2x2) -> Complex64 {
67 m[0][0] + m[1][1]
68}
69
70#[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#[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#[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#[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]]); let k1 = KrausOperator([[zero, sp], [sp, zero]]); let k2 = KrausOperator([[zero, -sp * im], [sp * im, zero]]); let k3 = KrausOperator([[sp, zero], [zero, -sp]]); vec![k0, k1, k2, k3]
179}
180
181#[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#[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#[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
255pub 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
268pub 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
274pub 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
280fn 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#[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 #[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); 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 #[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); let rho_out = apply_channel(&rho, &litude_damping(1.0));
380 assert!(approx_eq_f64(rho_out[0][0].re, 1.0)); 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); let rho_out = apply_channel(&rho, &litude_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(&litude_damping(g), TOL));
396 }
397 }
398
399 #[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 #[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 #[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 #[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 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 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 #[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, &litude_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}