Skip to main content

quantrs2_sim/
noise_advanced.rs

1//! Advanced noise models beyond simple single-qubit depolarising channels.
2//!
3//! Provides two-qubit correlated noise, crosstalk, leakage to higher energy
4//! levels, and time-dependent amplitude / phase damping noise channels for
5//! realistic device simulation.
6
7#![allow(clippy::needless_range_loop)]
8
9use scirs2_core::Complex64;
10use std::f64::consts::PI;
11use std::time::Duration;
12
13use quantrs2_core::error::QuantRS2Result;
14use quantrs2_core::qubit::QubitId;
15
16use crate::noise::{NoiseChannel, NoiseChannelType, NoiseModel};
17
18/// Multiply two single-qubit (2x2) operators stored as flattened row-major
19/// `Complex64` slices and return the product `a * b` (also flattened 2x2).
20///
21/// Index layout: element `[i][j]` lives at `2 * i + j`.
22fn matmul_2x2(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
23    let mut out = vec![Complex64::new(0.0, 0.0); 4];
24    for i in 0..2 {
25        for j in 0..2 {
26            let mut acc = Complex64::new(0.0, 0.0);
27            for k in 0..2 {
28                acc += a[2 * i + k] * b[2 * k + j];
29            }
30            out[2 * i + j] = acc;
31        }
32    }
33    out
34}
35
36/// Tensor (Kronecker) product of two single-qubit (2x2) operators stored as
37/// flattened row-major `Complex64` slices, returning the resulting 4x4 operator
38/// as a flattened row-major slice (16 elements).
39///
40/// For `A ⊗ B`, the element at `[2*r1 + r2][2*c1 + c2]` equals
41/// `A[r1][c1] * B[r2][c2]`. In the flattened 4x4 layout the destination index is
42/// `4 * (2*r1 + r2) + (2*c1 + c2)`.
43fn tensor_2x2(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
44    let mut out = vec![Complex64::new(0.0, 0.0); 16];
45    for r1 in 0..2 {
46        for c1 in 0..2 {
47            let a_val = a[2 * r1 + c1];
48            for r2 in 0..2 {
49                for c2 in 0..2 {
50                    let row = 2 * r1 + r2;
51                    let col = 2 * c1 + c2;
52                    out[4 * row + col] = a_val * b[2 * r2 + c2];
53                }
54            }
55        }
56    }
57    out
58}
59
60/// The four single-qubit Pauli matrices `{I, X, Y, Z}` as flattened row-major
61/// 2x2 `Complex64` slices.
62fn single_qubit_paulis() -> [Vec<Complex64>; 4] {
63    let zero = Complex64::new(0.0, 0.0);
64    let one = Complex64::new(1.0, 0.0);
65    let i_unit = Complex64::new(0.0, 1.0);
66    [
67        // I
68        vec![one, zero, zero, one],
69        // X
70        vec![zero, one, one, zero],
71        // Y
72        vec![zero, -i_unit, i_unit, zero],
73        // Z
74        vec![one, zero, zero, -one],
75    ]
76}
77
78/// Two-qubit depolarizing noise channel
79#[derive(Debug, Clone)]
80pub struct TwoQubitDepolarizingChannel {
81    /// First qubit
82    pub qubit1: QubitId,
83
84    /// Second qubit
85    pub qubit2: QubitId,
86
87    /// Probability of error
88    pub probability: f64,
89}
90
91impl NoiseChannel for TwoQubitDepolarizingChannel {
92    fn name(&self) -> &'static str {
93        "TwoQubitDepolarizing"
94    }
95
96    fn qubits(&self) -> Vec<QubitId> {
97        vec![self.qubit1, self.qubit2]
98    }
99
100    fn apply_to_statevector(&self, state: &mut [Complex64]) -> QuantRS2Result<()> {
101        let q1_idx = self.qubit1.id() as usize;
102        #[allow(clippy::needless_range_loop)]
103        let q2_idx = self.qubit2.id() as usize;
104        let dim = state.len();
105
106        // Apply two-qubit depolarizing noise with probability p
107        if fastrand::f64() < self.probability {
108            // Choose randomly between 15 possible Pauli errors (excluding I⊗I)
109            let error_type = fastrand::u32(..) % 15;
110
111            // Create a copy of the state to read from
112            let state_copy = state.to_vec();
113
114            match error_type {
115                0 => {
116                    // X⊗I
117                    for i in 0..dim {
118                        let flipped_i = i ^ (1 << q1_idx);
119                        state[i] = state_copy[flipped_i];
120                    }
121                }
122                1 => {
123                    // I⊗X
124                    for i in 0..dim {
125                        let flipped_i = i ^ (1 << q2_idx);
126                        state[i] = state_copy[flipped_i];
127                    }
128                }
129                2 => {
130                    // X⊗X
131                    for i in 0..dim {
132                        let flipped_i = i ^ (1 << q1_idx) ^ (1 << q2_idx);
133                        state[i] = state_copy[flipped_i];
134                    }
135                }
136                3 => {
137                    // Y⊗I
138                    for i in 0..dim {
139                        let flipped_i = i ^ (1 << q1_idx);
140                        let phase = if (i >> q1_idx) & 1 == 1 { 1.0 } else { -1.0 };
141                        state[i] = state_copy[flipped_i] * Complex64::new(0.0, phase);
142                    }
143                }
144                4 => {
145                    // I⊗Y
146                    for i in 0..dim {
147                        let flipped_i = i ^ (1 << q2_idx);
148                        let phase = if (i >> q2_idx) & 1 == 1 { 1.0 } else { -1.0 };
149                        state[i] = state_copy[flipped_i] * Complex64::new(0.0, phase);
150                    }
151                }
152                5 => {
153                    // Y⊗Y
154                    for i in 0..dim {
155                        let flipped_i = i ^ (1 << q1_idx) ^ (1 << q2_idx);
156                        let phase1 = if (i >> q1_idx) & 1 == 1 { 1.0 } else { -1.0 };
157                        let phase2 = if (i >> q2_idx) & 1 == 1 { 1.0 } else { -1.0 };
158                        state[i] = state_copy[flipped_i] * Complex64::new(0.0, phase1 * phase2);
159                    }
160                }
161                6 => {
162                    // Z⊗I
163                    for i in 0..dim {
164                        if (i >> q1_idx) & 1 == 1 {
165                            state[i] = -state_copy[i];
166                        }
167                    }
168                }
169                7 => {
170                    // I⊗Z
171                    for i in 0..dim {
172                        if (i >> q2_idx) & 1 == 1 {
173                            state[i] = -state_copy[i];
174                        }
175                    }
176                }
177                8 => {
178                    // Z⊗Z
179                    for i in 0..dim {
180                        let parity = ((i >> q1_idx) & 1) ^ ((i >> q2_idx) & 1);
181                        if parity == 1 {
182                            state[i] = -state_copy[i];
183                        }
184                    }
185                }
186                9 => {
187                    // X⊗Y
188                    for i in 0..dim {
189                        let flipped_i = i ^ (1 << q1_idx) ^ (1 << q2_idx);
190                        let phase = if (i >> q2_idx) & 1 == 1 { 1.0 } else { -1.0 };
191                        state[i] = state_copy[flipped_i] * Complex64::new(0.0, phase);
192                    }
193                }
194                10 => {
195                    // X⊗Z
196                    for i in 0..dim {
197                        let flipped_i = i ^ (1 << q1_idx);
198                        if (flipped_i >> q2_idx) & 1 == 1 {
199                            state[i] = -state_copy[flipped_i];
200                        } else {
201                            state[i] = state_copy[flipped_i];
202                        }
203                    }
204                }
205                11 => {
206                    // Y⊗X
207                    for i in 0..dim {
208                        let flipped_i = i ^ (1 << q1_idx) ^ (1 << q2_idx);
209                        let phase = if (i >> q1_idx) & 1 == 1 { 1.0 } else { -1.0 };
210                        state[i] = state_copy[flipped_i] * Complex64::new(0.0, phase);
211                    }
212                }
213                12 => {
214                    // Y⊗Z
215                    for i in 0..dim {
216                        let flipped_i = i ^ (1 << q1_idx);
217                        let phase = if ((i >> q1_idx) & 1 == 1) ^ ((i >> q2_idx) & 1 == 1) {
218                            Complex64::new(0.0, -1.0)
219                        } else {
220                            Complex64::new(0.0, 1.0)
221                        };
222                        state[i] = state_copy[flipped_i] * phase;
223                    }
224                }
225                13 => {
226                    // Z⊗X
227                    for i in 0..dim {
228                        let flipped_i = i ^ (1 << q2_idx);
229                        if (i >> q1_idx) & 1 == 1 {
230                            state[i] = -state_copy[flipped_i];
231                        } else {
232                            state[i] = state_copy[flipped_i];
233                        }
234                    }
235                }
236                14 => {
237                    // Z⊗Y
238                    for i in 0..dim {
239                        let flipped_i = i ^ (1 << q2_idx);
240                        let phase = if ((i >> q1_idx) & 1 == 1) ^ ((i >> q2_idx) & 1 == 1) {
241                            Complex64::new(0.0, -1.0)
242                        } else {
243                            Complex64::new(0.0, 1.0)
244                        };
245                        state[i] = state_copy[flipped_i] * phase;
246                    }
247                }
248                _ => unreachable!(),
249            }
250        }
251
252        Ok(())
253    }
254
255    fn kraus_operators(&self) -> Vec<Vec<Complex64>> {
256        // Two-qubit depolarizing channel:
257        //   rho -> (1 - p) rho + (p / 15) * sum_{(a,b) != (I,I)} (P_a x P_b) rho (P_a x P_b)^dagger
258        // This yields 16 Kraus operators, each a flattened 4x4 matrix (16 elems):
259        //   K_0    = sqrt(1 - p)  * (I x I)
260        //   K_1..15 = sqrt(p / 15) * (P_a x P_b)   for the 15 non-identity Pauli pairs.
261        // Trace preservation: sum_k K_k^dagger K_k
262        //   = (1 - p) I_4 + (p / 15) * 15 * I_4 = I_4   (each Pauli is unitary).
263        let p = self.probability;
264        let sqrt_1_minus_p = Complex64::new((1.0 - p).sqrt(), 0.0);
265        let sqrt_p_15 = Complex64::new((p / 15.0).sqrt(), 0.0);
266
267        let paulis = single_qubit_paulis();
268
269        let mut operators = Vec::with_capacity(16);
270        for (a_idx, pauli_a) in paulis.iter().enumerate() {
271            for (b_idx, pauli_b) in paulis.iter().enumerate() {
272                let mut op = tensor_2x2(pauli_a, pauli_b);
273                // (a_idx, b_idx) == (0, 0) is the identity pair I x I.
274                let scale = if a_idx == 0 && b_idx == 0 {
275                    sqrt_1_minus_p
276                } else {
277                    sqrt_p_15
278                };
279                for value in &mut op {
280                    *value *= scale;
281                }
282                operators.push(op);
283            }
284        }
285
286        operators
287    }
288
289    fn probability(&self) -> f64 {
290        self.probability
291    }
292}
293
294/// Thermal relaxation noise channel (combination of T1 and T2 effects)
295#[derive(Debug, Clone)]
296pub struct ThermalRelaxationChannel {
297    /// Target qubit
298    pub target: QubitId,
299
300    /// T1 relaxation time (seconds)
301    pub t1: f64,
302
303    /// T2 pure dephasing time (seconds)
304    pub t2: f64,
305
306    /// Gate time (seconds)
307    pub gate_time: f64,
308
309    /// Excited state population at thermal equilibrium (0.0 to 1.0)
310    pub excited_state_population: f64,
311}
312
313impl NoiseChannel for ThermalRelaxationChannel {
314    fn name(&self) -> &'static str {
315        "ThermalRelaxation"
316    }
317
318    fn qubits(&self) -> Vec<QubitId> {
319        vec![self.target]
320    }
321
322    fn apply_to_statevector(&self, state: &mut [Complex64]) -> QuantRS2Result<()> {
323        let target_idx = self.target.id() as usize;
324        let dim = state.len();
325
326        // Calculate relaxation and dephasing probabilities
327        let p_reset = 1.0 - (-self.gate_time / self.t1).exp();
328        let p_phase = 0.5 * (1.0 - (-self.gate_time / self.t2).exp());
329
330        // Create a copy of the state for reading
331        let state_copy = state.to_vec();
332
333        // Apply thermal relaxation
334        // First apply amplitude damping (relaxation)
335        for i in 0..dim {
336            if (i >> target_idx) & 1 == 1 {
337                // This basis state has the target qubit in |1⟩
338                let base_idx = i & !(1 << target_idx); // Flip the target bit to 0
339
340                // Apply relaxation with probability p_reset
341                if fastrand::f64() < p_reset {
342                    // With probability (1-p_eq), collapse to |0⟩ state
343                    // With probability p_eq, collapse to |1⟩ state (thermal equilibrium)
344                    if fastrand::f64() < self.excited_state_population {
345                        // Stay in |1⟩ due to thermal excitation
346                        state[i] = state_copy[i];
347                    } else {
348                        // Collapse to |0⟩
349                        state[base_idx] += state_copy[i];
350                        state[i] = Complex64::new(0.0, 0.0);
351                    }
352                } else {
353                    // No relaxation occurs, but apply sqrt(1-p) factor
354                    state[i] = state_copy[i] * Complex64::new((1.0 - p_reset).sqrt(), 0.0);
355                }
356            }
357        }
358
359        // Then apply phase damping (dephasing on top of amplitude damping)
360        for i in 0..dim {
361            if (i >> target_idx) & 1 == 1 {
362                // Apply additional pure dephasing
363                if fastrand::f64() < p_phase {
364                    // Random phase
365                    state[i] *= Complex64::new(-1.0, 0.0); // Apply phase flip
366                }
367            }
368        }
369
370        // Normalize the state
371        NoiseChannelType::normalize_state(state);
372
373        Ok(())
374    }
375
376    fn kraus_operators(&self) -> Vec<Vec<Complex64>> {
377        // Thermal relaxation = generalized amplitude damping (T1) composed with
378        // pure phase damping (the residual T2 dephasing). Both are CPTP, so their
379        // composition is CPTP and trace-preserving.
380        //
381        // Generalized amplitude damping with gamma = p_reset and excited-state
382        // population p_e (all 2x2, flattened row-major as [a00, a01, a10, a11]):
383        //   A0 = sqrt(p_e)     * [[1, 0], [0, sqrt(1 - gamma)]]
384        //   A1 = sqrt(p_e)     * [[0, sqrt(gamma)], [0, 0]]
385        //   A2 = sqrt(1 - p_e) * [[sqrt(1 - gamma), 0], [0, 1]]
386        //   A3 = sqrt(1 - p_e) * [[0, 0], [sqrt(gamma), 0]]
387        // Phase damping with gamma_phi = 2 * p_phase (clamped to [0, 1]):
388        //   P0 = [[1, 0], [0, sqrt(1 - gamma_phi)]]
389        //   P1 = [[0, 0], [0, sqrt(gamma_phi)]]
390        // Final Kraus set = { P_i * A_j } (8 operators).
391        // Trace check: sum_{i,j} (P_i A_j)^dagger (P_i A_j)
392        //   = sum_j A_j^dagger (sum_i P_i^dagger P_i) A_j
393        //   = sum_j A_j^dagger A_j = I_2.
394        let p_reset = 1.0 - (-self.gate_time / self.t1).exp();
395        let p_phase = 0.5 * (1.0 - (-self.gate_time / self.t2).exp());
396
397        let gamma = p_reset.clamp(0.0, 1.0);
398        let gamma_phi = (2.0 * p_phase).clamp(0.0, 1.0);
399        let p_e = self.excited_state_population.clamp(0.0, 1.0);
400
401        let zero = Complex64::new(0.0, 0.0);
402        let sqrt_p_e = Complex64::new(p_e.sqrt(), 0.0);
403        let sqrt_1_minus_p_e = Complex64::new((1.0 - p_e).sqrt(), 0.0);
404        let sqrt_gamma = Complex64::new(gamma.sqrt(), 0.0);
405        let sqrt_1_minus_gamma = Complex64::new((1.0 - gamma).sqrt(), 0.0);
406        let sqrt_gamma_phi = Complex64::new(gamma_phi.sqrt(), 0.0);
407        let sqrt_1_minus_gamma_phi = Complex64::new((1.0 - gamma_phi).sqrt(), 0.0);
408        let one = Complex64::new(1.0, 0.0);
409
410        // Generalized amplitude damping operators.
411        let a0 = vec![sqrt_p_e, zero, zero, sqrt_p_e * sqrt_1_minus_gamma];
412        let a1 = vec![zero, sqrt_p_e * sqrt_gamma, zero, zero];
413        let a2 = vec![
414            sqrt_1_minus_p_e * sqrt_1_minus_gamma,
415            zero,
416            zero,
417            sqrt_1_minus_p_e,
418        ];
419        let a3 = vec![zero, zero, sqrt_1_minus_p_e * sqrt_gamma, zero];
420        let amplitude_ops = [a0, a1, a2, a3];
421
422        // Pure phase-damping operators.
423        let p0 = vec![one, zero, zero, sqrt_1_minus_gamma_phi];
424        let p1 = vec![zero, zero, zero, sqrt_gamma_phi];
425        let phase_ops = [p0, p1];
426
427        let mut operators = Vec::with_capacity(amplitude_ops.len() * phase_ops.len());
428        for phase_op in &phase_ops {
429            for amplitude_op in &amplitude_ops {
430                operators.push(matmul_2x2(phase_op, amplitude_op));
431            }
432        }
433
434        operators
435    }
436
437    fn probability(&self) -> f64 {
438        // Return the combined probability of an error occurring
439        let p_reset = 1.0 - (-self.gate_time / self.t1).exp();
440        let p_phase = 0.5 * (1.0 - (-self.gate_time / self.t2).exp());
441        p_reset + p_phase - p_reset * p_phase // Combined probability
442    }
443}
444
445/// Crosstalk noise channel for adjacent qubits
446#[derive(Debug, Clone)]
447pub struct CrosstalkChannel {
448    /// Primary qubit
449    pub primary: QubitId,
450
451    /// Neighbor qubit
452    pub neighbor: QubitId,
453
454    /// Crosstalk strength (0.0 to 1.0)
455    pub strength: f64,
456}
457
458impl NoiseChannel for CrosstalkChannel {
459    fn name(&self) -> &'static str {
460        "Crosstalk"
461    }
462
463    fn qubits(&self) -> Vec<QubitId> {
464        vec![self.primary, self.neighbor]
465    }
466
467    fn apply_to_statevector(&self, state: &mut [Complex64]) -> QuantRS2Result<()> {
468        let primary_idx = self.primary.id() as usize;
469        let neighbor_idx = self.neighbor.id() as usize;
470        let dim = state.len();
471
472        // Apply crosstalk with probability based on strength
473        if fastrand::f64() < self.strength {
474            // Create a copy of the state for reading
475            let state_copy = state.to_vec();
476
477            // Randomly select an effect (simplified model):
478            // 1. ZZ interaction
479            // 2. Neighbor rotation
480            let effect = fastrand::u32(..) % 2;
481
482            match effect {
483                0 => {
484                    // ZZ interaction
485                    for i in 0..dim {
486                        let parity = ((i >> primary_idx) & 1) ^ ((i >> neighbor_idx) & 1);
487                        if parity == 1 {
488                            // Apply phase shift if qubits have different parity
489                            let phase = fastrand::f64() * PI;
490                            state[i] *= Complex64::new(phase.cos(), phase.sin());
491                        }
492                    }
493                }
494                1 => {
495                    // Small rotation on neighbor when primary qubit is |1⟩
496                    for i in 0..dim {
497                        if (i >> primary_idx) & 1 == 1 {
498                            // Primary qubit is |1⟩, apply partial X rotation to neighbor
499                            let neighbor_bit = (i >> neighbor_idx) & 1;
500                            let flipped_i = i ^ (1 << neighbor_idx);
501
502                            // Small, random amplitude swap
503                            let theta: f64 = fastrand::f64() * 0.2; // Small angle
504                            let cos_theta = theta.cos();
505                            let sin_theta = theta.sin();
506
507                            let amp_original = state_copy[i];
508                            let amp_flipped = state_copy[flipped_i];
509
510                            if neighbor_bit == 0 {
511                                state[i] = amp_original * Complex64::new(cos_theta, 0.0)
512                                    + amp_flipped * Complex64::new(sin_theta, 0.0);
513                                state[flipped_i] = amp_original * Complex64::new(-sin_theta, 0.0)
514                                    + amp_flipped * Complex64::new(cos_theta, 0.0);
515                            } else {
516                                state[i] = amp_original * Complex64::new(cos_theta, 0.0)
517                                    - amp_flipped * Complex64::new(sin_theta, 0.0);
518                                state[flipped_i] = amp_original * Complex64::new(sin_theta, 0.0)
519                                    + amp_flipped * Complex64::new(cos_theta, 0.0);
520                            }
521                        }
522                    }
523                }
524                _ => unreachable!(),
525            }
526        }
527
528        // Normalize the state
529        NoiseChannelType::normalize_state(state);
530
531        Ok(())
532    }
533
534    fn kraus_operators(&self) -> Vec<Vec<Complex64>> {
535        // The dominant, always-on crosstalk between two coupled qubits is the
536        // coherent ZZ interaction U_ZZ(phi) = exp(-i * phi/2 * (Z x Z)), with the
537        // coupling angle proportional to the crosstalk strength (phi = strength * PI).
538        // Because this is a coherent (unitary) process, the channel is described by
539        // a SINGLE Kraus operator K = U_ZZ, which is trivially trace-preserving
540        // (K^dagger K = I_4 since U_ZZ is unitary).
541        //
542        // Z x Z is diagonal in the computational basis (|00>, |01>, |10>, |11>)
543        // with eigenvalues (+1, -1, -1, +1), so
544        //   U_ZZ = diag(e^{-i phi/2}, e^{+i phi/2}, e^{+i phi/2}, e^{-i phi/2}).
545        // The result is returned as a flattened row-major 4x4 matrix (16 elems).
546        let phi = self.strength * PI;
547        let half = phi / 2.0;
548        let zero = Complex64::new(0.0, 0.0);
549        let phase_neg = Complex64::new(half.cos(), -half.sin()); // e^{-i phi/2}
550        let phase_pos = Complex64::new(half.cos(), half.sin()); // e^{+i phi/2}
551
552        // Diagonal 4x4 with the ZZ phases on the diagonal.
553        let mut op = vec![zero; 16];
554        op[0] = phase_neg; // |00>
555        op[5] = phase_pos; // |01>
556        op[10] = phase_pos; // |10>
557        op[15] = phase_neg; // |11>
558
559        vec![op]
560    }
561
562    fn probability(&self) -> f64 {
563        self.strength
564    }
565}
566
567/// Extension to `NoiseChannelType` to include advanced noise channels
568#[derive(Debug, Clone)]
569pub enum AdvancedNoiseChannelType {
570    /// Base noise channel types
571    Base(NoiseChannelType),
572
573    /// Two-qubit depolarizing channel
574    TwoQubitDepolarizing(TwoQubitDepolarizingChannel),
575
576    /// Thermal relaxation channel
577    ThermalRelaxation(ThermalRelaxationChannel),
578
579    /// Crosstalk channel
580    Crosstalk(CrosstalkChannel),
581}
582
583impl AdvancedNoiseChannelType {
584    /// Get the name of the noise channel
585    #[must_use]
586    pub fn name(&self) -> &'static str {
587        match self {
588            Self::Base(ch) => ch.name(),
589            Self::TwoQubitDepolarizing(ch) => ch.name(),
590            Self::ThermalRelaxation(ch) => ch.name(),
591            Self::Crosstalk(ch) => ch.name(),
592        }
593    }
594
595    /// Get the qubits this channel affects
596    #[must_use]
597    pub fn qubits(&self) -> Vec<QubitId> {
598        match self {
599            Self::Base(ch) => ch.qubits(),
600            Self::TwoQubitDepolarizing(ch) => ch.qubits(),
601            Self::ThermalRelaxation(ch) => ch.qubits(),
602            Self::Crosstalk(ch) => ch.qubits(),
603        }
604    }
605
606    /// Apply the noise channel to a state vector
607    pub fn apply_to_statevector(&self, state: &mut [Complex64]) -> QuantRS2Result<()> {
608        match self {
609            Self::Base(ch) => ch.apply_to_statevector(state),
610            Self::TwoQubitDepolarizing(ch) => ch.apply_to_statevector(state),
611            Self::ThermalRelaxation(ch) => ch.apply_to_statevector(state),
612            Self::Crosstalk(ch) => ch.apply_to_statevector(state),
613        }
614    }
615
616    /// Get the probability of the noise occurring
617    #[must_use]
618    pub fn probability(&self) -> f64 {
619        match self {
620            Self::Base(ch) => ch.probability(),
621            Self::TwoQubitDepolarizing(ch) => ch.probability(),
622            Self::ThermalRelaxation(ch) => ch.probability(),
623            Self::Crosstalk(ch) => ch.probability(),
624        }
625    }
626}
627
628/// Advanced noise model that supports the new noise channel types
629#[derive(Debug, Clone)]
630pub struct AdvancedNoiseModel {
631    /// List of noise channels
632    pub channels: Vec<AdvancedNoiseChannelType>,
633
634    /// Whether the noise is applied after each gate
635    pub per_gate: bool,
636}
637
638impl AdvancedNoiseModel {
639    /// Create a new empty noise model
640    #[must_use]
641    pub const fn new(per_gate: bool) -> Self {
642        Self {
643            channels: Vec::new(),
644            per_gate,
645        }
646    }
647
648    /// Add a basic noise channel to the model
649    pub fn add_base_channel(&mut self, channel: NoiseChannelType) -> &mut Self {
650        self.channels.push(AdvancedNoiseChannelType::Base(channel));
651        self
652    }
653
654    /// Add a two-qubit depolarizing noise channel to the model
655    pub fn add_two_qubit_depolarizing(
656        &mut self,
657        channel: TwoQubitDepolarizingChannel,
658    ) -> &mut Self {
659        self.channels
660            .push(AdvancedNoiseChannelType::TwoQubitDepolarizing(channel));
661        self
662    }
663
664    /// Add a thermal relaxation noise channel to the model
665    pub fn add_thermal_relaxation(&mut self, channel: ThermalRelaxationChannel) -> &mut Self {
666        self.channels
667            .push(AdvancedNoiseChannelType::ThermalRelaxation(channel));
668        self
669    }
670
671    /// Add a crosstalk noise channel to the model
672    pub fn add_crosstalk(&mut self, channel: CrosstalkChannel) -> &mut Self {
673        self.channels
674            .push(AdvancedNoiseChannelType::Crosstalk(channel));
675        self
676    }
677
678    /// Apply all noise channels to a state vector
679    pub fn apply_to_statevector(&self, state: &mut [Complex64]) -> QuantRS2Result<()> {
680        for channel in &self.channels {
681            channel.apply_to_statevector(state)?;
682        }
683
684        // Normalize the state vector after applying all noise channels
685        NoiseChannelType::normalize_state(state);
686
687        Ok(())
688    }
689
690    /// Get the total number of channels
691    #[must_use]
692    pub fn num_channels(&self) -> usize {
693        self.channels.len()
694    }
695
696    /// Convert to basic noise model (for backward compatibility)
697    #[must_use]
698    pub fn to_basic_model(&self) -> NoiseModel {
699        let mut model = NoiseModel::new(self.per_gate);
700
701        for channel in &self.channels {
702            if let AdvancedNoiseChannelType::Base(ch) = channel {
703                model.channels.push(ch.clone());
704            }
705        }
706
707        model
708    }
709}
710
711impl Default for AdvancedNoiseModel {
712    fn default() -> Self {
713        Self::new(true)
714    }
715}
716
717/// Builder for realistic device noise models
718pub struct RealisticNoiseModelBuilder {
719    model: AdvancedNoiseModel,
720}
721
722impl RealisticNoiseModelBuilder {
723    /// Create a new noise model builder
724    #[must_use]
725    pub const fn new(per_gate: bool) -> Self {
726        Self {
727            model: AdvancedNoiseModel::new(per_gate),
728        }
729    }
730
731    /// Add realistic IBM Quantum device noise parameters
732    #[must_use]
733    pub fn with_ibm_device_noise(mut self, qubits: &[QubitId], device_name: &str) -> Self {
734        match device_name {
735            "ibmq_lima" | "ibmq_belem" | "ibmq_quito" => {
736                // 5-qubit IBM Quantum Falcon processors
737                // Parameters are approximate and based on typical values
738
739                // Relaxation and dephasing times
740                let t1_values = [115e-6, 100e-6, 120e-6, 105e-6, 110e-6]; // ~100 microseconds
741                let t2_values = [95e-6, 80e-6, 100e-6, 90e-6, 85e-6]; // ~90 microseconds
742
743                // Single-qubit gates
744                let gate_time_1q = 35e-9; // 35 nanoseconds
745                let gate_error_1q = 0.001; // 0.1% error rate
746
747                // Two-qubit gates (CNOT)
748                // let _gate_time_2q = 300e-9; // 300 nanoseconds
749                let gate_error_2q = 0.01; // 1% error rate
750
751                // Readout errors
752                let readout_error = 0.025; // 2.5% error
753
754                // Add individual qubit noise
755                for (i, &qubit) in qubits.iter().enumerate().take(5) {
756                    let t1 = t1_values[i % 5];
757                    let t2 = t2_values[i % 5];
758
759                    // Add thermal relaxation
760                    self.model.add_thermal_relaxation(ThermalRelaxationChannel {
761                        target: qubit,
762                        t1,
763                        t2,
764                        gate_time: gate_time_1q,
765                        excited_state_population: 0.01, // ~1% thermal excitation
766                    });
767
768                    // Add depolarizing noise for single-qubit gates
769                    self.model.add_base_channel(NoiseChannelType::Depolarizing(
770                        crate::noise::DepolarizingChannel {
771                            target: qubit,
772                            probability: gate_error_1q,
773                        },
774                    ));
775
776                    // Add readout error as a bit flip channel
777                    self.model.add_base_channel(NoiseChannelType::BitFlip(
778                        crate::noise::BitFlipChannel {
779                            target: qubit,
780                            probability: readout_error,
781                        },
782                    ));
783                }
784
785                // Add two-qubit gate noise (for nearest-neighbor connectivity)
786                for i in 0..qubits.len().saturating_sub(1) {
787                    let q1 = qubits[i];
788                    let q2 = qubits[i + 1];
789
790                    // Add two-qubit depolarizing noise
791                    self.model
792                        .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
793                            qubit1: q1,
794                            qubit2: q2,
795                            probability: gate_error_2q,
796                        });
797
798                    // Add crosstalk between adjacent qubits
799                    self.model.add_crosstalk(CrosstalkChannel {
800                        primary: q1,
801                        neighbor: q2,
802                        strength: 0.003, // 0.3% crosstalk
803                    });
804                }
805            }
806            "ibmq_bogota" | "ibmq_santiago" | "ibmq_casablanca" => {
807                // 5-qubit IBM Quantum Falcon processors (newer)
808                // Parameters are approximate and based on typical values
809
810                // Relaxation and dephasing times
811                let t1_values = [140e-6, 130e-6, 145e-6, 135e-6, 150e-6]; // ~140 microseconds
812                let t2_values = [120e-6, 110e-6, 125e-6, 115e-6, 130e-6]; // ~120 microseconds
813
814                // Single-qubit gates
815                let gate_time_1q = 30e-9; // 30 nanoseconds
816                let gate_error_1q = 0.0005; // 0.05% error rate
817
818                // Two-qubit gates (CNOT)
819                // let _gate_time_2q = 250e-9; // 250 nanoseconds
820                let gate_error_2q = 0.008; // 0.8% error rate
821
822                // Readout errors
823                let readout_error = 0.02; // 2% error
824
825                // Add individual qubit noise
826                for (i, &qubit) in qubits.iter().enumerate().take(5) {
827                    let t1 = t1_values[i % 5];
828                    let t2 = t2_values[i % 5];
829
830                    // Add thermal relaxation
831                    self.model.add_thermal_relaxation(ThermalRelaxationChannel {
832                        target: qubit,
833                        t1,
834                        t2,
835                        gate_time: gate_time_1q,
836                        excited_state_population: 0.008, // ~0.8% thermal excitation
837                    });
838
839                    // Add depolarizing noise for single-qubit gates
840                    self.model.add_base_channel(NoiseChannelType::Depolarizing(
841                        crate::noise::DepolarizingChannel {
842                            target: qubit,
843                            probability: gate_error_1q,
844                        },
845                    ));
846
847                    // Add readout error as a bit flip channel
848                    self.model.add_base_channel(NoiseChannelType::BitFlip(
849                        crate::noise::BitFlipChannel {
850                            target: qubit,
851                            probability: readout_error,
852                        },
853                    ));
854                }
855
856                // Add two-qubit gate noise (for nearest-neighbor connectivity)
857                for i in 0..qubits.len().saturating_sub(1) {
858                    let q1 = qubits[i];
859                    let q2 = qubits[i + 1];
860
861                    // Add two-qubit depolarizing noise
862                    self.model
863                        .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
864                            qubit1: q1,
865                            qubit2: q2,
866                            probability: gate_error_2q,
867                        });
868
869                    // Add crosstalk between adjacent qubits
870                    self.model.add_crosstalk(CrosstalkChannel {
871                        primary: q1,
872                        neighbor: q2,
873                        strength: 0.002, // 0.2% crosstalk
874                    });
875                }
876            }
877            "ibm_cairo" | "ibm_hanoi" | "ibm_auckland" => {
878                // 27-qubit IBM Quantum Falcon processors
879                // Parameters are approximate and based on typical values
880
881                // Relaxation and dephasing times (average values)
882                let t1 = 130e-6; // 130 microseconds
883                let t2 = 100e-6; // 100 microseconds
884
885                // Single-qubit gates
886                let gate_time_1q = 35e-9; // 35 nanoseconds
887                let gate_error_1q = 0.0004; // 0.04% error rate
888
889                // Two-qubit gates (CNOT)
890                // let _gate_time_2q = 275e-9; // 275 nanoseconds
891                let gate_error_2q = 0.007; // 0.7% error rate
892
893                // Readout errors
894                let readout_error = 0.018; // 1.8% error
895
896                // Add individual qubit noise
897                for &qubit in qubits {
898                    // Add thermal relaxation
899                    self.model.add_thermal_relaxation(ThermalRelaxationChannel {
900                        target: qubit,
901                        t1,
902                        t2,
903                        gate_time: gate_time_1q,
904                        excited_state_population: 0.007, // ~0.7% thermal excitation
905                    });
906
907                    // Add depolarizing noise for single-qubit gates
908                    self.model.add_base_channel(NoiseChannelType::Depolarizing(
909                        crate::noise::DepolarizingChannel {
910                            target: qubit,
911                            probability: gate_error_1q,
912                        },
913                    ));
914
915                    // Add readout error as a bit flip channel
916                    self.model.add_base_channel(NoiseChannelType::BitFlip(
917                        crate::noise::BitFlipChannel {
918                            target: qubit,
919                            probability: readout_error,
920                        },
921                    ));
922                }
923
924                // Add two-qubit gate noise (for nearest-neighbor connectivity)
925                for i in 0..qubits.len().saturating_sub(1) {
926                    let q1 = qubits[i];
927                    let q2 = qubits[i + 1];
928
929                    // Add two-qubit depolarizing noise
930                    self.model
931                        .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
932                            qubit1: q1,
933                            qubit2: q2,
934                            probability: gate_error_2q,
935                        });
936
937                    // Add crosstalk between adjacent qubits
938                    self.model.add_crosstalk(CrosstalkChannel {
939                        primary: q1,
940                        neighbor: q2,
941                        strength: 0.0015, // 0.15% crosstalk
942                    });
943                }
944            }
945            "ibm_washington" | "ibm_eagle" => {
946                // 127-qubit IBM Quantum Eagle processors
947                // Parameters are approximate and based on typical values
948
949                // Relaxation and dephasing times (average values)
950                let t1 = 150e-6; // 150 microseconds
951                let t2 = 120e-6; // 120 microseconds
952
953                // Single-qubit gates
954                let gate_time_1q = 30e-9; // 30 nanoseconds
955                let gate_error_1q = 0.0003; // 0.03% error rate
956
957                // Two-qubit gates (CNOT)
958                // let _gate_time_2q = 220e-9; // 220 nanoseconds
959                let gate_error_2q = 0.006; // 0.6% error rate
960
961                // Readout errors
962                let readout_error = 0.015; // 1.5% error
963
964                // Add individual qubit noise
965                for &qubit in qubits {
966                    // Add thermal relaxation
967                    self.model.add_thermal_relaxation(ThermalRelaxationChannel {
968                        target: qubit,
969                        t1,
970                        t2,
971                        gate_time: gate_time_1q,
972                        excited_state_population: 0.006, // ~0.6% thermal excitation
973                    });
974
975                    // Add depolarizing noise for single-qubit gates
976                    self.model.add_base_channel(NoiseChannelType::Depolarizing(
977                        crate::noise::DepolarizingChannel {
978                            target: qubit,
979                            probability: gate_error_1q,
980                        },
981                    ));
982
983                    // Add readout error as a bit flip channel
984                    self.model.add_base_channel(NoiseChannelType::BitFlip(
985                        crate::noise::BitFlipChannel {
986                            target: qubit,
987                            probability: readout_error,
988                        },
989                    ));
990                }
991
992                // Add two-qubit gate noise (for nearest-neighbor connectivity)
993                for i in 0..qubits.len().saturating_sub(1) {
994                    let q1 = qubits[i];
995                    let q2 = qubits[i + 1];
996
997                    // Add two-qubit depolarizing noise
998                    self.model
999                        .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
1000                            qubit1: q1,
1001                            qubit2: q2,
1002                            probability: gate_error_2q,
1003                        });
1004
1005                    // Add crosstalk between adjacent qubits
1006                    self.model.add_crosstalk(CrosstalkChannel {
1007                        primary: q1,
1008                        neighbor: q2,
1009                        strength: 0.001, // 0.1% crosstalk
1010                    });
1011                }
1012            }
1013            _ => {
1014                // Generic IBM Quantum device (conservative estimates)
1015                // Parameters are approximate and based on typical values
1016
1017                // Relaxation and dephasing times (average values)
1018                let t1 = 100e-6; // 100 microseconds
1019                let t2 = 80e-6; // 80 microseconds
1020
1021                // Single-qubit gates
1022                let gate_time_1q = 40e-9; // 40 nanoseconds
1023                let gate_error_1q = 0.001; // 0.1% error rate
1024
1025                // Two-qubit gates (CNOT)
1026                // let _gate_time_2q = 300e-9; // 300 nanoseconds
1027                let gate_error_2q = 0.01; // 1% error rate
1028
1029                // Readout errors
1030                let readout_error = 0.025; // 2.5% error
1031
1032                // Add individual qubit noise
1033                for &qubit in qubits {
1034                    // Add thermal relaxation
1035                    self.model.add_thermal_relaxation(ThermalRelaxationChannel {
1036                        target: qubit,
1037                        t1,
1038                        t2,
1039                        gate_time: gate_time_1q,
1040                        excited_state_population: 0.01, // ~1% thermal excitation
1041                    });
1042
1043                    // Add depolarizing noise for single-qubit gates
1044                    self.model.add_base_channel(NoiseChannelType::Depolarizing(
1045                        crate::noise::DepolarizingChannel {
1046                            target: qubit,
1047                            probability: gate_error_1q,
1048                        },
1049                    ));
1050
1051                    // Add readout error as a bit flip channel
1052                    self.model.add_base_channel(NoiseChannelType::BitFlip(
1053                        crate::noise::BitFlipChannel {
1054                            target: qubit,
1055                            probability: readout_error,
1056                        },
1057                    ));
1058                }
1059
1060                // Add two-qubit gate noise (for nearest-neighbor connectivity)
1061                for i in 0..qubits.len().saturating_sub(1) {
1062                    let q1 = qubits[i];
1063                    let q2 = qubits[i + 1];
1064
1065                    // Add two-qubit depolarizing noise
1066                    self.model
1067                        .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
1068                            qubit1: q1,
1069                            qubit2: q2,
1070                            probability: gate_error_2q,
1071                        });
1072
1073                    // Add crosstalk between adjacent qubits
1074                    self.model.add_crosstalk(CrosstalkChannel {
1075                        primary: q1,
1076                        neighbor: q2,
1077                        strength: 0.003, // 0.3% crosstalk
1078                    });
1079                }
1080            }
1081        }
1082
1083        self
1084    }
1085
1086    /// Add realistic Rigetti device noise parameters
1087    #[must_use]
1088    pub fn with_rigetti_device_noise(mut self, qubits: &[QubitId], device_name: &str) -> Self {
1089        match device_name {
1090            "Aspen-M-3" | "Aspen-M-2" => {
1091                // Rigetti Aspen-M series processors
1092                // Parameters are approximate and based on typical values
1093
1094                // Relaxation and dephasing times (average values)
1095                let t1 = 20e-6; // 20 microseconds
1096                let t2 = 15e-6; // 15 microseconds
1097
1098                // Single-qubit gates
1099                let gate_time_1q = 50e-9; // 50 nanoseconds
1100                let gate_error_1q = 0.0015; // 0.15% error rate
1101
1102                // Two-qubit gates (CZ)
1103                // let _gate_time_2q = 220e-9; // 220 nanoseconds
1104                let gate_error_2q = 0.02; // 2% error rate
1105
1106                // Readout errors
1107                let readout_error = 0.03; // 3% error
1108
1109                // Add individual qubit noise
1110                for &qubit in qubits {
1111                    // Add thermal relaxation
1112                    self.model.add_thermal_relaxation(ThermalRelaxationChannel {
1113                        target: qubit,
1114                        t1,
1115                        t2,
1116                        gate_time: gate_time_1q,
1117                        excited_state_population: 0.02, // ~2% thermal excitation
1118                    });
1119
1120                    // Add depolarizing noise for single-qubit gates
1121                    self.model.add_base_channel(NoiseChannelType::Depolarizing(
1122                        crate::noise::DepolarizingChannel {
1123                            target: qubit,
1124                            probability: gate_error_1q,
1125                        },
1126                    ));
1127
1128                    // Add readout error as a bit flip channel
1129                    self.model.add_base_channel(NoiseChannelType::BitFlip(
1130                        crate::noise::BitFlipChannel {
1131                            target: qubit,
1132                            probability: readout_error,
1133                        },
1134                    ));
1135                }
1136
1137                // Add two-qubit gate noise (for nearest-neighbor connectivity)
1138                for i in 0..qubits.len().saturating_sub(1) {
1139                    let q1 = qubits[i];
1140                    let q2 = qubits[i + 1];
1141
1142                    // Add two-qubit depolarizing noise
1143                    self.model
1144                        .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
1145                            qubit1: q1,
1146                            qubit2: q2,
1147                            probability: gate_error_2q,
1148                        });
1149
1150                    // Add crosstalk between adjacent qubits
1151                    self.model.add_crosstalk(CrosstalkChannel {
1152                        primary: q1,
1153                        neighbor: q2,
1154                        strength: 0.004, // 0.4% crosstalk
1155                    });
1156                }
1157            }
1158            _ => {
1159                // Generic Rigetti device (conservative estimates)
1160                // Parameters are approximate and based on typical values
1161
1162                // Relaxation and dephasing times (average values)
1163                let t1 = 15e-6; // 15 microseconds
1164                let t2 = 12e-6; // 12 microseconds
1165
1166                // Single-qubit gates
1167                let gate_time_1q = 60e-9; // 60 nanoseconds
1168                let gate_error_1q = 0.002; // 0.2% error rate
1169
1170                // Two-qubit gates (CZ)
1171                // let _gate_time_2q = 250e-9; // 250 nanoseconds
1172                let gate_error_2q = 0.025; // 2.5% error rate
1173
1174                // Readout errors
1175                let readout_error = 0.035; // 3.5% error
1176
1177                // Add individual qubit noise
1178                for &qubit in qubits {
1179                    // Add thermal relaxation
1180                    self.model.add_thermal_relaxation(ThermalRelaxationChannel {
1181                        target: qubit,
1182                        t1,
1183                        t2,
1184                        gate_time: gate_time_1q,
1185                        excited_state_population: 0.025, // ~2.5% thermal excitation
1186                    });
1187
1188                    // Add depolarizing noise for single-qubit gates
1189                    self.model.add_base_channel(NoiseChannelType::Depolarizing(
1190                        crate::noise::DepolarizingChannel {
1191                            target: qubit,
1192                            probability: gate_error_1q,
1193                        },
1194                    ));
1195
1196                    // Add readout error as a bit flip channel
1197                    self.model.add_base_channel(NoiseChannelType::BitFlip(
1198                        crate::noise::BitFlipChannel {
1199                            target: qubit,
1200                            probability: readout_error,
1201                        },
1202                    ));
1203                }
1204
1205                // Add two-qubit gate noise (for nearest-neighbor connectivity)
1206                for i in 0..qubits.len().saturating_sub(1) {
1207                    let q1 = qubits[i];
1208                    let q2 = qubits[i + 1];
1209
1210                    // Add two-qubit depolarizing noise
1211                    self.model
1212                        .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
1213                            qubit1: q1,
1214                            qubit2: q2,
1215                            probability: gate_error_2q,
1216                        });
1217
1218                    // Add crosstalk between adjacent qubits
1219                    self.model.add_crosstalk(CrosstalkChannel {
1220                        primary: q1,
1221                        neighbor: q2,
1222                        strength: 0.005, // 0.5% crosstalk
1223                    });
1224                }
1225            }
1226        }
1227
1228        self
1229    }
1230
1231    /// Add custom thermal relaxation parameters
1232    #[must_use]
1233    pub fn with_custom_thermal_relaxation(
1234        mut self,
1235        qubits: &[QubitId],
1236        t1: Duration,
1237        t2: Duration,
1238        gate_time: Duration,
1239    ) -> Self {
1240        let t1_seconds = t1.as_secs_f64();
1241        let t2_seconds = t2.as_secs_f64();
1242        let gate_time_seconds = gate_time.as_secs_f64();
1243
1244        for &qubit in qubits {
1245            self.model.add_thermal_relaxation(ThermalRelaxationChannel {
1246                target: qubit,
1247                t1: t1_seconds,
1248                t2: t2_seconds,
1249                gate_time: gate_time_seconds,
1250                excited_state_population: 0.01, // Default 1% thermal excitation
1251            });
1252        }
1253
1254        self
1255    }
1256
1257    /// Add custom two-qubit depolarizing noise
1258    #[must_use]
1259    pub fn with_custom_two_qubit_noise(
1260        mut self,
1261        qubit_pairs: &[(QubitId, QubitId)],
1262        probability: f64,
1263    ) -> Self {
1264        for &(q1, q2) in qubit_pairs {
1265            self.model
1266                .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
1267                    qubit1: q1,
1268                    qubit2: q2,
1269                    probability,
1270                });
1271        }
1272
1273        self
1274    }
1275
1276    /// Add custom crosstalk noise between pairs of qubits
1277    #[must_use]
1278    pub fn with_custom_crosstalk(
1279        mut self,
1280        qubit_pairs: &[(QubitId, QubitId)],
1281        strength: f64,
1282    ) -> Self {
1283        for &(q1, q2) in qubit_pairs {
1284            self.model.add_crosstalk(CrosstalkChannel {
1285                primary: q1,
1286                neighbor: q2,
1287                strength,
1288            });
1289        }
1290
1291        self
1292    }
1293
1294    /// Build the noise model
1295    #[must_use]
1296    pub fn build(self) -> AdvancedNoiseModel {
1297        self.model
1298    }
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303    use super::*;
1304
1305    /// Accumulate `sum_k K_k^dagger K_k` for a Kraus set whose operators are
1306    /// flattened row-major square matrices, returning the resulting `dim x dim`
1307    /// matrix (flattened row-major). The dimension is inferred from the length of
1308    /// each inner vector (a `dim x dim` matrix has `dim * dim` entries).
1309    fn accumulate_kraus_completeness(kraus: &[Vec<Complex64>]) -> Vec<Complex64> {
1310        assert!(!kraus.is_empty(), "Kraus set must be non-empty");
1311
1312        let len = kraus[0].len();
1313        let dim = (len as f64).sqrt().round() as usize;
1314        assert_eq!(
1315            dim * dim,
1316            len,
1317            "each Kraus operator must be a square matrix"
1318        );
1319
1320        let mut acc = vec![Complex64::new(0.0, 0.0); len];
1321        for op in kraus {
1322            assert_eq!(op.len(), len, "all Kraus operators must share a dimension");
1323            // (K^dagger K)[i][j] = sum_k conj(K[k][i]) * K[k][j]
1324            for i in 0..dim {
1325                for j in 0..dim {
1326                    let mut sum = Complex64::new(0.0, 0.0);
1327                    for k in 0..dim {
1328                        sum += op[k * dim + i].conj() * op[k * dim + j];
1329                    }
1330                    acc[i * dim + j] += sum;
1331                }
1332            }
1333        }
1334
1335        acc
1336    }
1337
1338    /// Assert that a flattened `dim x dim` matrix equals the identity within
1339    /// `tol`.
1340    fn assert_is_identity(matrix: &[Complex64], tol: f64) {
1341        let len = matrix.len();
1342        let dim = (len as f64).sqrt().round() as usize;
1343        assert_eq!(dim * dim, len, "matrix must be square");
1344
1345        for i in 0..dim {
1346            for j in 0..dim {
1347                let value = matrix[i * dim + j];
1348                let expected = if i == j { 1.0 } else { 0.0 };
1349                assert!(
1350                    (value.re - expected).abs() < tol && value.im.abs() < tol,
1351                    "element [{i}][{j}] = {value:?} is not {expected} within {tol}"
1352                );
1353            }
1354        }
1355    }
1356
1357    #[test]
1358    fn test_two_qubit_depolarizing_kraus_trace_preserving() {
1359        let channel = TwoQubitDepolarizingChannel {
1360            qubit1: QubitId::new(0),
1361            qubit2: QubitId::new(1),
1362            probability: 0.137,
1363        };
1364
1365        let kraus = channel.kraus_operators();
1366
1367        // Two-qubit depolarizing has exactly 16 Kraus operators (15 Pauli pairs
1368        // plus identity), not the old fabricated 2 one-element vectors.
1369        assert_eq!(kraus.len(), 16, "expected 16 Kraus operators");
1370        for op in &kraus {
1371            assert_eq!(op.len(), 16, "each operator must be a flattened 4x4 matrix");
1372        }
1373
1374        let completeness = accumulate_kraus_completeness(&kraus);
1375        assert_is_identity(&completeness, 1e-10);
1376    }
1377
1378    #[test]
1379    fn test_two_qubit_depolarizing_kraus_edge_probabilities() {
1380        for &probability in &[0.0_f64, 1.0_f64] {
1381            let channel = TwoQubitDepolarizingChannel {
1382                qubit1: QubitId::new(0),
1383                qubit2: QubitId::new(1),
1384                probability,
1385            };
1386            let kraus = channel.kraus_operators();
1387            assert_eq!(kraus.len(), 16);
1388            let completeness = accumulate_kraus_completeness(&kraus);
1389            assert_is_identity(&completeness, 1e-10);
1390        }
1391    }
1392
1393    #[test]
1394    fn test_thermal_relaxation_kraus_trace_preserving() {
1395        let channel = ThermalRelaxationChannel {
1396            target: QubitId::new(0),
1397            t1: 100e-6,
1398            t2: 80e-6,
1399            gate_time: 35e-9,
1400            excited_state_population: 0.03,
1401        };
1402
1403        let kraus = channel.kraus_operators();
1404
1405        // Composition of generalized amplitude damping (4 ops) and phase damping
1406        // (2 ops) yields 8 single-qubit operators, well beyond the old single
1407        // fabricated one-element vector.
1408        assert!(
1409            kraus.len() >= 4,
1410            "expected at least 4 Kraus operators, got {}",
1411            kraus.len()
1412        );
1413        assert_eq!(kraus.len(), 8, "expected 8 composed Kraus operators");
1414        for op in &kraus {
1415            assert_eq!(op.len(), 4, "each operator must be a flattened 2x2 matrix");
1416        }
1417
1418        let completeness = accumulate_kraus_completeness(&kraus);
1419        assert_is_identity(&completeness, 1e-10);
1420    }
1421
1422    #[test]
1423    fn test_thermal_relaxation_kraus_extreme_parameters() {
1424        // Very short T1/T2 relative to gate time drive gamma and gamma_phi toward
1425        // 1; the decomposition must remain trace-preserving and clamped.
1426        let channel = ThermalRelaxationChannel {
1427            target: QubitId::new(0),
1428            t1: 1e-9,
1429            t2: 1e-9,
1430            gate_time: 1e-6,
1431            excited_state_population: 0.5,
1432        };
1433
1434        let kraus = channel.kraus_operators();
1435        assert_eq!(kraus.len(), 8);
1436        let completeness = accumulate_kraus_completeness(&kraus);
1437        assert_is_identity(&completeness, 1e-10);
1438    }
1439
1440    #[test]
1441    fn test_crosstalk_kraus_trace_preserving() {
1442        let channel = CrosstalkChannel {
1443            primary: QubitId::new(0),
1444            neighbor: QubitId::new(1),
1445            strength: 0.3,
1446        };
1447
1448        let kraus = channel.kraus_operators();
1449
1450        // Coherent ZZ crosstalk is a single unitary Kraus operator on the
1451        // two-qubit (4x4) space, not the old fabricated 1-element vector.
1452        assert_eq!(kraus.len(), 1, "coherent crosstalk has a single Kraus op");
1453        assert_eq!(
1454            kraus[0].len(),
1455            16,
1456            "the operator must be a flattened 4x4 unitary"
1457        );
1458
1459        let completeness = accumulate_kraus_completeness(&kraus);
1460        assert_is_identity(&completeness, 1e-10);
1461    }
1462
1463    #[test]
1464    fn test_tensor_2x2_matches_known_pauli_product() {
1465        let paulis = single_qubit_paulis();
1466        // Z x X should map |0>|+> style basis correctly; verify the explicit
1467        // 4x4 layout against a hand-computed reference.
1468        let z = &paulis[3];
1469        let x = &paulis[1];
1470        let zx = tensor_2x2(z, x);
1471
1472        // Z x X = [[0,1,0,0],[1,0,0,0],[0,0,0,-1],[0,0,-1,0]]
1473        let one = Complex64::new(1.0, 0.0);
1474        let neg_one = Complex64::new(-1.0, 0.0);
1475        let zero = Complex64::new(0.0, 0.0);
1476        let expected = vec![
1477            zero, one, zero, zero, one, zero, zero, zero, zero, zero, zero, neg_one, zero, zero,
1478            neg_one, zero,
1479        ];
1480        for (got, want) in zx.iter().zip(expected.iter()) {
1481            assert!((got.re - want.re).abs() < 1e-12 && (got.im - want.im).abs() < 1e-12);
1482        }
1483    }
1484
1485    #[test]
1486    fn test_matmul_2x2_identity_and_pauli() {
1487        let paulis = single_qubit_paulis();
1488        let identity = &paulis[0];
1489        let x = &paulis[1];
1490
1491        // I * X == X
1492        let product = matmul_2x2(identity, x);
1493        for (got, want) in product.iter().zip(x.iter()) {
1494            assert!((got.re - want.re).abs() < 1e-12 && (got.im - want.im).abs() < 1e-12);
1495        }
1496
1497        // X * X == I
1498        let xx = matmul_2x2(x, x);
1499        for (got, want) in xx.iter().zip(identity.iter()) {
1500            assert!((got.re - want.re).abs() < 1e-12 && (got.im - want.im).abs() < 1e-12);
1501        }
1502    }
1503}