Skip to main content

cqlib_core/device/
noise.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13//! # Noise Model Module
14//!
15//! This module provides types for simulating quantum noise in device simulations.
16//! It includes single-qubit and two-qubit noise channels, readout errors,
17//! and a comprehensive noise model for realistic quantum circuit simulation.
18//!
19//! ## Noise Channels
20//!
21//! - **Single-qubit**: Bit-flip, phase-flip, depolarizing, amplitude damping, phase damping
22//! - **Two-qubit**: Two-qubit depolarizing
23//! - **Readout**: Measurement error modeling
24
25use crate::circuit::{Qubit, StandardGate};
26use crate::qis::pauli::Pauli;
27use ndarray::linalg::kron;
28use ndarray::{Array2, array};
29use num_complex::Complex64;
30use std::collections::HashMap;
31use std::hash::{Hash, Hasher};
32use thiserror::Error;
33
34const ZERO: Complex64 = Complex64::new(0.0, 0.0);
35const ONE: Complex64 = Complex64::new(1.0, 0.0);
36
37/// Defines errors that can occur when building or applying a noise model.
38#[derive(Error, Debug, Clone, PartialEq)]
39pub enum NoiseError {
40    /// Error for an invalid probability value, which must be in [0, 1].
41    #[error("Invalid noise probability: {value} (must be in [0, 1]). Context: {context}")]
42    InvalidProbability { value: f64, context: String },
43
44    /// Error when a gate is applied to non-distinct qubits.
45    #[error("Qubit collision detected: {qubits:?}. All qubits in a gate must be distinct.")]
46    QubitCollision { qubits: Vec<usize> },
47
48    /// Error for incorrect number of qubits provided to an operation.
49    #[error("Inconsistent arity: expected {expected} qubits, but got {actual}.")]
50    InconsistentArity { expected: u8, actual: u8 },
51
52    /// Internal error in the noise model.
53    #[error("Internal noise model error: {0}")]
54    Internal(String),
55}
56
57/// Represents single-qubit quantum noise channels.
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum SingleQubitNoise {
60    /// Bit-flip noise with probability $p$.
61    /// Kraus operators: $E_0 = \sqrt{1-p} I$, $E_1 = \sqrt{p} X$.
62    BitFlip(f64),
63    /// Phase-flip noise with probability $p$.
64    /// Kraus operators: $E_0 = \sqrt{1-p} I$, $E_1 = \sqrt{p} Z$.
65    PhaseFlip(f64),
66    /// General Pauli noise with probabilities $p_x$, $p_y$, $p_z$.
67    /// Kraus operators: $\sqrt{1-p_x-p_y-p_z} I$, $\sqrt{p_x} X$, $\sqrt{p_y} Y$, $\sqrt{p_z} Z$.
68    Pauli { px: f64, py: f64, pz: f64 },
69    /// Depolarizing noise with parameter $p$.
70    /// Kraus operators: $\sqrt{1-p} I$, $\sqrt{p/3} X$, $\sqrt{p/3} Y$, $\sqrt{p/3} Z$.
71    Depolarizing(f64),
72    /// Amplitude damping channel with damping parameter $\gamma$.
73    /// Kraus operators: $E_0 = \begin{pmatrix} 1 & 0 \\ 0 & \sqrt{1-\gamma} \end{pmatrix}$, $E_1 = \begin{pmatrix} 0 & \sqrt{\gamma} \\ 0 & 0 \end{pmatrix}$.
74    AmplitudeDamping(f64),
75    /// Phase damping channel with scattering probability $\lambda$.
76    /// Kraus operators: $E_0 = \begin{pmatrix} 1 & 0 \\ 0 & \sqrt{1-\lambda} \end{pmatrix}$, $E_1 = \begin{pmatrix} 0 & 0 \\ 0 & \sqrt{\lambda} \end{pmatrix}$.
77    PhaseDamping(f64),
78}
79
80impl SingleQubitNoise {
81    /// Checks if the noise parameters are valid probabilities.
82    pub fn is_valid(&self) -> bool {
83        match *self {
84            Self::BitFlip(p)
85            | Self::PhaseFlip(p)
86            | Self::Depolarizing(p)
87            | Self::AmplitudeDamping(p)
88            | Self::PhaseDamping(p) => (0.0..=1.0).contains(&p),
89            Self::Pauli { px, py, pz } => {
90                px >= 0.0 && py >= 0.0 && pz >= 0.0 && (px + py + pz) <= 1.0
91            }
92        }
93    }
94
95    fn validate(&self) -> Result<(), NoiseError> {
96        let probability = |value: f64, context: &str| {
97            if value.is_finite() && (0.0..=1.0).contains(&value) {
98                Ok(())
99            } else {
100                Err(NoiseError::InvalidProbability {
101                    value,
102                    context: context.to_string(),
103                })
104            }
105        };
106        match *self {
107            Self::BitFlip(p) => probability(p, "bit-flip probability"),
108            Self::PhaseFlip(p) => probability(p, "phase-flip probability"),
109            Self::Depolarizing(p) => probability(p, "single-qubit depolarizing probability"),
110            Self::AmplitudeDamping(p) => probability(p, "amplitude-damping probability"),
111            Self::PhaseDamping(p) => probability(p, "phase-damping probability"),
112            Self::Pauli { px, py, pz } => {
113                probability(px, "Pauli X probability")?;
114                probability(py, "Pauli Y probability")?;
115                probability(pz, "Pauli Z probability")?;
116                let total = px + py + pz;
117                if total <= 1.0 {
118                    Ok(())
119                } else {
120                    Err(NoiseError::InvalidProbability {
121                        value: total,
122                        context: "sum of Pauli probabilities".to_string(),
123                    })
124                }
125            }
126        }
127    }
128
129    /// Returns the Kraus operators for the single-qubit noise channel.
130    pub fn to_kraus(&self) -> Vec<Array2<Complex64>> {
131        match *self {
132            Self::BitFlip(p) => {
133                vec![
134                    Pauli::I.to_matrix() * (1.0 - p).sqrt(),
135                    Pauli::X.to_matrix() * p.sqrt(),
136                ]
137            }
138            Self::PhaseFlip(p) => {
139                vec![
140                    Pauli::I.to_matrix() * (1.0 - p).sqrt(),
141                    Pauli::Z.to_matrix() * p.sqrt(),
142                ]
143            }
144            Self::Depolarizing(p) => {
145                let p_i = (1.0 - p).sqrt();
146                let p_other = (p / 3.0).sqrt();
147                vec![
148                    Pauli::I.to_matrix() * p_i,
149                    Pauli::X.to_matrix() * p_other,
150                    Pauli::Y.to_matrix() * p_other,
151                    Pauli::Z.to_matrix() * p_other,
152                ]
153            }
154            Self::AmplitudeDamping(gamma) => {
155                vec![
156                    array![[ONE, ZERO], [ZERO, Complex64::from((1.0 - gamma).sqrt())]],
157                    array![[ZERO, Complex64::from(gamma.sqrt())], [ZERO, ZERO]],
158                ]
159            }
160            Self::PhaseDamping(lambda) => {
161                vec![
162                    array![[ONE, ZERO], [ZERO, Complex64::from((1.0 - lambda).sqrt())]],
163                    array![[ZERO, ZERO], [ZERO, Complex64::from(lambda.sqrt())]],
164                ]
165            }
166            Self::Pauli { px, py, pz } => {
167                let pi = (1.0 - (px + py + pz)).sqrt();
168                vec![
169                    Pauli::I.to_matrix() * pi,
170                    Pauli::X.to_matrix() * px.sqrt(),
171                    Pauli::Y.to_matrix() * py.sqrt(),
172                    Pauli::Z.to_matrix() * pz.sqrt(),
173                ]
174            }
175        }
176    }
177}
178
179/// Represents two-qubit quantum noise channels.
180#[derive(Debug, Clone, Copy, PartialEq)]
181pub enum TwoQubitNoise {
182    /// Standard 2-qubit depolarizing channel.
183    /// Error probability $p/15$ for each of the 15 non-identity 2-qubit Pauli operators $P_i \otimes P_j$.
184    Depolarizing(f64),
185
186    /// Independent single-qubit noise applied to each qubit in a two-qubit gate.
187    /// Channel is $\mathcal{E}_{q0} \otimes \mathcal{E}_{q1}$.
188    Independent {
189        q0_noise: SingleQubitNoise,
190        q1_noise: SingleQubitNoise,
191    },
192
193    /// Correlated Pauli error on two qubits with probability $p$.
194    /// Kraus operators: $\sqrt{1-p} I \otimes I$ and $\sqrt{p} P_{q0} \otimes P_{q1}$.
195    CorrelatedPauli { op_q0: Pauli, op_q1: Pauli, p: f64 },
196}
197
198impl TwoQubitNoise {
199    /// Checks if the noise parameters are valid.
200    pub fn is_valid(&self) -> bool {
201        match *self {
202            Self::Depolarizing(p) | Self::CorrelatedPauli { p, .. } => (0.0..=1.0).contains(&p),
203            Self::Independent { q0_noise, q1_noise } => q0_noise.is_valid() && q1_noise.is_valid(),
204        }
205    }
206
207    fn validate(&self) -> Result<(), NoiseError> {
208        match *self {
209            Self::Depolarizing(p) => SingleQubitNoise::Depolarizing(p).validate().map_err(|_| {
210                NoiseError::InvalidProbability {
211                    value: p,
212                    context: "two-qubit depolarizing probability".to_string(),
213                }
214            }),
215            Self::CorrelatedPauli { p, .. } => {
216                SingleQubitNoise::BitFlip(p).validate().map_err(|_| {
217                    NoiseError::InvalidProbability {
218                        value: p,
219                        context: "correlated-Pauli probability".to_string(),
220                    }
221                })
222            }
223            Self::Independent { q0_noise, q1_noise } => {
224                q0_noise.validate()?;
225                q1_noise.validate()
226            }
227        }
228    }
229
230    /// Returns the Kraus operators for the two-qubit noise channel.
231    pub fn to_kraus(&self) -> Vec<Array2<Complex64>> {
232        match self {
233            Self::Independent { q0_noise, q1_noise } => {
234                let ks0 = q0_noise.to_kraus();
235                let ks1 = q1_noise.to_kraus();
236                let mut result = Vec::with_capacity(ks0.len() * ks1.len());
237                for e in &ks0 {
238                    for f in &ks1 {
239                        result.push(kron(e, f));
240                    }
241                }
242                result
243            }
244            Self::Depolarizing(p) => {
245                let mut ops = Vec::with_capacity(16);
246                let p_i = (1.0 - p).sqrt();
247                let p_other = (p / 15.0).sqrt();
248
249                // Performance optimization: cache the 4 basic Pauli matrices
250                // to avoid repeated construction in the loop.
251                let pauli_matrices = [
252                    Pauli::I.to_matrix(),
253                    Pauli::X.to_matrix(),
254                    Pauli::Y.to_matrix(),
255                    Pauli::Z.to_matrix(),
256                ];
257
258                for (i, m0) in pauli_matrices.iter().enumerate() {
259                    for (j, m1) in pauli_matrices.iter().enumerate() {
260                        let m_combined = kron(m0, m1);
261                        if i == 0 && j == 0 {
262                            ops.push(m_combined * p_i);
263                        } else {
264                            ops.push(m_combined * p_other);
265                        }
266                    }
267                }
268                ops
269            }
270            Self::CorrelatedPauli { op_q0, op_q1, p } => {
271                let i_mat = Pauli::I.to_matrix();
272                vec![
273                    kron(&i_mat, &i_mat) * (1.0 - p).sqrt(),
274                    kron(&op_q0.to_matrix(), &op_q1.to_matrix()) * p.sqrt(),
275                ]
276            }
277        }
278    }
279}
280
281/// Represents asymmetric readout errors.
282#[derive(Debug, Clone, Copy, PartialEq)]
283pub struct ReadoutError {
284    /// Probability of measuring 0 given that the true state is 1: $P(0|1)$.
285    pub p_0_given_1: f64,
286    /// Probability of measuring 1 given that the true state is 0: $P(1|0)$.
287    pub p_1_given_0: f64,
288}
289
290impl ReadoutError {
291    /// Checks if the probabilities are valid.
292    pub fn is_valid(&self) -> bool {
293        (0.0..=1.0).contains(&self.p_0_given_1) && (0.0..=1.0).contains(&self.p_1_given_0)
294    }
295
296    fn validate(&self) -> Result<(), NoiseError> {
297        for (value, context) in [
298            (self.p_0_given_1, "readout P(0|1)"),
299            (self.p_1_given_0, "readout P(1|0)"),
300        ] {
301            if !(value.is_finite() && (0.0..=1.0).contains(&value)) {
302                return Err(NoiseError::InvalidProbability {
303                    value,
304                    context: context.to_string(),
305                });
306            }
307        }
308        Ok(())
309    }
310}
311
312const MAX_GATE_ARITY: usize = 3;
313
314/// A key identifying a specific operation on specific qubits, used to map noise parameters.
315#[derive(Debug, Clone, Copy, Eq)]
316pub struct OperationKey {
317    /// The standard quantum gate.
318    gate: StandardGate,
319    /// Fixed-size array to store qubit indices without heap allocation.
320    /// Unused slots are zero-padded but ignored in Hash and Eq.
321    qubits: [usize; MAX_GATE_ARITY],
322    /// The actual number of qubits involved in the operation.
323    arity: u8,
324}
325
326// Manually implement Hash to only hash valid data, ignoring padding.
327impl Hash for OperationKey {
328    fn hash<H: Hasher>(&self, state: &mut H) {
329        self.gate.hash(state);
330        self.qubits().hash(state);
331    }
332}
333
334impl PartialEq for OperationKey {
335    fn eq(&self, other: &Self) -> bool {
336        // 1. Gate types must match
337        if self.gate != other.gate {
338            return false;
339        }
340        // 2. Arity must match
341        if self.arity != other.arity {
342            return false;
343        }
344        // 3. Compare only the valid slice of qubit indices, ignoring padding
345        self.qubits() == other.qubits()
346    }
347}
348
349impl OperationKey {
350    /// Constructor for a single-qubit gate operation key.
351    pub fn new_single(gate: StandardGate, q0: Qubit) -> Self {
352        Self {
353            gate,
354            qubits: [q0.index(), 0, 0],
355            arity: 1,
356        }
357    }
358
359    /// Constructor for a two-qubit gate operation key.
360    /// Returns an error if the qubits are identical.
361    pub fn new_double(gate: StandardGate, q0: Qubit, q1: Qubit) -> Result<Self, NoiseError> {
362        if q0 == q1 {
363            return Err(NoiseError::QubitCollision {
364                qubits: vec![q0.index(), q1.index()],
365            });
366        }
367        Ok(Self {
368            gate,
369            qubits: [q0.index(), q1.index(), 0],
370            arity: 2,
371        })
372    }
373
374    /// Constructor for a three-qubit gate operation key.
375    /// Returns an error if any qubits overlap.
376    pub fn new_triple(
377        gate: StandardGate,
378        q0: Qubit,
379        q1: Qubit,
380        q2: Qubit,
381    ) -> Result<Self, NoiseError> {
382        if q0 == q1 || q1 == q2 || q0 == q2 {
383            return Err(NoiseError::QubitCollision {
384                qubits: vec![q0.index(), q1.index(), q2.index()],
385            });
386        }
387        Ok(Self {
388            gate,
389            qubits: [q0.index(), q1.index(), q2.index()],
390            arity: 3,
391        })
392    }
393
394    /// Returns a slice of the actual qubits involved, ignoring padding.
395    pub fn qubits(&self) -> &[usize] {
396        &self.qubits[..self.arity as usize]
397    }
398
399    /// Returns the standard gate associated with the operation.
400    pub fn gate(&self) -> &StandardGate {
401        &self.gate
402    }
403}
404
405/// A noise model containing error definitions for different operations and measurements.
406#[derive(Debug, Clone, Default)]
407pub struct NoiseModel {
408    /// Readout errors mapped by qubit.
409    readout_errors: HashMap<Qubit, ReadoutError>,
410    /// Single-qubit errors mapped by operation key.
411    /// Distinct storage to prevent logical confusion.
412    single_gate_errors: HashMap<OperationKey, Vec<SingleQubitNoise>>,
413    /// Two-qubit errors mapped by operation key.
414    two_gate_errors: HashMap<OperationKey, Vec<TwoQubitNoise>>,
415}
416
417impl NoiseModel {
418    /// Creates a new, empty noise model.
419    pub fn new() -> Self {
420        Self::default()
421    }
422
423    /// Adds a readout error for a specific qubit.
424    pub fn add_readout_error(
425        &mut self,
426        qubit: Qubit,
427        error: ReadoutError,
428    ) -> Result<(), NoiseError> {
429        error.validate().map_err(|error| match error {
430            NoiseError::InvalidProbability { value, context } => NoiseError::InvalidProbability {
431                value,
432                context: format!("{context} for qubit {qubit:?}"),
433            },
434            other => other,
435        })?;
436        self.readout_errors.insert(qubit, error);
437        Ok(())
438    }
439
440    /// Adds single-qubit noise to a specific gate on a target qubit.
441    pub fn add_single_qubit_error(
442        &mut self,
443        gate: StandardGate,
444        qubit: Qubit,
445        noise: SingleQubitNoise,
446    ) -> Result<(), NoiseError> {
447        noise.validate().map_err(|error| match error {
448            NoiseError::InvalidProbability { value, context } => NoiseError::InvalidProbability {
449                value,
450                context: format!("{context} for gate {gate:?}"),
451            },
452            other => other,
453        })?;
454        let key = OperationKey::new_single(gate, qubit);
455        self.single_gate_errors.entry(key).or_default().push(noise);
456        Ok(())
457    }
458
459    /// Adds two-qubit noise to a specific gate on target qubits.
460    pub fn add_two_qubit_error(
461        &mut self,
462        gate: StandardGate,
463        q0: Qubit,
464        q1: Qubit,
465        noise: TwoQubitNoise,
466    ) -> Result<(), NoiseError> {
467        noise.validate().map_err(|error| match error {
468            NoiseError::InvalidProbability { value, context } => NoiseError::InvalidProbability {
469                value,
470                context: format!("{context} for gate {gate:?}"),
471            },
472            other => other,
473        })?;
474        let key = OperationKey::new_double(gate, q0, q1)?;
475        self.two_gate_errors.entry(key).or_default().push(noise);
476        Ok(())
477    }
478
479    /// Retrieves the readout error for a given qubit, if present.
480    pub fn get_readout_error(&self, key: &Qubit) -> Option<&ReadoutError> {
481        self.readout_errors.get(key)
482    }
483
484    /// Retrieves the list of single-qubit errors for a given operation key.
485    pub fn get_single_qubit_errors(&self, key: &OperationKey) -> Option<&Vec<SingleQubitNoise>> {
486        self.single_gate_errors.get(key)
487    }
488
489    /// Retrieves the list of two-qubit errors for a given operation key.
490    pub fn get_two_qubit_errors(&self, key: &OperationKey) -> Option<&Vec<TwoQubitNoise>> {
491        self.two_gate_errors.get(key)
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498    use crate::circuit::gate::standard_gate::StandardGate;
499
500    #[test]
501    fn operation_keys_allow_effective_controlled_arity_and_reject_collisions() {
502        let q0 = Qubit::new(0);
503        let q1 = Qubit::new(1);
504
505        assert_eq!(
506            OperationKey::new_single(StandardGate::CX, q0).qubits(),
507            &[0]
508        );
509        assert_eq!(
510            OperationKey::new_double(StandardGate::H, q0, q1)
511                .unwrap()
512                .qubits(),
513            &[0, 1]
514        );
515        assert_eq!(
516            OperationKey::new_triple(StandardGate::CX, q0, q1, Qubit::new(2))
517                .unwrap()
518                .qubits(),
519            &[0, 1, 2]
520        );
521        assert!(matches!(
522            OperationKey::new_double(StandardGate::CX, q0, q0),
523            Err(NoiseError::QubitCollision { .. })
524        ));
525        assert!(matches!(
526            OperationKey::new_triple(StandardGate::CCX, q0, q1, q1),
527            Err(NoiseError::QubitCollision { .. })
528        ));
529    }
530
531    #[test]
532    fn noise_errors_report_the_actual_invalid_probability() {
533        let mut model = NoiseModel::new();
534
535        match model.add_single_qubit_error(
536            StandardGate::H,
537            Qubit::new(0),
538            SingleQubitNoise::BitFlip(-0.25),
539        ) {
540            Err(NoiseError::InvalidProbability { value, context }) => {
541                assert_eq!(value, -0.25);
542                assert!(context.contains("bit-flip"));
543            }
544            result => panic!("unexpected result: {result:?}"),
545        }
546
547        match model.add_readout_error(
548            Qubit::new(0),
549            ReadoutError {
550                p_0_given_1: 1.5,
551                p_1_given_0: 0.0,
552            },
553        ) {
554            Err(NoiseError::InvalidProbability { value, context }) => {
555                assert_eq!(value, 1.5);
556                assert!(context.contains("P(0|1)"));
557            }
558            result => panic!("unexpected result: {result:?}"),
559        }
560    }
561
562    #[test]
563    fn pauli_probability_boundary_has_complete_finite_kraus_operators() {
564        let noise = SingleQubitNoise::Pauli {
565            px: 0.3,
566            py: 0.6,
567            pz: 0.1,
568        };
569        assert!(noise.is_valid());
570        let ops = noise.to_kraus();
571        let mut completeness = ndarray::Array2::<Complex64>::zeros((2, 2));
572        for op in &ops {
573            assert!(op.iter().all(|v| v.re.is_finite() && v.im.is_finite()));
574            completeness += &op.t().mapv(|v| v.conj()).dot(op);
575        }
576        for i in 0..2 {
577            for j in 0..2 {
578                assert!(
579                    (completeness[[i, j]] - Complex64::from(if i == j { 1.0 } else { 0.0 })).norm()
580                        < 1e-12
581                );
582            }
583        }
584        assert!(
585            !SingleQubitNoise::Pauli {
586                px: 0.4,
587                py: 0.6,
588                pz: 0.1
589            }
590            .is_valid()
591        );
592    }
593}