1use std::fmt::Debug;
5use std::ops::Add;
6
7use bitvec::view::BitView;
8use num::PrimInt;
9use num::complex::{Complex, Complex64, ComplexFloat};
10use num::traits::{One, ToPrimitive, Zero};
11
12use crate::prelude::*;
13use rand::RngExt;
14use rand::rngs::SmallRng;
15
16impl<T: Config> TableauLike for Tableau<T>
22where
23 T::Coeff: PartialOrd<f64>,
24 <T::Storage as BitView>::Store: PrimInt,
29{
30 type Coeff = T::Coeff;
31 type Rng = SmallRng;
32
33 #[inline]
34 fn rng_mut(&mut self) -> &mut Self::Rng {
35 &mut self.rng
36 }
37}
38
39impl<T: Config, I: TableauIndex, C: SparseVector<Complex<T::Coeff>, I>> TableauLike
40 for GeneralizedTableau<T, I, C>
41where
42 T::Coeff: PartialOrd<f64>,
43 Complex<T::Coeff>: From<Complex<f64>>,
44 <T::Storage as BitView>::Store: PrimInt,
45{
46 type Coeff = T::Coeff;
47 type Rng = SmallRng;
48
49 #[inline]
50 fn rng_mut(&mut self) -> &mut Self::Rng {
51 &mut self.tableau.rng
52 }
53
54 #[inline]
56 fn is_qubit_lost(&self, addr: usize) -> bool {
57 self.is_inactive(addr)
58 }
59}
60
61pub fn is_admissible_correlated_loss<C>(p: &[C; 3]) -> bool
75where
76 C: Clone + PartialOrd<f64> + Add<Output = C>,
77{
78 const SLACK: f64 = 1e-9;
83 p[0] >= 0.0
84 && p[1] >= 0.0
85 && p[2] >= 0.0
86 && p[2] <= 1.0
87 && p[0].clone() + p[1].clone() + p[1].clone() <= 1.0 + SLACK
88}
89
90macro_rules! impl_tableau_noise {
97 (generics: [$($gen:tt)*], ty: $ty:ty, where: [$($bound:tt)*] $(,)?) => {
98 impl<T: Config $($gen)*> Depolarizing<T> for $ty
99 where $($bound)*
100 {
101 fn depolarize1(&mut self, addr0: usize, p: T::Coeff) {
102 self.depolarize_impl(addr0, p);
103 }
104 }
105
106 impl<T: Config $($gen)*> PauliError<T> for $ty
107 where $($bound)*
108 {
109 fn pauli_error(&mut self, addr0: usize, p: [T::Coeff; 3]) {
110 self.pauli_error_impl(addr0, p);
111 }
112 }
113
114 impl<T: Config $($gen)*> TwoQubitPauliError<T> for $ty
115 where $($bound)*
116 {
117 fn two_qubit_pauli_error(&mut self, addr0: usize, addr1: usize, p: [T::Coeff; 15]) {
118 self.two_qubit_pauli_error_impl(addr0, addr1, p);
119 }
120 }
121
122 impl<T: Config $($gen)*> Depolarizing2<T> for $ty
123 where $($bound)*
124 {
125 fn depolarize2(&mut self, addr0: usize, addr1: usize, p: T::Coeff) {
126 self.depolarize2_impl(addr0, addr1, p);
127 }
128 }
129 };
130}
131
132impl_tableau_noise! {
133 generics: [],
134 ty: Tableau<T>,
135 where: [
136 T::Coeff: PartialOrd<f64>,
137 <T::Storage as BitView>::Store: PrimInt,
138 ],
139}
140
141impl_tableau_noise! {
142 generics: [, I: TableauIndex, C: SparseVector<Complex<T::Coeff>, I>],
143 ty: GeneralizedTableau<T, I, C>,
144 where: [
145 T::Coeff: PartialOrd<f64>,
146 Complex<T::Coeff>: From<Complex<f64>>,
147 <T::Storage as BitView>::Store: PrimInt,
148 ],
149}
150
151impl<T: Config, I: TableauIndex + Send + Sync, C: SparseVector<Complex<T::Coeff>, I>> LossChannel<T>
154 for GeneralizedTableau<T, I, C>
155where
156 <<T as Config>::Storage as BitView>::Store: PrimInt,
157 C: std::fmt::Debug,
158 T::Coeff: PartialOrd<f64>
159 + One
160 + Zero
161 + Clone
162 + num::Num
163 + ToPrimitive
164 + std::fmt::Debug
165 + Send
166 + Sync,
167 Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
168 + From<Complex64>
169 + std::ops::MulAssign
170 + std::ops::AddAssign
171 + One
172 + ComplexFloat
173 + Copy,
174 I: Debug,
175{
176 fn loss_channel(&mut self, addr0: usize, p: <T as Config>::Coeff) {
177 if p < self.tableau.rng.random::<f64>() {
178 return;
179 }
180 if self.qubit_status[addr0] == QubitStatus::Lost {
181 return;
182 }
183
184 self.qubit_status[addr0] = QubitStatus::Live;
188 let outcome = self.measure(addr0);
190 self.measurement_record.pop();
193 if let Some(true) = outcome {
194 self.x(addr0);
196 }
197 self.qubit_status[addr0] = QubitStatus::Lost;
198 }
199}
200
201impl<T: Config, I: TableauIndex + Send + Sync, C: SparseVector<Complex<T::Coeff>, I>>
202 AsymmetricLossChannel<T> for GeneralizedTableau<T, I, C>
203where
204 <<T as Config>::Storage as BitView>::Store: PrimInt,
205 C: std::fmt::Debug,
206 T::Coeff: PartialOrd<f64>
207 + One
208 + Zero
209 + Clone
210 + num::Num
211 + ToPrimitive
212 + std::fmt::Debug
213 + Send
214 + Sync,
215 Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
216 + From<Complex64>
217 + std::ops::MulAssign
218 + std::ops::AddAssign
219 + One
220 + ComplexFloat
221 + Copy,
222 I: Debug,
223{
224 fn asymmetric_loss_channel(&mut self, addr0: usize, p0: T::Coeff, p1: T::Coeff) {
252 if self.qubit_status[addr0] == QubitStatus::Lost {
253 return;
254 }
255 let z = self.z_expectation(addr0);
257 let p_tot = p0.to_f64().unwrap() * 0.5 * (1.0 + z) + p1.to_f64().unwrap() * 0.5 * (1.0 - z);
258
259 if p_tot < self.tableau.rng.random::<f64>() {
260 return;
261 }
262 self.qubit_status[addr0] = QubitStatus::Live;
265 if let Some(true) = self.measure(addr0) {
266 self.x(addr0);
267 }
268 self.measurement_record.pop();
269 self.qubit_status[addr0] = QubitStatus::Lost;
270 }
271}
272
273impl<T: Config, I: TableauIndex + Send + Sync, C: SparseVector<Complex<T::Coeff>, I>>
274 CorrelatedLossChannel<T> for GeneralizedTableau<T, I, C>
275where
276 <<T as Config>::Storage as BitView>::Store: PrimInt,
277 C: std::fmt::Debug,
278 T::Coeff: PartialOrd<f64>
279 + One
280 + Zero
281 + Clone
282 + num::Num
283 + ToPrimitive
284 + std::fmt::Debug
285 + Send
286 + Sync,
287 Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
288 + From<Complex64>
289 + std::ops::MulAssign
290 + std::ops::AddAssign
291 + One
292 + ComplexFloat
293 + Copy,
294 I: Debug,
295{
296 fn correlated_loss_channel(
310 &mut self,
311 addr0: usize,
312 addr1: usize,
313 p: [<T as Config>::Coeff; 3],
314 ) {
315 debug_assert!(
316 is_admissible_correlated_loss(&p),
317 "correlated loss needs p0, p1 >= 0, p0 + 2*p1 <= 1, p2 in [0, 1]; got {p:?}"
318 );
319
320 if self.qubit_status[addr0] == QubitStatus::Lost {
321 self.loss_channel(addr1, p[2].clone());
322 return;
323 } else if self.qubit_status[addr1] == QubitStatus::Lost {
324 self.loss_channel(addr0, p[2].clone());
325 return;
326 }
327 let r = self.tableau.rng.random::<f64>();
333 let mut cumulative = T::Coeff::zero();
334 for (i, p_i) in p[..2].iter().enumerate() {
335 cumulative += if i == 1 {
339 p_i.clone() + p_i.clone()
340 } else {
341 p_i.clone()
342 };
343 if cumulative > r {
344 if i == 0 {
345 self.qubit_status[addr0] = QubitStatus::Live;
347 self.qubit_status[addr1] = QubitStatus::Live;
348 self.reset(addr0);
349 self.reset(addr1);
350 self.qubit_status[addr0] = QubitStatus::Lost;
351 self.qubit_status[addr1] = QubitStatus::Lost;
352 } else {
353 let choice = self.tableau.rng.random::<bool>();
355 if choice {
356 self.qubit_status[addr1] = QubitStatus::Live;
357 self.reset(addr1);
358 self.qubit_status[addr1] = QubitStatus::Lost;
359 } else {
360 self.qubit_status[addr0] = QubitStatus::Live;
361 self.reset(addr0);
362 self.qubit_status[addr0] = QubitStatus::Lost;
363 }
364 }
365 return;
366 }
367 }
368 }
369}
370
371impl<T: Config, I: TableauIndex, C: SparseVector<Complex<T::Coeff>, I>> ResetLossChannel<T>
372 for GeneralizedTableau<T, I, C>
373{
374 fn reset_loss_channel(&mut self, addr0: usize) {
375 if self.qubit_status[addr0] == QubitStatus::Lost {
376 self.qubit_status[addr0] = QubitStatus::Live;
377 }
378 }
379}
380
381impl<T: Config, I: TableauIndex, C: SparseVector<Complex<T::Coeff>, I>> LeakageChannel<T>
382 for GeneralizedTableau<T, I, C>
383where
384 <<T as Config>::Storage as BitView>::Store: PrimInt,
385 C: std::fmt::Debug,
386 T::Coeff: PartialOrd
387 + PartialOrd<f64>
388 + One
389 + Zero
390 + Clone
391 + num::Num
392 + ToPrimitive
393 + std::fmt::Debug,
394 Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
395 + From<Complex64>
396 + std::ops::MulAssign
397 + std::ops::AddAssign
398 + One
399 + ComplexFloat
400 + Copy,
401 I: Debug,
402{
403 fn leakage_channel(&mut self, addr0: usize, p0: T::Coeff, p1: T::Coeff) {
404 if self.is_inactive(addr0) {
405 return;
406 }
407
408 debug_assert!(T::Coeff::zero() <= p0 && p0 <= T::Coeff::one());
409 debug_assert!(T::Coeff::zero() <= p1 && p1 <= T::Coeff::one());
410 debug_assert!(
411 T::Coeff::zero() <= p0.clone() + p1.clone()
412 && p0.clone() + p1.clone() <= T::Coeff::one()
413 );
414
415 let p_tot = p0.clone() + p1;
416 let r = self.tableau.rng.random::<f64>();
417
418 if p_tot <= r {
419 return;
420 }
421
422 let m = self
426 .measure(addr0)
427 .expect("qubit was checked active before, this should be unreachable");
428 self.measurement_record.pop();
429
430 if p0 > r {
435 if m {
436 self.x(addr0);
437 }
438 } else if !m {
439 self.x(addr0);
440 }
441 self.qubit_status[addr0] = QubitStatus::Leaked;
442 }
443}
444
445impl<T, I, C> ResetLeakageChannel<T> for GeneralizedTableau<T, I, C>
446where
447 T: Config,
448 <<T as Config>::Storage as BitView>::Store: PrimInt,
449 I: TableauIndex + Debug,
450 C: SparseVector<Complex<T::Coeff>, I> + Debug,
451 T::Coeff: One
452 + Zero
453 + Clone
454 + num::Num
455 + ToPrimitive
456 + std::fmt::Debug
457 + std::ops::Mul<f64>
458 + PartialOrd<f64>,
459 Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
460 + From<Complex64>
461 + std::ops::MulAssign
462 + std::ops::AddAssign
463 + One
464 + ComplexFloat
465 + Copy,
466{
467 fn reset_leakage_channel(&mut self, addr0: usize) {
468 if self.qubit_status[addr0] != QubitStatus::Leaked {
469 return;
471 }
472
473 self.qubit_status[addr0] = QubitStatus::Live;
474 self.reset(addr0);
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use ppvm_pauli_sum::config::fxhash::ByteF64;
482
483 type TestConfig = ByteF64<1>;
484 type TestTab = GeneralizedTableau<TestConfig>;
485
486 fn tab(n: usize) -> TestTab {
487 GeneralizedTableau::new(n, 1e-12)
488 }
489
490 #[test]
493 fn depolarize_p0_no_change() {
494 let mut t = tab(1);
495 t.depolarize1(0, 0.0);
496 assert!(!t.measure(0).unwrap());
497 }
498
499 #[test]
500 fn depolarize_p1_does_not_mark_lost() {
501 let mut t = tab(1);
503 t.depolarize1(0, 1.0);
504 assert!(!t.is_lost(0));
505 }
506
507 #[test]
510 fn pauli_error_zero_prob_no_change() {
511 let mut t = tab(1);
512 t.pauli_error(0, [0.0, 0.0, 0.0]);
513 assert!(!t.measure(0).unwrap());
514 }
515
516 #[test]
517 fn pauli_error_x_flips_qubit() {
518 let mut t = tab(1);
519 t.pauli_error(0, [1.0, 0.0, 0.0]); assert!(t.measure(0).unwrap());
521 }
522
523 #[test]
524 fn pauli_error_y_flips_qubit() {
525 let mut t = tab(1);
526 t.pauli_error(0, [0.0, 1.0, 0.0]); assert!(t.measure(0).unwrap());
528 }
529
530 #[test]
531 fn pauli_error_z_no_measurement_change() {
532 let mut t = tab(1);
533 t.pauli_error(0, [0.0, 0.0, 1.0]); assert!(!t.measure(0).unwrap());
535 }
536
537 #[test]
538 fn pauli_error_x_on_excited_qubit_flips_back() {
539 let mut t = tab(1);
540 t.x(0); t.pauli_error(0, [1.0, 0.0, 0.0]); assert!(!t.measure(0).unwrap());
543 }
544
545 #[test]
548 fn two_qubit_pauli_error_zero_prob_no_change() {
549 let mut t = tab(2);
550 t.two_qubit_pauli_error(0, 1, [0.0; 15]);
551 assert!(!t.measure(0).unwrap());
552 assert!(!t.measure(1).unwrap());
553 }
554
555 #[test]
556 fn two_qubit_pauli_error_ix_flips_second_only() {
557 let mut t = tab(2);
559 let mut p = [0.0f64; 15];
560 p[0] = 1.0;
561 t.two_qubit_pauli_error(0, 1, p);
562 assert!(!t.measure(0).unwrap());
563 assert!(t.measure(1).unwrap());
564 }
565
566 #[test]
567 fn two_qubit_pauli_error_xi_flips_first_only() {
568 let mut t = tab(2);
570 let mut p = [0.0f64; 15];
571 p[3] = 1.0;
572 t.two_qubit_pauli_error(0, 1, p);
573 assert!(t.measure(0).unwrap());
574 assert!(!t.measure(1).unwrap());
575 }
576
577 #[test]
578 fn two_qubit_pauli_error_xx_flips_both() {
579 let mut t = tab(2);
581 let mut p = [0.0f64; 15];
582 p[4] = 1.0;
583 t.two_qubit_pauli_error(0, 1, p);
584 assert!(t.measure(0).unwrap());
585 assert!(t.measure(1).unwrap());
586 }
587
588 #[test]
589 fn two_qubit_pauli_error_zz_no_measurement_change() {
590 let mut t = tab(2);
592 let mut p = [0.0f64; 15];
593 p[14] = 1.0;
594 t.two_qubit_pauli_error(0, 1, p);
595 assert!(!t.measure(0).unwrap());
596 assert!(!t.measure(1).unwrap());
597 }
598
599 #[test]
600 fn two_qubit_pauli_error_both_lost_no_change() {
601 let mut t = tab(2);
602 t.qubit_status[0] = QubitStatus::Lost;
603 t.qubit_status[1] = QubitStatus::Lost;
604 let mut p = [0.0f64; 15];
605 p[4] = 1.0; t.two_qubit_pauli_error(0, 1, p);
607 assert!(t.is_lost(0));
608 assert!(t.is_lost(1));
609 }
610
611 #[test]
612 fn two_qubit_pauli_error_first_lost_no_apply() {
613 let mut t = tab(2);
615 t.qubit_status[0] = QubitStatus::Lost;
616 let mut p = [0.0f64; 15];
617 p[0] = 1.0; t.two_qubit_pauli_error(0, 1, p);
619 assert!(!t.measure(1).unwrap()); }
621
622 #[test]
625 fn depolarize2_p0_no_change() {
626 let mut t = tab(2);
627 t.depolarize2(0, 1, 0.0);
628 assert!(!t.measure(0).unwrap());
629 assert!(!t.measure(1).unwrap());
630 }
631
632 #[test]
633 fn depolarize2_both_lost_no_change() {
634 let mut t = tab(2);
635 t.qubit_status[0] = QubitStatus::Lost;
636 t.qubit_status[1] = QubitStatus::Lost;
637 t.depolarize2(0, 1, 1.0);
638 assert!(t.is_lost(0));
639 assert!(t.is_lost(1));
640 }
641
642 #[test]
643 fn depolarize2_first_lost_p0_second_unchanged() {
644 let mut t = tab(2);
645 t.qubit_status[0] = QubitStatus::Lost;
646 t.depolarize2(0, 1, 0.0); assert!(!t.measure(1).unwrap());
648 }
649
650 #[test]
651 fn depolarize2_second_lost_p0_first_unchanged() {
652 let mut t = tab(2);
653 t.qubit_status[1] = QubitStatus::Lost;
654 t.depolarize2(0, 1, 0.0); assert!(!t.measure(0).unwrap());
656 }
657
658 #[test]
661 fn loss_channel_p0_qubit_not_lost() {
662 let mut t = tab(1);
663 t.loss_channel(0, 0.0);
664 assert!(!t.is_lost(0));
665 }
666
667 #[test]
668 fn loss_channel_p1_qubit_marked_lost() {
669 let mut t = tab(1);
670 t.loss_channel(0, 1.0);
671 assert!(t.is_lost(0));
672 }
673
674 #[test]
675 fn loss_channel_p1_qubit_reset_to_zero() {
676 let mut t = tab(1);
678 t.x(0);
679 t.loss_channel(0, 1.0);
680 assert!(t.is_lost(0));
681 assert!(t.measure(0).is_none()); }
683
684 #[test]
685 fn loss_channel_does_not_pollute_measurement_record() {
686 let mut t = tab(1);
689 t.x(0);
690 t.loss_channel(0, 1.0);
691 assert!(t.current_measurement_record().is_empty());
692 }
693
694 #[test]
695 fn loss_channel_p1_subsequent_gate_is_noop() {
696 let mut t = tab(1);
697 t.loss_channel(0, 1.0);
698 t.x(0); assert!(t.measure(0).is_none());
700 t.qubit_status[0] = QubitStatus::Live;
701 assert!(!t.measure(0).unwrap()); }
703
704 #[test]
705 fn loss_channel_p0_second_qubit_unaffected() {
706 let mut t = tab(2);
707 t.loss_channel(0, 0.0);
708 t.loss_channel(1, 0.0);
709 assert!(!t.is_lost(0));
710 assert!(!t.is_lost(1));
711 }
712
713 #[test]
716 fn reset_loss_channel_clears_lost_flag() {
717 let mut t = tab(1);
718 t.loss_channel(0, 1.0);
719 assert!(t.is_lost(0));
720 t.reset_loss_channel(0);
721 assert!(!t.is_lost(0));
722 }
723
724 #[test]
725 fn reset_loss_channel_qubit_in_ground_state() {
726 let mut t = tab(1);
728 t.x(0); t.loss_channel(0, 1.0); t.reset_loss_channel(0);
731 assert!(!t.measure(0).unwrap()); }
733
734 #[test]
735 fn reset_loss_channel_gates_work_again() {
736 let mut t = tab(1);
737 t.loss_channel(0, 1.0);
738 t.reset_loss_channel(0);
739 t.x(0); assert!(t.measure(0).unwrap());
741 }
742
743 #[test]
750 fn depolarize_rng_consumed_on_lost_qubit() {
751 let seed = 42u64;
752 let mut t_active = tab(1);
753 t_active.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
754 t_active.depolarize1(0, 0.3);
755 let next_active: f64 = t_active.tableau.rng.random();
756
757 let mut t_lost = tab(1);
758 t_lost.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
759 t_lost.qubit_status[0] = QubitStatus::Lost;
760 t_lost.depolarize1(0, 0.3);
761 let next_lost: f64 = t_lost.tableau.rng.random();
762
763 assert_eq!(next_active, next_lost);
764 }
765
766 #[test]
767 fn pauli_error_rng_consumed_on_lost_qubit() {
768 let seed = 42u64;
769 let mut t_active = tab(1);
770 t_active.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
771 t_active.pauli_error(0, [0.1, 0.1, 0.1]);
772 let next_active: f64 = t_active.tableau.rng.random();
773
774 let mut t_lost = tab(1);
775 t_lost.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
776 t_lost.qubit_status[0] = QubitStatus::Lost;
777 t_lost.pauli_error(0, [0.1, 0.1, 0.1]);
778 let next_lost: f64 = t_lost.tableau.rng.random();
779
780 assert_eq!(next_active, next_lost);
781 }
782
783 #[test]
786 fn depolarize_statistics() {
787 let p = 0.6_f64;
790 let expected = 2.0 * p / 3.0; let trials = 500;
792
793 let ones = (0..trials)
794 .filter(|_| {
795 let mut t = tab(1);
796 t.depolarize1(0, p);
797 t.measure(0).unwrap()
798 })
799 .count();
800
801 let fraction = ones as f64 / trials as f64;
802 assert!(
804 (fraction - expected).abs() < 0.1,
805 "Expected fraction {expected:.3}, got {fraction:.3}"
806 );
807 }
808
809 #[test]
810 fn depolarize2_statistics() {
811 let p = 0.6_f64;
814 let expected = 8.0 * p / 15.0; let trials = 500;
816
817 let ones = (0..trials)
818 .filter(|_| {
819 let mut t = tab(2);
820 t.depolarize2(0, 1, p);
821 t.measure(0).unwrap()
822 })
823 .count();
824
825 let fraction = ones as f64 / trials as f64;
826 assert!(
828 (fraction - expected).abs() < 0.1,
829 "Expected fraction {expected:.3}, got {fraction:.3}"
830 );
831 }
832
833 #[test]
834 fn test_cnot() {
835 let mut t = tab(2);
836 t.x(0);
837 t.cnot(0, 1);
838 t.loss_channel(0, 1.0);
839 assert!(t.measure(0).is_none());
840 assert!(t.measure(1).unwrap());
841
842 let mut t = tab(2);
843 t.loss_channel(0, 1.0);
844 t.x(0);
845 t.cnot(0, 1);
846 assert!(!t.measure(1).unwrap());
847 assert!(t.measure(0).is_none());
848 }
849
850 #[test]
851 fn test_ghz_statistics() {
852 let mut t = tab(2);
853 t.h(0);
854 t.cnot(0, 1);
855
856 let trials = 100u64;
857 let mut z_avg = 0.0;
858 let p = 0.1;
859 for i in 0..trials {
860 let mut t_trial = t.fork(Some(i));
861 t_trial.loss_channel(0, p);
862
863 let outcome0 = t_trial.measure(0);
864 let outcome1 = t_trial.measure(1);
865 if outcome0.unwrap_or(false) == outcome1.unwrap_or(false) {
866 z_avg += 1.0 / trials as f64;
867 } else {
868 z_avg += -1.0 / trials as f64;
869 }
870 }
871
872 println!("{}", z_avg);
873 assert!((z_avg - (1.0 - p)).abs() < 10.0 / trials as f64);
874 }
875
876 #[test]
879 fn correlated_loss_p0_no_loss() {
880 let mut t = tab(2);
882 t.correlated_loss_channel(0, 1, [0.0, 0.0, 0.0]);
883 assert!(!t.is_lost(0));
884 assert!(!t.is_lost(1));
885 }
886
887 #[test]
888 fn correlated_loss_p0_both_lost() {
889 let mut t = tab(2);
891 t.correlated_loss_channel(0, 1, [1.0, 0.0, 0.0]);
892 assert!(t.is_lost(0));
893 assert!(t.is_lost(1));
894 }
895
896 #[test]
897 fn correlated_loss_p1_exactly_one_lost() {
898 let trials = 200;
904 for seed in 0..trials {
905 let mut t = tab(2);
906 t.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
907 t.correlated_loss_channel(0, 1, [0.0, 0.5, 0.0]);
908 assert!(
909 t.is_lost(0) ^ t.is_lost(1),
910 "Expected exactly one lost qubit (seed {seed})"
911 );
912 }
913 }
914
915 #[test]
916 fn correlated_loss_p1_both_qubits_chosen_equally() {
917 let trials = 1000u64;
920 let mut addr0_lost = 0u64;
921 for seed in 0..trials {
922 let mut t = tab(2);
923 t.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
924 t.correlated_loss_channel(0, 1, [0.0, 0.5, 0.0]);
925 if t.is_lost(0) {
926 addr0_lost += 1;
927 }
928 }
929 let fraction = addr0_lost as f64 / trials as f64;
930 assert!(
932 (fraction - 0.5).abs() < 0.08,
933 "Expected ~0.5, got {fraction:.3}"
934 );
935 }
936
937 #[test]
938 fn correlated_loss_both_lost_resets_to_zero() {
939 let mut t = tab(2);
941 t.x(0);
942 t.x(1);
943 t.correlated_loss_channel(0, 1, [1.0, 0.0, 0.0]);
944 assert!(t.is_lost(0));
945 assert!(t.is_lost(1));
946 t.qubit_status[0] = QubitStatus::Live;
948 t.qubit_status[1] = QubitStatus::Live;
949 assert!(!t.measure(0).unwrap());
950 assert!(!t.measure(1).unwrap());
951 }
952
953 #[test]
954 fn correlated_loss_single_lost_resets_to_zero() {
955 for seed in 0..1000u64 {
959 let mut t = tab(2);
960 t.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
961 t.x(0); t.correlated_loss_channel(0, 1, [0.0, 0.5, 0.0]);
964 if t.is_lost(0) {
965 t.qubit_status[0] = QubitStatus::Live;
966 assert!(!t.measure(0).unwrap(), "Lost qubit should be reset to |0⟩");
967 return;
968 }
969 }
970 panic!("addr0 was never chosen as the lost qubit in 1000 trials");
971 }
972
973 #[test]
974 fn correlated_loss_addr0_already_lost_applies_p2_to_addr1() {
975 let mut t = tab(2);
977 t.qubit_status[0] = QubitStatus::Lost;
978 t.correlated_loss_channel(0, 1, [0.0, 0.0, 1.0]);
979 assert!(t.is_lost(0));
980 assert!(t.is_lost(1));
981 }
982
983 #[test]
984 fn correlated_loss_addr1_already_lost_applies_p2_to_addr0() {
985 let mut t = tab(2);
987 t.qubit_status[1] = QubitStatus::Lost;
988 t.correlated_loss_channel(0, 1, [0.0, 0.0, 1.0]);
989 assert!(t.is_lost(0));
990 assert!(t.is_lost(1));
991 }
992
993 #[test]
994 fn correlated_loss_addr0_already_lost_p2_zero_addr1_survives() {
995 let mut t = tab(2);
997 t.qubit_status[0] = QubitStatus::Lost;
998 t.correlated_loss_channel(0, 1, [0.0, 0.0, 0.0]);
999 assert!(!t.is_lost(1));
1000 }
1001
1002 #[test]
1003 fn correlated_loss_leaked_partner_uses_both_present_branch() {
1004 let mut t = tab(2);
1007 t.leakage_channel(0, 0.0, 1.0);
1008 t.correlated_loss_channel(0, 1, [1.0, 0.0, 0.0]);
1009 assert!(t.is_lost(0));
1010 assert!(t.is_lost(1));
1011 assert!(!t.is_leaked(0));
1012 }
1013
1014 #[test]
1015 fn correlated_loss_statistics_both() {
1016 let p_both = 0.3_f64;
1018 let trials = 1000u64;
1019 let mut both_lost = 0u64;
1020 for seed in 0..trials {
1021 let mut t = tab(2);
1022 t.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
1023 t.correlated_loss_channel(0, 1, [p_both, 0.0, 0.0]);
1024 if t.is_lost(0) && t.is_lost(1) {
1025 both_lost += 1;
1026 }
1027 }
1028 let fraction = both_lost as f64 / trials as f64;
1029 assert!(
1031 (fraction - p_both).abs() < 0.07,
1032 "Expected ~{p_both:.2}, got {fraction:.3}"
1033 );
1034 }
1035
1036 #[test]
1037 fn correlated_loss_statistics_single() {
1038 let p_single = 0.2_f64;
1042 let expected = 2.0 * p_single;
1043 let trials = 1000u64;
1044 let mut one_lost = 0u64;
1045 for seed in 0..trials {
1046 let mut t = tab(2);
1047 t.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
1048 t.correlated_loss_channel(0, 1, [0.0, p_single, 0.0]);
1049 if t.is_lost(0) ^ t.is_lost(1) {
1050 one_lost += 1;
1051 }
1052 }
1053 let fraction = one_lost as f64 / trials as f64;
1054 assert!(
1056 (fraction - expected).abs() < 0.08,
1057 "Expected ~{expected:.2}, got {fraction:.3}"
1058 );
1059 }
1060
1061 type LossyTestSum = PauliSum<
1071 ppvm_pauli_sum::config::fxhash::Byte<
1072 1,
1073 f64,
1074 NoStrategy,
1075 LossyPauliWord<[u8; 1], fxhash::FxBuildHasher>,
1076 >,
1077 >;
1078
1079 fn pauli_sum_survivor(p: [f64; 3]) -> f64 {
1082 let mut sum = LossyTestSum::builder().n_qubits(2).build();
1083 sum += ("ZZ", 1.0);
1084 sum.correlated_loss_channel(0, 1, p);
1085 let zz: LossyPauliWord<[u8; 1], fxhash::FxBuildHasher> = "ZZ".into();
1086 *sum.data()
1087 .get(&zz)
1088 .expect("the all-present term survives a pure rescale")
1089 }
1090
1091 #[test]
1092 fn correlated_loss_exactly_one_lost_is_two_p1_and_agrees_with_pauli_sum() {
1093 let p1 = 0.3_f64;
1094 let p = [0.0, p1, 0.0];
1095 let trials = 20_000u64;
1096 let mut one_lost = 0u64;
1097 for seed in 0..trials {
1098 let mut t = tab(2);
1099 t.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
1100 t.correlated_loss_channel(0, 1, p);
1101 if t.is_lost(0) ^ t.is_lost(1) {
1102 one_lost += 1;
1103 }
1104 }
1105 let trajectory = one_lost as f64 / trials as f64;
1106 let pauli_sum = 1.0 - pauli_sum_survivor(p);
1108 assert!(
1109 (pauli_sum - 2.0 * p1).abs() < 1e-12,
1110 "ppvm-pauli-sum P(exactly one) = {pauli_sum}, want 2*p[1] = {}",
1111 2.0 * p1
1112 );
1113 assert!(
1114 (trajectory - 2.0 * p1).abs() < 0.02,
1115 "trajectory P(exactly one lost) = {trajectory:.4}, want 2*p[1] = {} \
1116 (ppvm-pauli-sum says {pauli_sum})",
1117 2.0 * p1
1118 );
1119 }
1120
1121 #[test]
1124 fn correlated_loss_paper_transport_survival_is_one_minus_p() {
1125 let p = 0.15_f64;
1126 let third = p / 3.0;
1127 let probabilities = [third, third, third];
1128 let trials = 20_000u64;
1129 let mut none_lost = 0u64;
1130 for seed in 0..trials {
1131 let mut t = tab(2);
1132 t.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
1133 t.correlated_loss_channel(0, 1, probabilities);
1134 if !t.is_lost(0) && !t.is_lost(1) {
1135 none_lost += 1;
1136 }
1137 }
1138 let fraction = none_lost as f64 / trials as f64;
1139 assert!(
1140 (fraction - (1.0 - p)).abs() < 0.02,
1141 "trajectory P(no loss) = {fraction:.4}, want 1 - {p} = {}",
1142 1.0 - p
1143 );
1144 let survivor = pauli_sum_survivor(probabilities);
1145 assert!(
1146 (survivor - (1.0 - p)).abs() < 1e-12,
1147 "ppvm-pauli-sum survivor {survivor}, want 1 - {p}"
1148 );
1149 }
1150
1151 #[test]
1156 #[cfg(debug_assertions)]
1157 #[should_panic(expected = "p0 + 2*p1 <= 1")]
1158 fn correlated_loss_rejects_inadmissible_probabilities() {
1159 let mut t = tab(2);
1160 t.correlated_loss_channel(0, 1, [0.6, 0.6, 0.0]);
1161 }
1162
1163 #[test]
1164 #[cfg(debug_assertions)]
1165 #[should_panic(expected = "p0 + 2*p1 <= 1")]
1166 fn correlated_loss_rejects_negative_probabilities() {
1167 let mut t = tab(2);
1168 t.correlated_loss_channel(0, 1, [5.0, -3.0, 17.0]);
1169 }
1170
1171 #[derive(Clone, Copy)]
1174 struct NoF64(f64);
1175
1176 impl ToPrimitive for NoF64 {
1177 fn to_i64(&self) -> Option<i64> {
1178 None
1179 }
1180 fn to_u64(&self) -> Option<u64> {
1181 None
1182 }
1183 fn to_f64(&self) -> Option<f64> {
1184 None
1185 }
1186 }
1187
1188 impl PartialEq<f64> for NoF64 {
1189 fn eq(&self, other: &f64) -> bool {
1190 self.0 == *other
1191 }
1192 }
1193
1194 impl PartialOrd<f64> for NoF64 {
1195 fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
1196 self.0.partial_cmp(other)
1197 }
1198 }
1199
1200 impl std::ops::Add for NoF64 {
1201 type Output = Self;
1202 fn add(self, rhs: Self) -> Self {
1203 Self(self.0 + rhs.0)
1204 }
1205 }
1206
1207 #[test]
1208 fn is_admissible_rejects_negative_p1() {
1209 assert!(!is_admissible_correlated_loss(&[0.0, -0.1, 0.0]));
1210 }
1211
1212 #[test]
1213 fn is_admissible_accepts_f64_saturated_third() {
1214 assert!(is_admissible_correlated_loss(&[1.0 / 3.0, 1.0 / 3.0, 0.5]));
1215 }
1216
1217 #[test]
1218 fn is_admissible_rejects_when_to_f64_fails() {
1219 let p = [NoF64(0.6), NoF64(0.6), NoF64(0.0)];
1220 assert!(
1221 !is_admissible_correlated_loss(&p),
1222 "p0 + 2·p1 = 1.8 must be inadmissible even without an f64 conversion"
1223 );
1224 }
1225
1226 #[test]
1227 fn is_admissible_accepts_saturated_boundary_when_to_f64_fails() {
1228 let p = [NoF64(1.0 / 3.0), NoF64(1.0 / 3.0), NoF64(0.5)];
1229 assert!(
1230 is_admissible_correlated_loss(&p),
1231 "[1/3, 1/3, _] must stay admissible without an f64 conversion"
1232 );
1233 }
1234
1235 #[test]
1239 fn correlated_loss_saturated_boundary_is_admissible() {
1240 let mut t = tab(2);
1241 t.correlated_loss_channel(0, 1, [0.2, 0.4, 1.0]);
1242 let mut t = tab(2);
1243 t.correlated_loss_channel(0, 1, [1.0 / 3.0, 1.0 / 3.0, 0.5]);
1244 }
1245
1246 #[test]
1249 fn z_expectation_ground_state_is_plus_one() {
1250 let t = tab(1);
1251 assert!((t.z_expectation(0) - 1.0).abs() < 1e-12);
1252 }
1253
1254 #[test]
1255 fn z_expectation_excited_state_is_minus_one() {
1256 let mut t = tab(1);
1257 t.x(0);
1258 assert!((t.z_expectation(0) + 1.0).abs() < 1e-12);
1259 }
1260
1261 #[test]
1262 fn z_expectation_superposition_is_zero() {
1263 let mut t = tab(1);
1264 t.h(0);
1265 assert!(t.z_expectation(0).abs() < 1e-12);
1266 }
1267
1268 #[test]
1271 fn asymmetric_loss_ground_state_uses_p0() {
1272 let mut t = tab(1);
1274 t.asymmetric_loss_channel(0, 1.0, 0.0);
1275 assert!(t.is_lost(0));
1276
1277 let mut t = tab(1);
1278 t.asymmetric_loss_channel(0, 0.0, 1.0); assert!(!t.is_lost(0));
1280 }
1281
1282 #[test]
1283 fn asymmetric_loss_excited_state_uses_p1() {
1284 let mut t = tab(1);
1286 t.x(0);
1287 t.asymmetric_loss_channel(0, 0.0, 1.0);
1288 assert!(t.is_lost(0));
1289
1290 let mut t = tab(1);
1291 t.x(0);
1292 t.asymmetric_loss_channel(0, 1.0, 0.0); assert!(!t.is_lost(0));
1294 }
1295
1296 #[test]
1297 fn asymmetric_loss_zero_prob_not_lost() {
1298 let mut t = tab(1);
1299 t.asymmetric_loss_channel(0, 0.0, 0.0);
1300 assert!(!t.is_lost(0));
1301 }
1302
1303 #[test]
1304 fn asymmetric_loss_already_lost_is_noop() {
1305 let mut t = tab(1);
1306 t.qubit_status[0] = QubitStatus::Lost;
1307 t.asymmetric_loss_channel(0, 1.0, 1.0);
1308 assert!(t.is_lost(0));
1309 }
1310
1311 #[test]
1312 fn asymmetric_loss_resets_lost_qubit_to_zero() {
1313 let mut t = tab(1);
1315 t.x(0);
1316 t.asymmetric_loss_channel(0, 0.0, 1.0);
1317 assert!(t.is_lost(0));
1318 t.qubit_status[0] = QubitStatus::Live;
1319 assert!(!t.measure(0).unwrap());
1320 }
1321
1322 #[test]
1323 fn asymmetric_loss_symmetric_matches_loss_channel() {
1324 let p = 0.3;
1326 let trials = 1000u64;
1327 let mut lost = 0u64;
1328 for seed in 0..trials {
1329 let mut t = tab(1);
1330 t.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
1331 t.h(0);
1332 t.asymmetric_loss_channel(0, p, p);
1333 if t.is_lost(0) {
1334 lost += 1;
1335 }
1336 }
1337 let frac = lost as f64 / trials as f64;
1338 assert!((frac - p).abs() < 0.07, "expected ~{p}, got {frac:.3}");
1339 }
1340
1341 #[test]
1342 fn asymmetric_loss_superposition_averages_probs() {
1343 let (p0, p1) = (0.2, 0.6);
1345 let expected = 0.5 * (p0 + p1); let trials = 1000u64;
1347 let mut lost = 0u64;
1348 for seed in 0..trials {
1349 let mut t = tab(1);
1350 t.tableau.rng = rand::SeedableRng::seed_from_u64(seed);
1351 t.h(0);
1352 t.asymmetric_loss_channel(0, p0, p1);
1353 if t.is_lost(0) {
1354 lost += 1;
1355 }
1356 }
1357 let frac = lost as f64 / trials as f64;
1358 assert!(
1359 (frac - expected).abs() < 0.07,
1360 "expected ~{expected}, got {frac:.3}"
1361 );
1362 }
1363
1364 #[test]
1367 fn leakage_p0_p1_zero_no_leak() {
1368 let mut t = tab(1);
1370 t.leakage_channel(0, 0.0, 0.0);
1371 assert!(!t.is_leaked(0));
1372 assert!(!t.is_lost(0));
1373 assert!(!t.measure(0).unwrap());
1374 }
1375
1376 #[test]
1377 fn leakage_to_zero_pins_qubit_to_zero() {
1378 let mut t = tab(1);
1380 t.x(0);
1381 t.leakage_channel(0, 1.0, 0.0);
1382 assert!(t.is_leaked(0));
1383 assert!(!t.is_lost(0)); assert_eq!(t.measure(0), Some(false));
1385 }
1386
1387 #[test]
1388 fn leakage_to_one_pins_qubit_to_one() {
1389 let mut t = tab(1);
1391 t.leakage_channel(0, 0.0, 1.0);
1392 assert!(t.is_leaked(0));
1393 assert!(!t.is_lost(0));
1394 assert_eq!(t.measure(0), Some(true));
1395 }
1396
1397 #[test]
1398 fn leaked_qubit_reports_a_bit_unlike_lost() {
1399 let mut t = tab(1);
1401 t.leakage_channel(0, 0.0, 1.0);
1402 assert!(t.measure(0).is_some());
1403 }
1404
1405 #[test]
1406 fn leakage_does_not_pollute_measurement_record() {
1407 let mut t = tab(1);
1410 t.leakage_channel(0, 0.0, 1.0);
1411 assert!(t.current_measurement_record().is_empty());
1412 }
1413
1414 #[test]
1415 fn leakage_collapses_superposition_then_pins() {
1416 let mut t = tab(1);
1419 t.tableau.rng = rand::SeedableRng::seed_from_u64(7);
1420 t.h(0); t.leakage_channel(0, 0.0, 1.0);
1422 assert!(t.is_leaked(0));
1423 assert_eq!(t.measure(0), Some(true));
1424 assert_eq!(t.measure(0), Some(true));
1425 }
1426
1427 #[test]
1428 fn single_qubit_gate_skips_leaked_qubit() {
1429 let mut t = tab(1);
1431 t.leakage_channel(0, 0.0, 1.0);
1432 t.x(0);
1433 assert_eq!(t.measure(0), Some(true));
1434 }
1435
1436 #[test]
1437 fn two_qubit_gate_skipped_when_control_leaked() {
1438 let mut t = tab(2);
1440 t.leakage_channel(0, 0.0, 1.0);
1441 t.cnot(0, 1);
1442 assert_eq!(t.measure(1), Some(false));
1443 assert_eq!(t.measure(0), Some(true));
1444 }
1445
1446 #[test]
1447 fn leaked_qubit_stays_deterministic_after_other_ops() {
1448 let mut t = tab(2);
1451 t.leakage_channel(0, 0.0, 1.0);
1452 t.h(1);
1453 let _ = t.measure(1);
1454 assert_eq!(t.measure(0), Some(true));
1455 }
1456
1457 #[test]
1458 fn leakage_channel_skips_already_leaked() {
1459 let mut t = tab(1);
1462 t.leakage_channel(0, 0.0, 1.0); t.leakage_channel(0, 1.0, 0.0); assert_eq!(t.measure(0), Some(true));
1465 }
1466
1467 #[test]
1468 fn leaked_qubit_can_still_be_lost() {
1469 let mut t = tab(1);
1471 t.leakage_channel(0, 0.0, 1.0);
1472 t.loss_channel(0, 1.0);
1473 assert!(t.is_lost(0));
1474 assert!(t.measure(0).is_none());
1475 }
1476
1477 #[test]
1478 fn loss_of_leaked_qubit_is_indistinguishable_from_lost() {
1479 let mut t = tab(1);
1484 t.leakage_channel(0, 0.0, 1.0); t.loss_channel(0, 1.0);
1486 assert!(t.is_lost(0));
1487 assert!(!t.is_leaked(0));
1488 t.reset_loss_channel(0);
1489 assert!(!t.is_lost(0));
1490 assert!(!t.is_leaked(0));
1491 assert_eq!(t.measure(0), Some(false));
1492 }
1493
1494 #[test]
1495 fn reset_skips_leaked_qubit() {
1496 let mut t = tab(1);
1498 t.leakage_channel(0, 0.0, 1.0); t.reset(0);
1500 assert!(t.is_leaked(0));
1501 assert_eq!(t.measure(0), Some(true));
1502 }
1503
1504 #[test]
1507 fn reset_leakage_channel_recovers_qubit_to_zero() {
1508 let mut t = tab(1);
1510 t.leakage_channel(0, 0.0, 1.0); t.reset_leakage_channel(0);
1512 assert!(!t.is_leaked(0));
1513 assert!(!t.is_lost(0));
1514 assert!(t.current_measurement_record().is_empty()); assert_eq!(t.measure(0), Some(false)); }
1517
1518 #[test]
1519 fn reset_leakage_channel_gates_work_again() {
1520 let mut t = tab(1);
1522 t.leakage_channel(0, 0.0, 1.0);
1523 t.reset_leakage_channel(0);
1524 t.x(0);
1525 assert_eq!(t.measure(0), Some(true));
1526 }
1527
1528 #[test]
1529 fn reset_leakage_channel_does_not_recover_lost() {
1530 let mut t = tab(1);
1532 t.qubit_status[0] = QubitStatus::Lost;
1533 t.reset_leakage_channel(0);
1534 assert!(t.is_lost(0));
1535 assert!(t.measure(0).is_none());
1536 }
1537
1538 #[test]
1539 fn reset_leakage_channel_noop_on_live_qubit() {
1540 let mut t = tab(1);
1542 t.x(0); t.reset_leakage_channel(0);
1544 assert!(!t.is_leaked(0));
1545 assert_eq!(t.measure(0), Some(true)); }
1547}