Skip to main content

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}