paulistrings/channel/ptm/
noise.rs

1//! The noise constructors of [`PtmChannel`]: Pauli-diagonal rescalings and amplitude damping.
2
3use super::{assert_distinct, assert_probability, sparse_storage, PtmChannel, PtmKind, PtmStorage};
4use num_complex::Complex64;
5
6impl PtmChannel {
7    /// Single-qubit depolarizing noise with error probability `p`: `X`, `Y` and `Z` are scaled by `1 - 4p/3`.
8    ///
9    /// # Panics
10    ///
11    /// Unless `p ∈ [0, 1]`.
12    pub fn depolarizing(qubit: usize, p: f64) -> Self {
13        assert_probability("depolarizing", "p", p);
14        let scale = 1.0 - 4.0 * p / 3.0;
15        Self::diagonal(&[qubit], vec![1.0, scale, scale, scale], PtmKind::Depolarizing)
16    }
17
18    /// Single-qubit dephasing noise with error probability `p`: `X` and `Y` are scaled by `1 - 2p`.
19    ///
20    /// # Panics
21    ///
22    /// Unless `p ∈ [0, 1]`.
23    pub fn dephasing(qubit: usize, p: f64) -> Self {
24        assert_probability("dephasing", "p", p);
25        let scale = 1.0 - 2.0 * p;
26        Self::diagonal(&[qubit], vec![1.0, scale, 1.0, scale], PtmKind::Dephasing)
27    }
28
29    /// Single-qubit Pauli channel `E(ρ) = (1-px-py-pz)ρ + px·XρX + py·YρY + pz·ZρZ`.
30    ///
31    /// It scales `X` by `1 - 2(py + pz)`, `Y` by `1 - 2(px + pz)` and `Z` by `1 - 2(px + py)`, so `(p/3, p/3, p/3)` is [`depolarizing`](Self::depolarizing) and `(0, 0, p)` is [`dephasing`](Self::dephasing).
32    ///
33    /// # Panics
34    ///
35    /// Unless each probability lies in `[0, 1]` and `px + py + pz ≤ 1`.
36    pub fn pauli_channel(qubit: usize, px: f64, py: f64, pz: f64) -> Self {
37        for (name, value) in [("px", px), ("py", py), ("pz", pz)] {
38            assert_probability("pauli_channel", name, value);
39        }
40        let total = px + py + pz;
41        assert!(
42            total <= 1.0,
43            "PtmChannel::pauli_channel: px + py + pz must be at most 1 (got {total})"
44        );
45        Self::diagonal(
46            &[qubit],
47            vec![1.0, 1.0 - 2.0 * (py + pz), 1.0 - 2.0 * (px + py), 1.0 - 2.0 * (px + pz)],
48            PtmKind::PauliChannel,
49        )
50    }
51
52    /// Two-qubit depolarizing noise `E(ρ) = (1-p)ρ + (p/15)·Σ_k P_k ρ P_k` over the 15 non-identity two-qubit Paulis.
53    ///
54    /// Every Pauli that is non-identity on the pair is scaled by `1 - 16p/15`.
55    ///
56    /// # Panics
57    ///
58    /// If `q0 == q1`, or unless `p ∈ [0, 1]`.
59    pub fn depolarizing_2q(q0: usize, q1: usize, p: f64) -> Self {
60        assert_distinct("depolarizing_2q", q0, q1);
61        assert_probability("depolarizing_2q", "p", p);
62        let mut scale = vec![1.0 - 16.0 * p / 15.0; 16];
63        scale[0] = 1.0;
64        Self::diagonal(&[q0, q1], scale, PtmKind::Depolarizing2q)
65    }
66
67    /// Single-qubit amplitude damping with Kraus operators `K_0 = |0⟩⟨0| + √(1-γ)|1⟩⟨1|`, `K_1 = √γ |0⟩⟨1|`.
68    ///
69    /// Not self-adjoint: `apply` is the Schrödinger map (`I → I + γZ`, `Z → (1-γ)Z`) and `apply_adjoint` its Heisenberg dual (`I → I`, `Z → (1-γ)Z + γI`); both scale `X` and `Y` by `√(1-γ)`.
70    ///
71    /// # Panics
72    ///
73    /// Unless `gamma ∈ [0, 1]`.
74    pub fn amplitude_damping(qubit: usize, gamma: f64) -> Self {
75        assert_probability("amplitude_damping", "gamma", gamma);
76        let transverse = Complex64::new((1.0 - gamma).sqrt(), 0.0);
77        let storage = sparse_storage(vec![
78            ((0, 0), Complex64::new(1.0, 0.0)),
79            ((0, 2), Complex64::new(gamma, 0.0)),
80            ((1, 1), transverse),
81            ((2, 2), Complex64::new(1.0 - gamma, 0.0)),
82            ((3, 3), transverse),
83        ]);
84        Self::new(&[qubit], storage, PtmKind::AmplitudeDamping)
85    }
86
87    fn diagonal(qubits: &[usize], scale: Vec<f64>, kind: PtmKind) -> Self {
88        Self::new(qubits, PtmStorage::Diagonal { scale: scale.into_boxed_slice() }, kind)
89    }
90}
91
92#[cfg(test)]
93mod tests;