ppvm_traits/traits/noise.rs
1// SPDX-FileCopyrightText: 2026 The PPVM Authors
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::config::Config;
5use num::Zero;
6
7// FIXME: most channels don't need to own probs, we can just reference them and clean up the code
8
9/// Single-qubit Pauli error channel — apply `X`, `Y`, or `Z` with the
10/// three given probabilities.
11pub trait PauliError<T: Config> {
12 /// Apply a Pauli-error channel `[p_x, p_y, p_z]` to one qubit.
13 fn pauli_error(&mut self, addr0: usize, p: [T::Coeff; 3]);
14
15 /// stim `X_ERROR(p)` — apply X with probability `p` to one qubit.
16 fn x_error(&mut self, addr0: usize, p: T::Coeff) {
17 let zero = T::Coeff::zero();
18 self.pauli_error(addr0, [p, zero.clone(), zero])
19 }
20
21 /// stim `Y_ERROR(p)` — apply Y with probability `p` to one qubit.
22 fn y_error(&mut self, addr0: usize, p: T::Coeff) {
23 let zero = T::Coeff::zero();
24 self.pauli_error(addr0, [zero.clone(), p, zero])
25 }
26
27 /// stim `Z_ERROR(p)` — apply Z with probability `p` to one qubit.
28 fn z_error(&mut self, addr0: usize, p: T::Coeff) {
29 let zero = T::Coeff::zero();
30 self.pauli_error(addr0, [zero.clone(), zero, p])
31 }
32
33 /// Explicit batched Pauli-error channel.
34 fn pauli_error_many(&mut self, targets: &[usize], p: [T::Coeff; 3]) {
35 for &q in targets {
36 self.pauli_error(q, p.clone());
37 }
38 }
39
40 /// Explicit batched `X_ERROR(p)`.
41 fn x_error_many(&mut self, targets: &[usize], p: T::Coeff) {
42 for &q in targets {
43 self.x_error(q, p.clone());
44 }
45 }
46
47 /// Explicit batched `Y_ERROR(p)`.
48 fn y_error_many(&mut self, targets: &[usize], p: T::Coeff) {
49 for &q in targets {
50 self.y_error(q, p.clone());
51 }
52 }
53
54 /// Explicit batched `Z_ERROR(p)`.
55 fn z_error_many(&mut self, targets: &[usize], p: T::Coeff) {
56 for &q in targets {
57 self.z_error(q, p.clone());
58 }
59 }
60}
61
62/// Two-qubit Pauli error channel.
63pub trait TwoQubitPauliError<T: Config> {
64 /// Apply a two-qubit Pauli-error channel to one pair. Probabilities are given in the order:
65 /// `{IX, IY, IZ, XI, XX, XY, XZ, YI, YX, YY, YZ, ZI, ZX, ZY, ZZ}`.
66 fn two_qubit_pauli_error(&mut self, addr0: usize, addr1: usize, p: [T::Coeff; 15]);
67
68 /// Explicit batched two-qubit Pauli-error channel.
69 fn two_qubit_pauli_error_many(&mut self, pairs: &[(usize, usize)], p: [T::Coeff; 15]) {
70 for &(a, b) in pairs {
71 self.two_qubit_pauli_error(a, b, p.clone());
72 }
73 }
74}
75
76/// Single-qubit depolarizing channel.
77pub trait Depolarizing<T: Config> {
78 /// Depolarize one qubit with probability `p`.
79 fn depolarize1(&mut self, addr0: usize, p: T::Coeff);
80
81 /// Explicit batched single-qubit depolarizing channel.
82 fn depolarize1_many(&mut self, targets: &[usize], p: T::Coeff) {
83 for &q in targets {
84 self.depolarize1(q, p.clone());
85 }
86 }
87}
88
89/// Two-qubit depolarizing channel.
90pub trait Depolarizing2<T: Config> {
91 /// Depolarize one qubit pair with probability `p`.
92 fn depolarize2(&mut self, addr0: usize, addr1: usize, p: T::Coeff);
93
94 /// Explicit batched two-qubit depolarizing channel.
95 fn depolarize2_many(&mut self, pairs: &[(usize, usize)], p: T::Coeff) {
96 for &(a, b) in pairs {
97 self.depolarize2(a, b, p.clone());
98 }
99 }
100}
101
102/// Amplitude-damping channel (single qubit).
103pub trait AmplitudeDamping<T: Config> {
104 /// Apply amplitude damping with damping parameter `gamma`.
105 fn amplitude_damping(&mut self, addr0: usize, gamma: T::Coeff);
106}
107
108/// Single-qubit loss channel — with probability `p`, mark the qubit as
109/// lost (`Pauli::L`).
110pub trait LossChannel<T: Config> {
111 /// Apply a loss channel to qubit `addr0` with loss probability `p`.
112 fn loss_channel(&mut self, addr0: usize, p: T::Coeff);
113}
114
115/// Correlated two-qubit loss channel.
116///
117/// # The `p[1]` convention (normative)
118///
119/// This is the one place the parameterization is defined; every backend
120/// (`ppvm-pauli-sum`, `ppvm-tableau`'s trajectory, `ppvm-tableau-sum`'s mixture)
121/// and every binding cites it rather than restating it. In the paper's notation
122/// `p = [p_LL, p_LQ, p_LN]`, and `p[1] = p_LQ` is the probability that a
123/// **named** one of the two atoms is lost while the other survives. The two
124/// single-loss events are disjoint, so
125///
126/// * the probability of losing *exactly one* atom is `2·p[1]`, and
127/// * the probability that both remain in the qubit subspace — the factor a
128/// fully in-subspace observable is scaled by — is `1 − 2·p[1] − p[0]`.
129///
130/// The channel is completely positive exactly on `p[0], p[1] >= 0`,
131/// `p[0] + 2·p[1] <= 1`, `p[2] ∈ [0, 1]`; the tableau backends `debug_assert`
132/// that region.
133pub trait CorrelatedLossChannel<T: Config> {
134 /// Apply a correlated loss channel to qubits at `addr0` and `addr1`.
135 ///
136 /// The three probabilities are:
137 /// * `p[0]`: The probability of losing both qubits simultaneously when
138 /// both of them are in the qubit subspace.
139 /// * `p[1]`: The probability of losing a **named** one of the two qubits when
140 /// both of them are in the qubit subspace, so losing *exactly one* has
141 /// probability `2·p[1]` and the both-present survivor is scaled by
142 /// `1 − 2·p[1] − p[0]` (which qubit is lost is 50/50).
143 /// * `p[2]`: The probability of losing one qubit when the other one has already
144 /// been lost prior to the channel.
145 fn correlated_loss_channel(&mut self, addr0: usize, addr1: usize, p: [T::Coeff; 3]);
146}
147
148/// Reset the loss bit on a qubit — used to model a re-cooling /
149/// re-loading event that brings a previously-lost atom back.
150pub trait ResetLossChannel<T: Config> {
151 /// Clear the loss bit at `addr0`.
152 fn reset_loss_channel(&mut self, addr0: usize);
153}
154
155/// State-dependent ("asymmetric") single-qubit loss channel: a qubit is
156/// lost from `|0⟩` with probability `p0` and from `|1⟩` with probability
157/// `p1`. Unlike [`LossChannel`], the total loss probability depends on the
158/// qubit's populations, so the channel reads the current `⟨Z⟩`.
159pub trait AsymmetricLossChannel<T: Config> {
160 /// Apply asymmetric loss to qubit `addr0`, with `p0` / `p1` the loss
161 /// probabilities from `|0⟩` / `|1⟩`. See the backend impl for the
162 /// trajectory approximation used (the survival back-action is omitted).
163 fn asymmetric_loss_channel(&mut self, addr0: usize, p0: T::Coeff, p1: T::Coeff);
164}
165
166/// Single-qubit leakage channel — with probability `p0` the qubit is
167/// leaked and pinned to `|0⟩`, with probability `p1` leaked and pinned
168/// to `|1⟩`. Unlike [`LossChannel`], a leaked qubit remains measurable
169/// and returns the pinned computational bit; gates still skip it.
170pub trait LeakageChannel<T: Config> {
171 /// Apply leakage to qubit `addr0`, with `p0` / `p1` the probabilities
172 /// of leaking into `|0⟩` / `|1⟩`.
173 fn leakage_channel(&mut self, addr0: usize, p0: T::Coeff, p1: T::Coeff);
174}
175
176/// Reset leakage on a qubit — used to model a leakage-reduction event
177/// that returns a previously-leaked qubit to the computational subspace
178/// in `|0⟩`.
179pub trait ResetLeakageChannel<T: Config> {
180 /// Clear leakage at `addr0` and reset the qubit to `|0⟩`.
181 ///
182 /// No-op if the qubit is live. A lost qubit cannot be recovered this way.
183 fn reset_leakage_channel(&mut self, addr0: usize);
184}