Skip to main content

quantrs2_core/networking/
channel.rs

1//! Quantum channel models for networking protocols.
2//!
3//! Provides Kraus-operator-based single-qubit channel primitives
4//! (depolarizing, dephasing, amplitude damping) used by BB84, E91, and
5//! quantum teleportation simulations.
6
7use scirs2_core::ndarray::Array2;
8use scirs2_core::Complex64;
9use std::f64::consts::SQRT_2;
10
11/// A single-qubit quantum channel acting on a density matrix ρ.
12///
13/// Channels are applied in-place via Kraus decomposition:
14/// ρ → Σ_k  K_k ρ K_k†
15pub trait NoiseChannel: Send + Sync {
16    /// Apply the channel in-place on a 2×2 density matrix.
17    fn apply(&self, rho: &mut Array2<Complex64>);
18}
19
20// ---------------------------------------------------------------------------
21// Helper: compute  K rho K†  and accumulate into `out`
22// ---------------------------------------------------------------------------
23fn kraus_act(k: &[[Complex64; 2]; 2], rho: &Array2<Complex64>, out: &mut Array2<Complex64>) {
24    // k_rho[i,j] = Σ_l k[i][l] * rho[l,j]
25    let mut k_rho = [[Complex64::new(0.0, 0.0); 2]; 2];
26    for i in 0..2 {
27        for j in 0..2 {
28            for l in 0..2 {
29                k_rho[i][j] += k[i][l] * rho[[l, j]];
30            }
31        }
32    }
33    // out += k_rho * k†  ↔  out[i,j] += Σ_l k_rho[i][l] * conj(k[j][l])
34    for i in 0..2 {
35        for j in 0..2 {
36            for l in 0..2 {
37                out[[i, j]] += k_rho[i][l] * k[j][l].conj();
38            }
39        }
40    }
41}
42
43// ---------------------------------------------------------------------------
44// Pauli matrices as [[Complex64; 2]; 2]
45// ---------------------------------------------------------------------------
46fn pauli_x() -> [[Complex64; 2]; 2] {
47    [
48        [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
49        [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
50    ]
51}
52
53fn pauli_y() -> [[Complex64; 2]; 2] {
54    [
55        [Complex64::new(0.0, 0.0), Complex64::new(0.0, -1.0)],
56        [Complex64::new(0.0, 1.0), Complex64::new(0.0, 0.0)],
57    ]
58}
59
60fn pauli_z() -> [[Complex64; 2]; 2] {
61    [
62        [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
63        [Complex64::new(0.0, 0.0), Complex64::new(-1.0, 0.0)],
64    ]
65}
66
67// ---------------------------------------------------------------------------
68// Depolarizing channel:  ρ → (1−p)ρ + (p/3)(XρX + YρY + ZρZ)
69// ---------------------------------------------------------------------------
70
71/// Depolarizing channel with probability `p`.
72///
73/// `p = 0`: identity; `p = 3/4`: fully mixed output for any input.
74#[derive(Debug, Clone)]
75pub struct DepolarizingChannel {
76    /// Error probability in [0, 1].
77    pub p: f64,
78}
79
80impl DepolarizingChannel {
81    /// Create a new depolarizing channel.
82    pub fn new(p: f64) -> Self {
83        Self {
84            p: p.clamp(0.0, 1.0),
85        }
86    }
87}
88
89impl NoiseChannel for DepolarizingChannel {
90    fn apply(&self, rho: &mut Array2<Complex64>) {
91        if self.p == 0.0 {
92            return;
93        }
94        let p = self.p;
95        let identity_weight = Complex64::new(1.0 - p, 0.0);
96        let pauli_weight = Complex64::new(p / 3.0, 0.0);
97
98        let rho_orig = rho.clone();
99
100        // Start with scaled identity term
101        rho.mapv_inplace(|v| v * identity_weight);
102
103        let paulis = [pauli_x(), pauli_y(), pauli_z()];
104        for pauli in &paulis {
105            let mut term = Array2::<Complex64>::zeros((2, 2));
106            kraus_act(pauli, &rho_orig, &mut term);
107            for i in 0..2 {
108                for j in 0..2 {
109                    rho[[i, j]] += pauli_weight * term[[i, j]];
110                }
111            }
112        }
113    }
114}
115
116// ---------------------------------------------------------------------------
117// Dephasing channel:  ρ → (1−p)ρ + p·ZρZ
118// ---------------------------------------------------------------------------
119
120/// Dephasing (phase-flip) channel with probability `p`.
121#[derive(Debug, Clone)]
122pub struct DephazingChannel {
123    /// Dephasing probability in [0, 1].
124    pub p: f64,
125}
126
127impl DephazingChannel {
128    /// Create a new dephasing channel.
129    pub fn new(p: f64) -> Self {
130        Self {
131            p: p.clamp(0.0, 1.0),
132        }
133    }
134}
135
136impl NoiseChannel for DephazingChannel {
137    fn apply(&self, rho: &mut Array2<Complex64>) {
138        if self.p == 0.0 {
139            return;
140        }
141        let p = self.p;
142        let z = pauli_z();
143
144        let rho_orig = rho.clone();
145        rho.mapv_inplace(|v| v * Complex64::new(1.0 - p, 0.0));
146
147        let mut z_term = Array2::<Complex64>::zeros((2, 2));
148        kraus_act(&z, &rho_orig, &mut z_term);
149        for i in 0..2 {
150            for j in 0..2 {
151                rho[[i, j]] += Complex64::new(p, 0.0) * z_term[[i, j]];
152            }
153        }
154    }
155}
156
157// ---------------------------------------------------------------------------
158// Amplitude damping channel:  K0 = [[1,0],[0,√(1-γ)]],  K1 = [[0,√γ],[0,0]]
159// ---------------------------------------------------------------------------
160
161/// Amplitude damping channel (spontaneous emission) with decay rate `gamma`.
162#[derive(Debug, Clone)]
163pub struct AmplitudeDampingChannel {
164    /// Decay probability γ in [0, 1].
165    pub gamma: f64,
166}
167
168impl AmplitudeDampingChannel {
169    /// Create a new amplitude damping channel.
170    pub fn new(gamma: f64) -> Self {
171        Self {
172            gamma: gamma.clamp(0.0, 1.0),
173        }
174    }
175}
176
177impl NoiseChannel for AmplitudeDampingChannel {
178    fn apply(&self, rho: &mut Array2<Complex64>) {
179        if self.gamma == 0.0 {
180            return;
181        }
182        let gamma = self.gamma;
183        let sqrt_1mg = (1.0 - gamma).sqrt();
184        let sqrt_g = gamma.sqrt();
185
186        let k0: [[Complex64; 2]; 2] = [
187            [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
188            [Complex64::new(0.0, 0.0), Complex64::new(sqrt_1mg, 0.0)],
189        ];
190        let k1: [[Complex64; 2]; 2] = [
191            [Complex64::new(0.0, 0.0), Complex64::new(sqrt_g, 0.0)],
192            [Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0)],
193        ];
194
195        let rho_orig = rho.clone();
196        rho.mapv_inplace(|_| Complex64::new(0.0, 0.0));
197        kraus_act(&k0, &rho_orig, rho);
198        kraus_act(&k1, &rho_orig, rho);
199    }
200}
201
202// ---------------------------------------------------------------------------
203// Pure-state helpers
204// ---------------------------------------------------------------------------
205
206/// Build the density matrix ρ = |ψ⟩⟨ψ| from a normalised 2-component state.
207pub fn pure_state_density(psi: &[Complex64; 2]) -> Array2<Complex64> {
208    let mut rho = Array2::<Complex64>::zeros((2, 2));
209    for i in 0..2 {
210        for j in 0..2 {
211            rho[[i, j]] = psi[i] * psi[j].conj();
212        }
213    }
214    rho
215}
216
217/// Compute the fidelity F = ⟨ψ|ρ|ψ⟩ for a pure target state |ψ⟩.
218pub fn fidelity_pure(psi: &[Complex64; 2], rho: &Array2<Complex64>) -> f64 {
219    // F = Σ_{i,j} conj(psi[i]) * rho[i,j] * psi[j]
220    let mut f = Complex64::new(0.0, 0.0);
221    for i in 0..2 {
222        for j in 0..2 {
223            f += psi[i].conj() * rho[[i, j]] * psi[j];
224        }
225    }
226    f.re.clamp(0.0, 1.0)
227}
228
229/// |0⟩ state
230pub fn ket_zero() -> [Complex64; 2] {
231    [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]
232}
233
234/// |1⟩ state
235pub fn ket_one() -> [Complex64; 2] {
236    [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)]
237}
238
239/// |+⟩ = (|0⟩+|1⟩)/√2 state
240pub fn ket_plus() -> [Complex64; 2] {
241    let v = 1.0 / SQRT_2;
242    [Complex64::new(v, 0.0), Complex64::new(v, 0.0)]
243}
244
245/// |−⟩ = (|0⟩−|1⟩)/√2 state
246pub fn ket_minus() -> [Complex64; 2] {
247    let v = 1.0 / SQRT_2;
248    [Complex64::new(v, 0.0), Complex64::new(-v, 0.0)]
249}
250
251/// Measure a single qubit in the computational basis; returns (outcome, collapsed density matrix).
252///
253/// `outcome = false` → |0⟩, `outcome = true` → |1⟩.
254/// The random choice is driven by the Born rule probability of each outcome.
255pub fn measure_computational(rho: &Array2<Complex64>, r: f64) -> (bool, Array2<Complex64>) {
256    // Probability of |0⟩
257    let p0 = rho[[0, 0]].re.clamp(0.0, 1.0);
258    let outcome = r >= p0;
259    let mut collapsed = Array2::<Complex64>::zeros((2, 2));
260    if !outcome {
261        // Project onto |0⟩: Π_0 ρ Π_0 / p0
262        if p0 > 1e-15 {
263            collapsed[[0, 0]] = Complex64::new(1.0, 0.0);
264        }
265    } else {
266        let p1 = (1.0 - p0).max(0.0);
267        if p1 > 1e-15 {
268            collapsed[[1, 1]] = Complex64::new(1.0, 0.0);
269        }
270    }
271    (outcome, collapsed)
272}
273
274// ---------------------------------------------------------------------------
275// Tests
276// ---------------------------------------------------------------------------
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use approx::assert_abs_diff_eq;
282
283    fn identity_rho() -> Array2<Complex64> {
284        let mut rho = Array2::<Complex64>::zeros((2, 2));
285        rho[[0, 0]] = Complex64::new(0.5, 0.0);
286        rho[[1, 1]] = Complex64::new(0.5, 0.0);
287        rho
288    }
289
290    #[test]
291    fn depolarizing_zero_is_identity() {
292        let ch = DepolarizingChannel::new(0.0);
293        let mut rho = pure_state_density(&ket_zero());
294        let orig = rho.clone();
295        ch.apply(&mut rho);
296        assert_abs_diff_eq!(rho[[0, 0]].re, orig[[0, 0]].re, epsilon = 1e-12);
297        assert_abs_diff_eq!(rho[[1, 1]].re, orig[[1, 1]].re, epsilon = 1e-12);
298    }
299
300    #[test]
301    fn depolarizing_fully_mixed_stays_mixed() {
302        let ch = DepolarizingChannel::new(0.75);
303        let mut rho = identity_rho();
304        ch.apply(&mut rho);
305        // Fully mixed state is a fixed point of depolarizing channel
306        assert_abs_diff_eq!(rho[[0, 0]].re, 0.5, epsilon = 1e-12);
307        assert_abs_diff_eq!(rho[[1, 1]].re, 0.5, epsilon = 1e-12);
308    }
309
310    #[test]
311    fn amplitude_damping_relaxes_to_ground() {
312        let ch = AmplitudeDampingChannel::new(1.0);
313        let mut rho = pure_state_density(&ket_one());
314        ch.apply(&mut rho);
315        assert_abs_diff_eq!(rho[[0, 0]].re, 1.0, epsilon = 1e-12);
316        assert_abs_diff_eq!(rho[[1, 1]].re, 0.0, epsilon = 1e-12);
317    }
318
319    #[test]
320    fn dephasing_zero_is_identity() {
321        let ch = DephazingChannel::new(0.0);
322        let mut rho = pure_state_density(&ket_plus());
323        let orig = rho.clone();
324        ch.apply(&mut rho);
325        for i in 0..2 {
326            for j in 0..2 {
327                assert_abs_diff_eq!(rho[[i, j]].re, orig[[i, j]].re, epsilon = 1e-12);
328            }
329        }
330    }
331}