Skip to main content

quantrs2_core/networking/
teleportation.rs

1//! Quantum teleportation and entanglement swapping.
2//!
3//! Implements single-hop quantum teleportation and an n-hop entanglement
4//! swapping chain using density-matrix representation with depolarizing noise.
5
6use crate::error::{QuantRS2Error, QuantRS2Result};
7use crate::networking::channel::{
8    fidelity_pure, measure_computational, pure_state_density, DepolarizingChannel, NoiseChannel,
9};
10use scirs2_core::ndarray::Array2;
11use scirs2_core::random::prelude::*;
12use scirs2_core::random::ChaCha20Rng;
13use scirs2_core::Complex64;
14use std::f64::consts::SQRT_2;
15
16// ---------------------------------------------------------------------------
17// Helper: convert u64 seed into 32-byte seed array for ChaCha20
18// ---------------------------------------------------------------------------
19fn seed_from_u64(seed: u64) -> [u8; 32] {
20    let mut bytes = [0u8; 32];
21    let s = seed.to_le_bytes();
22    bytes[..8].copy_from_slice(&s);
23    bytes[8..16].copy_from_slice(&s);
24    bytes[16..24].copy_from_slice(&s);
25    bytes[24..32].copy_from_slice(&s);
26    bytes
27}
28
29// ---------------------------------------------------------------------------
30// Two-qubit depolarizing channel helpers
31// ---------------------------------------------------------------------------
32
33/// Apply depolarizing noise independently to each qubit of a 4×4 two-qubit state.
34fn apply_noise_2q(rho4: &mut Array2<Complex64>, p: f64) {
35    if p <= 0.0 {
36        return;
37    }
38    apply_depolarizing_qubit_top(rho4, p);
39    apply_depolarizing_qubit_bot(rho4, p);
40}
41
42/// Apply depolarizing(p) to the top qubit (bit 1) of a 4-dim two-qubit density matrix.
43fn apply_depolarizing_qubit_top(rho4: &mut Array2<Complex64>, p: f64) {
44    let rho_orig = rho4.clone();
45    let scale_id = Complex64::new(1.0 - p, 0.0);
46    let scale_p = Complex64::new(p / 3.0, 0.0);
47
48    // (1-p) * ρ
49    rho4.mapv_inplace(|v| v * scale_id);
50
51    // X_A ⊗ I_B: index i → i XOR 2
52    let mut term = Array2::<Complex64>::zeros((4, 4));
53    for i in 0..4 {
54        for j in 0..4 {
55            term[[i, j]] = rho_orig[[i ^ 2, j ^ 2]];
56        }
57    }
58    for i in 0..4 {
59        for j in 0..4 {
60            rho4[[i, j]] += scale_p * term[[i, j]];
61        }
62    }
63
64    // Y_A ⊗ I_B: phase = i if top bit = 0, else -i
65    let phase_top = |i: usize| -> Complex64 {
66        if (i >> 1) & 1 == 0 {
67            Complex64::new(0.0, 1.0)
68        } else {
69            Complex64::new(0.0, -1.0)
70        }
71    };
72    let mut term2 = Array2::<Complex64>::zeros((4, 4));
73    for i in 0..4 {
74        for j in 0..4 {
75            term2[[i, j]] = phase_top(i) * rho_orig[[i ^ 2, j ^ 2]] * phase_top(j).conj();
76        }
77    }
78    for i in 0..4 {
79        for j in 0..4 {
80            rho4[[i, j]] += scale_p * term2[[i, j]];
81        }
82    }
83
84    // Z_A ⊗ I_B: sign = +1 if top bit = 0, else -1
85    let sign_top = |i: usize| -> f64 {
86        if (i >> 1) & 1 == 0 {
87            1.0
88        } else {
89            -1.0
90        }
91    };
92    let mut term3 = Array2::<Complex64>::zeros((4, 4));
93    for i in 0..4 {
94        for j in 0..4 {
95            term3[[i, j]] = Complex64::new(sign_top(i) * sign_top(j), 0.0) * rho_orig[[i, j]];
96        }
97    }
98    for i in 0..4 {
99        for j in 0..4 {
100            rho4[[i, j]] += scale_p * term3[[i, j]];
101        }
102    }
103}
104
105/// Apply depolarizing(p) to the bottom qubit (bit 0) of a 4-dim two-qubit density matrix.
106fn apply_depolarizing_qubit_bot(rho4: &mut Array2<Complex64>, p: f64) {
107    let rho_orig = rho4.clone();
108    let scale_id = Complex64::new(1.0 - p, 0.0);
109    let scale_p = Complex64::new(p / 3.0, 0.0);
110
111    rho4.mapv_inplace(|v| v * scale_id);
112
113    // I_A ⊗ X_B: index i → i XOR 1
114    let mut term = Array2::<Complex64>::zeros((4, 4));
115    for i in 0..4 {
116        for j in 0..4 {
117            term[[i, j]] = rho_orig[[i ^ 1, j ^ 1]];
118        }
119    }
120    for i in 0..4 {
121        for j in 0..4 {
122            rho4[[i, j]] += scale_p * term[[i, j]];
123        }
124    }
125
126    // I_A ⊗ Y_B: phase = i if bottom bit = 0, else -i
127    let phase_bot = |i: usize| -> Complex64 {
128        if i & 1 == 0 {
129            Complex64::new(0.0, 1.0)
130        } else {
131            Complex64::new(0.0, -1.0)
132        }
133    };
134    let mut term2 = Array2::<Complex64>::zeros((4, 4));
135    for i in 0..4 {
136        for j in 0..4 {
137            term2[[i, j]] = phase_bot(i) * rho_orig[[i ^ 1, j ^ 1]] * phase_bot(j).conj();
138        }
139    }
140    for i in 0..4 {
141        for j in 0..4 {
142            rho4[[i, j]] += scale_p * term2[[i, j]];
143        }
144    }
145
146    // I_A ⊗ Z_B: sign = +1 if bottom bit = 0, else -1
147    let sign_bot = |i: usize| -> f64 {
148        if i & 1 == 0 {
149            1.0
150        } else {
151            -1.0
152        }
153    };
154    let mut term3 = Array2::<Complex64>::zeros((4, 4));
155    for i in 0..4 {
156        for j in 0..4 {
157            term3[[i, j]] = Complex64::new(sign_bot(i) * sign_bot(j), 0.0) * rho_orig[[i, j]];
158        }
159    }
160    for i in 0..4 {
161        for j in 0..4 {
162            rho4[[i, j]] += scale_p * term3[[i, j]];
163        }
164    }
165}
166
167// ---------------------------------------------------------------------------
168// Bell state: |Φ+⟩ = (|00⟩+|11⟩)/√2
169// ---------------------------------------------------------------------------
170
171/// 4×4 density matrix for the Bell state |Φ+⟩⟨Φ+|.
172/// Basis order: 00→0, 01→1, 10→2, 11→3.
173fn bell_phi_plus() -> Array2<Complex64> {
174    let v = 0.5_f64;
175    let mut rho = Array2::<Complex64>::zeros((4, 4));
176    rho[[0, 0]] = Complex64::new(v, 0.0);
177    rho[[0, 3]] = Complex64::new(v, 0.0);
178    rho[[3, 0]] = Complex64::new(v, 0.0);
179    rho[[3, 3]] = Complex64::new(v, 0.0);
180    rho
181}
182
183// ---------------------------------------------------------------------------
184// Single-qubit corrections
185// ---------------------------------------------------------------------------
186
187/// Apply Pauli-X on a single-qubit 2×2 density matrix.
188fn apply_x(rho: &mut Array2<Complex64>) {
189    let r00 = rho[[0, 0]];
190    let r01 = rho[[0, 1]];
191    let r10 = rho[[1, 0]];
192    let r11 = rho[[1, 1]];
193    rho[[0, 0]] = r11;
194    rho[[0, 1]] = r10;
195    rho[[1, 0]] = r01;
196    rho[[1, 1]] = r00;
197}
198
199/// Apply Pauli-Z on a single-qubit 2×2 density matrix.
200fn apply_z(rho: &mut Array2<Complex64>) {
201    rho[[0, 1]] = -rho[[0, 1]];
202    rho[[1, 0]] = -rho[[1, 0]];
203}
204
205// ---------------------------------------------------------------------------
206// Bell measurement on (ψ, A) within a 3-qubit 8×8 system
207// ---------------------------------------------------------------------------
208
209/// Build 8×8 ρ = ρ_ψ ⊗ ρ_AB.
210/// Bit ordering: ψ = bit 2 (MSB), A = bit 1, B = bit 0 (LSB).
211/// Index i = 4*ψ + 2*A + B.
212fn tensor3_density(rho_psi: &Array2<Complex64>, rho_ab: &Array2<Complex64>) -> Array2<Complex64> {
213    let mut out = Array2::<Complex64>::zeros((8, 8));
214    for p in 0..2_usize {
215        for p2 in 0..2_usize {
216            for a in 0..2_usize {
217                for a2 in 0..2_usize {
218                    for b in 0..2_usize {
219                        for b2 in 0..2_usize {
220                            let i = 4 * p + 2 * a + b;
221                            let j = 4 * p2 + 2 * a2 + b2;
222                            out[[i, j]] = rho_psi[[p, p2]] * rho_ab[[2 * a + b, 2 * a2 + b2]];
223                        }
224                    }
225                }
226            }
227        }
228    }
229    out
230}
231
232/// Apply the Bell-measurement circuit CNOT(ψ→A) then H(ψ) on an 8×8 density matrix.
233fn apply_bell_circuit_8x8(rho8: Array2<Complex64>) -> Array2<Complex64> {
234    // CNOT: control = ψ (bit 2), target = A (bit 1).
235    //   |ψ,a,b⟩ → |ψ, ψ XOR a, b⟩
236    //   New index: i' where bit2 unchanged, bit1 flipped iff bit2=1.
237    let cnot_perm = |i: usize| -> usize {
238        let psi_bit = (i >> 2) & 1;
239        if psi_bit == 1 {
240            i ^ 2
241        } else {
242            i
243        }
244    };
245    let mut after_cnot = Array2::<Complex64>::zeros((8, 8));
246    for i in 0..8 {
247        for j in 0..8 {
248            after_cnot[[cnot_perm(i), cnot_perm(j)]] = rho8[[i, j]];
249        }
250    }
251
252    // H on ψ (bit 2):
253    //   |psi,a,b⟩ → (1/√2) Σ_{p'} (-1)^{psi*p'} |p',a,b⟩
254    let h_val = 1.0 / SQRT_2;
255    let mut after_h = Array2::<Complex64>::zeros((8, 8));
256    for i in 0..8 {
257        let pi = (i >> 2) & 1;
258        for j in 0..8 {
259            let pj = (j >> 2) & 1;
260            for pi2 in 0..2_usize {
261                for pj2 in 0..2_usize {
262                    let phase_i: f64 = if pi == 1 && pi2 == 1 { -1.0 } else { 1.0 };
263                    let phase_j: f64 = if pj == 1 && pj2 == 1 { -1.0 } else { 1.0 };
264                    let i2 = (i & 3) | (pi2 << 2);
265                    let j2 = (j & 3) | (pj2 << 2);
266                    after_h[[i2, j2]] +=
267                        Complex64::new(h_val * h_val * phase_i * phase_j, 0.0) * after_cnot[[i, j]];
268                }
269            }
270        }
271    }
272    after_h
273}
274
275/// Probability of qubit ψ (bit 2) being |0⟩.
276fn prob_qubit_0_in_8x8(rho8: &Array2<Complex64>) -> f64 {
277    (0..4_usize)
278        .map(|i| rho8[[i, i]].re)
279        .sum::<f64>()
280        .clamp(0.0, 1.0)
281}
282
283/// Project qubit ψ (bit 2) onto |m⟩ and renormalise.
284fn project_qubit_2_in_8x8(rho8: &Array2<Complex64>, m: bool) -> Array2<Complex64> {
285    let start = if m { 4 } else { 0 };
286    let end = start + 4;
287    let prob: f64 = (start..end)
288        .map(|i| rho8[[i, i]].re)
289        .sum::<f64>()
290        .max(1e-15);
291    let mut out = Array2::<Complex64>::zeros((8, 8));
292    for i in start..end {
293        for j in start..end {
294            out[[i, j]] = rho8[[i, j]] / Complex64::new(prob, 0.0);
295        }
296    }
297    out
298}
299
300/// Probability of qubit A (bit 1) being |0⟩.
301fn prob_qubit_1_in_8x8(rho8: &Array2<Complex64>) -> f64 {
302    (0..8_usize)
303        .filter(|&i| (i & 2) == 0)
304        .map(|i| rho8[[i, i]].re)
305        .sum::<f64>()
306        .clamp(0.0, 1.0)
307}
308
309/// Project qubit A (bit 1) onto |m⟩ and renormalise.
310fn project_qubit_1_in_8x8(rho8: &Array2<Complex64>, m: bool) -> Array2<Complex64> {
311    let bit_val = if m { 2 } else { 0 };
312    let prob: f64 = (0..8_usize)
313        .filter(|&i| (i & 2) == bit_val)
314        .map(|i| rho8[[i, i]].re)
315        .sum::<f64>()
316        .max(1e-15);
317    let mut out = Array2::<Complex64>::zeros((8, 8));
318    for i in 0..8 {
319        if (i & 2) == bit_val {
320            for j in 0..8 {
321                if (j & 2) == bit_val {
322                    out[[i, j]] = rho8[[i, j]] / Complex64::new(prob, 0.0);
323                }
324            }
325        }
326    }
327    out
328}
329
330/// Trace out qubits ψ (bit 2) and A (bit 1); return reduced density matrix of B (bit 0).
331fn partial_trace_to_qubit_b(rho8: &Array2<Complex64>) -> Array2<Complex64> {
332    let mut rho2 = Array2::<Complex64>::zeros((2, 2));
333    for b in 0..2_usize {
334        for b2 in 0..2_usize {
335            for psi in 0..2_usize {
336                for a in 0..2_usize {
337                    let i = 4 * psi + 2 * a + b;
338                    let j = 4 * psi + 2 * a + b2;
339                    rho2[[b, b2]] += rho8[[i, j]];
340                }
341            }
342        }
343    }
344    rho2
345}
346
347/// Perform a Bell-basis measurement on (ψ, A) and return classical bits + Bob's state.
348fn bell_measure_and_correct(
349    psi: &[Complex64; 2],
350    rho_ab: &Array2<Complex64>,
351    rng: &mut ChaCha20Rng,
352) -> (bool, bool, Array2<Complex64>) {
353    let rho_in = pure_state_density(psi);
354    let rho8 = tensor3_density(&rho_in, rho_ab);
355    let rho8_after = apply_bell_circuit_8x8(rho8);
356
357    // Measure qubit ψ
358    let p0_psi = prob_qubit_0_in_8x8(&rho8_after);
359    let r0: f64 = rng.random();
360    let m0 = r0 >= p0_psi;
361    let rho8_m0 = project_qubit_2_in_8x8(&rho8_after, m0);
362
363    // Measure qubit A
364    let p0_a = prob_qubit_1_in_8x8(&rho8_m0);
365    let r1: f64 = rng.random();
366    let m1 = r1 >= p0_a;
367    let rho8_m1 = project_qubit_1_in_8x8(&rho8_m0, m1);
368
369    let rho_b = partial_trace_to_qubit_b(&rho8_m1);
370    (m0, m1, rho_b)
371}
372
373// ---------------------------------------------------------------------------
374// TeleportationProtocol
375// ---------------------------------------------------------------------------
376
377/// Single-hop quantum teleportation protocol.
378#[derive(Debug, Clone)]
379pub struct TeleportationProtocol {
380    /// Depolarizing noise probability applied to each qubit of the resource Bell pair.
381    pub noise: f64,
382    /// Seed for the random number generator.
383    pub rng_seed: u64,
384}
385
386/// Result of a single teleportation run.
387#[derive(Debug, Clone)]
388pub struct TeleportationResult {
389    /// Fidelity between the teleported state and the original input state.
390    pub fidelity: f64,
391    /// Classical correction bits sent from Alice to Bob: (m0, m1).
392    pub correction_bits: (bool, bool),
393}
394
395impl TeleportationProtocol {
396    /// Create a new teleportation protocol.
397    pub fn new(noise: f64, rng_seed: u64) -> Self {
398        Self {
399            noise: noise.clamp(0.0, 1.0),
400            rng_seed,
401        }
402    }
403
404    /// Run the teleportation protocol for the given input state.
405    ///
406    /// `state` must be a normalised 2-component complex vector.
407    pub fn teleport(&self, state: [Complex64; 2]) -> QuantRS2Result<TeleportationResult> {
408        let norm_sq = state[0].norm_sqr() + state[1].norm_sqr();
409        if (norm_sq - 1.0).abs() > 0.05 {
410            return Err(QuantRS2Error::InvalidInput(
411                "Input state must be normalised".to_string(),
412            ));
413        }
414
415        let mut rng = ChaCha20Rng::from_seed(seed_from_u64(self.rng_seed));
416
417        // Prepare resource Bell pair |Φ+⟩ with noise
418        let mut rho_ab = bell_phi_plus();
419        apply_noise_2q(&mut rho_ab, self.noise);
420
421        // Bell measurement and classical corrections
422        let (m0, m1, mut rho_b) = bell_measure_and_correct(&state, &rho_ab, &mut rng);
423
424        if m1 {
425            apply_x(&mut rho_b);
426        }
427        if m0 {
428            apply_z(&mut rho_b);
429        }
430
431        let fidelity = fidelity_pure(&state, &rho_b);
432
433        Ok(TeleportationResult {
434            fidelity,
435            correction_bits: (m0, m1),
436        })
437    }
438}
439
440// ---------------------------------------------------------------------------
441// EntanglementSwapping
442// ---------------------------------------------------------------------------
443
444/// Multi-hop entanglement swapping chain.
445///
446/// Topology: A — B₁ — B₂ — ... — B_{n-1} — C
447/// Each link is a Bell pair with independent depolarizing noise.
448#[derive(Debug, Clone)]
449pub struct EntanglementSwapping {
450    /// Number of hops (links). 1 = direct Bell pair; 2 = one relay; etc.
451    pub n_hops: usize,
452    /// Depolarizing noise probability per qubit per hop.
453    pub noise_per_link: f64,
454    /// Seed for RNG.
455    pub rng_seed: u64,
456}
457
458/// Result of the entanglement swapping protocol.
459#[derive(Debug, Clone)]
460pub struct SwappingResult {
461    /// End-to-end fidelity of the final A-C Bell pair.
462    pub end_to_end_fidelity: f64,
463}
464
465impl EntanglementSwapping {
466    /// Create a new entanglement swapping chain.
467    pub fn new(n_hops: usize, noise_per_link: f64, rng_seed: u64) -> QuantRS2Result<Self> {
468        if n_hops == 0 {
469            return Err(QuantRS2Error::InvalidInput(
470                "n_hops must be at least 1".to_string(),
471            ));
472        }
473        Ok(Self {
474            n_hops,
475            noise_per_link: noise_per_link.clamp(0.0, 1.0),
476            rng_seed,
477        })
478    }
479
480    /// Run the entanglement swapping protocol.
481    pub fn run(&self) -> QuantRS2Result<SwappingResult> {
482        let mut rng = ChaCha20Rng::from_seed(seed_from_u64(self.rng_seed));
483
484        // Prepare all n_hops Bell pairs with noise
485        let mut links: Vec<Array2<Complex64>> = (0..self.n_hops)
486            .map(|_| {
487                let mut rho = bell_phi_plus();
488                apply_noise_2q(&mut rho, self.noise_per_link);
489                rho
490            })
491            .collect();
492
493        // Merge links one by one via entanglement swapping
494        while links.len() > 1 {
495            let rho_left = links.remove(0);
496            let rho_right = links.remove(0);
497            let rho_ac = bell_swap_4qubit(&rho_left, &rho_right, &mut rng);
498            links.insert(0, rho_ac);
499        }
500
501        let rho_ac = &links[0];
502        let fidelity = bell_state_fidelity(rho_ac);
503
504        Ok(SwappingResult {
505            end_to_end_fidelity: fidelity,
506        })
507    }
508}
509
510// ---------------------------------------------------------------------------
511// Bell measurement for entanglement swapping (4-qubit → 2-qubit)
512// ---------------------------------------------------------------------------
513
514/// Tensor product of two 4×4 density matrices → 16×16.
515/// Index ordering: left-pair (bits 3,2), right-pair (bits 1,0).
516fn tensor_product_4qubit(
517    rho_left: &Array2<Complex64>,
518    rho_right: &Array2<Complex64>,
519) -> Array2<Complex64> {
520    let mut out = Array2::<Complex64>::zeros((16, 16));
521    for ab in 0..4 {
522        for ab2 in 0..4 {
523            for cd in 0..4 {
524                for cd2 in 0..4 {
525                    let i = 4 * ab + cd;
526                    let j = 4 * ab2 + cd2;
527                    out[[i, j]] = rho_left[[ab, ab2]] * rho_right[[cd, cd2]];
528                }
529            }
530        }
531    }
532    out
533}
534
535/// Apply CNOT(qubit1→qubit2) then H(qubit1) on a 4-qubit 16×16 density matrix.
536/// Qubit indexing (from MSB): qubit0=bit3, qubit1=bit2, qubit2=bit1, qubit3=bit0.
537fn apply_bell_circuit_on_middle_qubits(rho: Array2<Complex64>) -> Array2<Complex64> {
538    // CNOT: control = qubit1 (bit2), target = qubit2 (bit1)
539    let cnot = |i: usize| -> usize {
540        let bit2 = (i >> 2) & 1;
541        if bit2 == 1 {
542            i ^ 2
543        } else {
544            i
545        }
546    };
547    let mut after_cnot = Array2::<Complex64>::zeros((16, 16));
548    for i in 0..16 {
549        for j in 0..16 {
550            after_cnot[[cnot(i), cnot(j)]] = rho[[i, j]];
551        }
552    }
553
554    // H on qubit1 (bit2)
555    let h = 1.0 / SQRT_2;
556    let mut after_h = Array2::<Complex64>::zeros((16, 16));
557    for i in 0..16 {
558        let pi = (i >> 2) & 1;
559        for j in 0..16 {
560            let pj = (j >> 2) & 1;
561            for pi2 in 0..2_usize {
562                for pj2 in 0..2_usize {
563                    let phi: f64 = if pi == 1 && pi2 == 1 { -1.0 } else { 1.0 };
564                    let phj: f64 = if pj == 1 && pj2 == 1 { -1.0 } else { 1.0 };
565                    let i2 = (i & !4) | (pi2 << 2);
566                    let j2 = (j & !4) | (pj2 << 2);
567                    after_h[[i2, j2]] +=
568                        Complex64::new(h * h * phi * phj, 0.0) * after_cnot[[i, j]];
569                }
570            }
571        }
572    }
573    after_h
574}
575
576/// Probability of qubit k (bit k from LSB) being |0⟩ in a 16-dim system.
577fn prob_qubit_k_16(rho16: &Array2<Complex64>, k: usize) -> f64 {
578    (0..16_usize)
579        .filter(|&i| (i >> k) & 1 == 0)
580        .map(|i| rho16[[i, i]].re)
581        .sum::<f64>()
582        .clamp(0.0, 1.0)
583}
584
585/// Project qubit k in a 16-dim system onto |m⟩ and renormalise.
586fn project_qubit_k_16(rho16: &Array2<Complex64>, k: usize, m: bool) -> Array2<Complex64> {
587    let bit_val = if m { 1 << k } else { 0 };
588    let mask = 1 << k;
589    let prob: f64 = (0..16_usize)
590        .filter(|&i| (i & mask) == bit_val)
591        .map(|i| rho16[[i, i]].re)
592        .sum::<f64>()
593        .max(1e-15);
594    let mut out = Array2::<Complex64>::zeros((16, 16));
595    for i in 0..16 {
596        if (i & mask) == bit_val {
597            for j in 0..16 {
598                if (j & mask) == bit_val {
599                    out[[i, j]] = rho16[[i, j]] / Complex64::new(prob, 0.0);
600                }
601            }
602        }
603    }
604    out
605}
606
607/// Trace out qubits 1 and 2 (bits 2 and 1) from a 16-dim system → 4-dim ρ_{A,C}.
608/// Remaining: qubit0 (bit3) and qubit3 (bit0) → index 2*a_bit + c_bit.
609fn partial_trace_middle_qubits(rho16: &Array2<Complex64>) -> Array2<Complex64> {
610    let mut out = Array2::<Complex64>::zeros((4, 4));
611    for a in 0..2_usize {
612        for c in 0..2_usize {
613            for a2 in 0..2_usize {
614                for c2 in 0..2_usize {
615                    let out_i = 2 * a + c;
616                    let out_j = 2 * a2 + c2;
617                    for b in 0..2_usize {
618                        for d in 0..2_usize {
619                            let i = (a << 3) | (b << 2) | (d << 1) | c;
620                            let j = (a2 << 3) | (b << 2) | (d << 1) | c2;
621                            out[[out_i, out_j]] += rho16[[i, j]];
622                        }
623                    }
624                }
625            }
626        }
627    }
628    out
629}
630
631/// Apply X on the right (bottom) qubit of a 4-dim two-qubit density matrix.
632fn apply_x_bot(rho4: &mut Array2<Complex64>) {
633    let orig = rho4.clone();
634    for i in 0..4 {
635        for j in 0..4 {
636            rho4[[i, j]] = orig[[i ^ 1, j ^ 1]];
637        }
638    }
639}
640
641/// Apply Z on the right (bottom) qubit of a 4-dim two-qubit density matrix.
642fn apply_z_bot(rho4: &mut Array2<Complex64>) {
643    let sign = |i: usize| -> f64 {
644        if i & 1 == 0 {
645            1.0
646        } else {
647            -1.0
648        }
649    };
650    for i in 0..4 {
651        for j in 0..4 {
652            rho4[[i, j]] *= Complex64::new(sign(i) * sign(j), 0.0);
653        }
654    }
655}
656
657/// Bell-measure the two middle qubits and return ρ_{A,C}.
658fn bell_swap_4qubit(
659    rho_left: &Array2<Complex64>,
660    rho_right: &Array2<Complex64>,
661    rng: &mut ChaCha20Rng,
662) -> Array2<Complex64> {
663    let rho16 = tensor_product_4qubit(rho_left, rho_right);
664    let rho16_bell = apply_bell_circuit_on_middle_qubits(rho16);
665
666    // Measure qubit1 (bit2 = Bl)
667    let p0_bl = prob_qubit_k_16(&rho16_bell, 2);
668    let r_bl: f64 = rng.random();
669    let m_bl = r_bl >= p0_bl;
670    let rho16_mbl = project_qubit_k_16(&rho16_bell, 2, m_bl);
671
672    // Measure qubit2 (bit1 = Br)
673    let p0_br = prob_qubit_k_16(&rho16_mbl, 1);
674    let r_br: f64 = rng.random();
675    let m_br = r_br >= p0_br;
676    let rho16_mbr = project_qubit_k_16(&rho16_mbl, 1, m_br);
677
678    let mut rho_ac = partial_trace_middle_qubits(&rho16_mbr);
679
680    // Apply corrections to C (right qubit): if m_br: X; if m_bl: Z
681    if m_br {
682        apply_x_bot(&mut rho_ac);
683    }
684    if m_bl {
685        apply_z_bot(&mut rho_ac);
686    }
687
688    rho_ac
689}
690
691/// Fidelity of a 4×4 density matrix with |Φ+⟩.
692/// F = ⟨Φ+|ρ|Φ+⟩ = (ρ\[0,0\] + ρ\[0,3\] + ρ\[3,0\] + ρ\[3,3\]) / 2.
693pub fn bell_state_fidelity(rho4: &Array2<Complex64>) -> f64 {
694    let f = 0.5 * (rho4[[0, 0]].re + rho4[[0, 3]].re + rho4[[3, 0]].re + rho4[[3, 3]].re);
695    f.clamp(0.0, 1.0)
696}
697
698// ---------------------------------------------------------------------------
699// Tests
700// ---------------------------------------------------------------------------
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    fn normalised_state(alpha: Complex64, beta: Complex64) -> [Complex64; 2] {
707        let n = (alpha.norm_sqr() + beta.norm_sqr()).sqrt();
708        [alpha / n, beta / n]
709    }
710
711    #[test]
712    fn teleportation_no_noise_fidelity_one() {
713        let psi = normalised_state(
714            Complex64::new(1.0 / SQRT_2, 0.0),
715            Complex64::new(0.0, 1.0 / SQRT_2),
716        );
717        let proto = TeleportationProtocol::new(0.0, 42);
718        let result = proto.teleport(psi).expect("teleport");
719        assert!(
720            result.fidelity > 0.98,
721            "expected fidelity ≈ 1, got {}",
722            result.fidelity
723        );
724    }
725
726    #[test]
727    fn teleportation_noisy_fidelity_decreases() {
728        let psi = normalised_state(Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0));
729        let proto_clean = TeleportationProtocol::new(0.0, 42);
730        let proto_noisy = TeleportationProtocol::new(0.3, 42);
731        let f_clean = proto_clean.teleport(psi).expect("teleport").fidelity;
732        let f_noisy = proto_noisy.teleport(psi).expect("teleport").fidelity;
733        assert!(
734            f_clean > f_noisy,
735            "clean fidelity ({}) should exceed noisy ({})",
736            f_clean,
737            f_noisy
738        );
739    }
740
741    #[test]
742    fn entanglement_swapping_single_hop_no_noise() {
743        let swap = EntanglementSwapping::new(1, 0.0, 42).expect("create");
744        let result = swap.run().expect("run");
745        assert!(
746            result.end_to_end_fidelity > 0.95,
747            "expected high fidelity for 1 hop noiseless, got {}",
748            result.end_to_end_fidelity
749        );
750    }
751
752    #[test]
753    fn entanglement_swapping_n_hops_degrades() {
754        let swap1 = EntanglementSwapping::new(1, 0.05, 42).expect("create");
755        let swap3 = EntanglementSwapping::new(3, 0.05, 42).expect("create");
756        let f1 = swap1.run().expect("run").end_to_end_fidelity;
757        let f3 = swap3.run().expect("run").end_to_end_fidelity;
758        assert!(
759            f1 > f3,
760            "fidelity should degrade with more hops: 1-hop={}, 3-hop={}",
761            f1,
762            f3
763        );
764    }
765}