quantrs2_sim/
noise_models.rs

1//! Noise models for realistic quantum simulation
2//!
3//! This module provides comprehensive noise modeling capabilities for quantum circuits,
4//! essential for simulating real quantum hardware behavior. It implements various
5//! quantum noise channels using Kraus operator representations.
6//!
7//! # Features
8//!
9//! - **Standard Noise Channels**: Depolarizing, bit flip, phase flip, amplitude damping
10//! - **Thermal Relaxation**: T1/T2 decoherence modeling
11//! - **Composite Noise**: Combine multiple noise sources
12//! - **Gate-Specific Noise**: Apply noise to specific gate types
13//! - **Measurement Noise**: Readout error modeling
14//!
15//! # Example
16//!
17//! ```rust
18//! use quantrs2_sim::noise_models::{NoiseModel, DepolarizingNoise};
19//! use scirs2_core::ndarray::Array1;
20//! use scirs2_core::Complex64;
21//! use std::sync::Arc;
22//!
23//! // Create a noise model with depolarizing noise
24//! let mut noise_model = NoiseModel::new();
25//! noise_model.add_channel(Arc::new(DepolarizingNoise::new(0.01)));
26//!
27//! // Apply noise to a quantum state
28//! let state = Array1::from_vec(vec![
29//!     Complex64::new(1.0, 0.0),
30//!     Complex64::new(0.0, 0.0),
31//! ]);
32//! let noisy_state = noise_model.apply_single_qubit(&state, 0).unwrap();
33//! ```
34
35use crate::error::SimulatorError;
36use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
37use scirs2_core::random::prelude::*;
38use scirs2_core::{Complex64, ComplexFloat};
39use std::collections::HashMap;
40use std::sync::Arc;
41
42/// Result type for noise operations
43pub type NoiseResult<T> = Result<T, SimulatorError>;
44
45/// Trait for quantum noise channels
46///
47/// A noise channel is characterized by its Kraus operators {K_i}, which satisfy
48/// the completeness relation: ∑_i K_i† K_i = I
49pub trait NoiseChannel: Send + Sync {
50    /// Returns the Kraus operators for this noise channel
51    fn kraus_operators(&self) -> Vec<Array2<Complex64>>;
52
53    /// Returns the name of this noise channel
54    fn name(&self) -> &str;
55
56    /// Returns the number of qubits this channel acts on
57    fn num_qubits(&self) -> usize;
58
59    /// Apply the noise channel to a quantum state using Kraus operators
60    ///
61    /// For a state |ψ⟩, the noisy state is: ρ = ∑_i K_i |ψ⟩⟨ψ| K_i†
62    fn apply(&self, state: &ArrayView1<Complex64>) -> NoiseResult<Array1<Complex64>> {
63        let kraus_ops = self.kraus_operators();
64        let dim = state.len();
65
66        // Verify state dimension
67        if dim != 2_usize.pow(self.num_qubits() as u32) {
68            return Err(SimulatorError::DimensionMismatch(format!(
69                "State dimension {} does not match {} qubits (expected {})",
70                dim,
71                self.num_qubits(),
72                2_usize.pow(self.num_qubits() as u32)
73            )));
74        }
75
76        // For mixed states, we need to sample from the Kraus operators
77        let mut rng = thread_rng();
78        let total_prob: f64 = kraus_ops
79            .iter()
80            .map(|k| {
81                // Compute ||K_i |ψ⟩||²
82                let result = k.dot(state);
83                result.iter().map(|c| c.norm_sqr()).sum::<f64>()
84            })
85            .sum();
86
87        // Sample which Kraus operator to apply
88        let mut cumulative = 0.0;
89        let sample: f64 = rng.gen();
90
91        for k in &kraus_ops {
92            let result = k.dot(state);
93            let prob = result.iter().map(|c| c.norm_sqr()).sum::<f64>() / total_prob;
94            cumulative += prob;
95
96            if sample < cumulative {
97                // Apply this Kraus operator and renormalize
98                let norm = result.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
99                return Ok(result.mapv(|c| c / norm));
100            }
101        }
102
103        // Fallback: apply last operator
104        let result = kraus_ops.last().unwrap().dot(state);
105        let norm = result.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
106        Ok(result.mapv(|c| c / norm))
107    }
108
109    /// Check if Kraus operators satisfy completeness relation
110    fn verify_completeness(&self) -> bool {
111        let kraus_ops = self.kraus_operators();
112        let dim = 2_usize.pow(self.num_qubits() as u32);
113
114        // Compute ∑_i K_i† K_i
115        let mut sum = Array2::<Complex64>::zeros((dim, dim));
116        for k in &kraus_ops {
117            // K_i† K_i
118            for i in 0..dim {
119                for j in 0..dim {
120                    let mut val = Complex64::new(0.0, 0.0);
121                    for m in 0..dim {
122                        val += k[[m, i]].conj() * k[[m, j]];
123                    }
124                    sum[[i, j]] += val;
125                }
126            }
127        }
128
129        // Check if sum is approximately identity
130        let mut is_identity = true;
131        for i in 0..dim {
132            for j in 0..dim {
133                let expected = if i == j {
134                    Complex64::new(1.0, 0.0)
135                } else {
136                    Complex64::new(0.0, 0.0)
137                };
138                let diff: Complex64 = sum[[i, j]] - expected;
139                if diff.norm() > 1e-10 {
140                    is_identity = false;
141                }
142            }
143        }
144        is_identity
145    }
146}
147
148/// Depolarizing noise channel
149///
150/// The depolarizing channel with probability p replaces the state with the
151/// maximally mixed state with probability p, and leaves it unchanged with
152/// probability 1-p.
153///
154/// For a single qubit: ρ → (1-p)ρ + p·I/2
155/// Kraus operators: {√(1-p)I, √(p/3)X, √(p/3)Y, √(p/3)Z}
156pub struct DepolarizingNoise {
157    /// Depolarizing probability (0 ≤ p ≤ 1)
158    pub probability: f64,
159    num_qubits: usize,
160}
161
162impl DepolarizingNoise {
163    /// Create a new single-qubit depolarizing channel
164    pub fn new(probability: f64) -> Self {
165        assert!(
166            (0.0..=1.0).contains(&probability),
167            "Probability must be between 0 and 1"
168        );
169        Self {
170            probability,
171            num_qubits: 1,
172        }
173    }
174
175    /// Create a new two-qubit depolarizing channel
176    pub fn new_two_qubit(probability: f64) -> Self {
177        assert!(
178            (0.0..=1.0).contains(&probability),
179            "Probability must be between 0 and 1"
180        );
181        Self {
182            probability,
183            num_qubits: 2,
184        }
185    }
186}
187
188impl NoiseChannel for DepolarizingNoise {
189    fn kraus_operators(&self) -> Vec<Array2<Complex64>> {
190        if self.num_qubits == 1 {
191            let p = self.probability;
192            let sqrt_1mp = (1.0 - p).sqrt();
193            let sqrt_p3 = (p / 3.0).sqrt();
194
195            vec![
196                // √(1-p) I
197                Array2::from_diag(&Array1::from_vec(vec![
198                    Complex64::new(sqrt_1mp, 0.0),
199                    Complex64::new(sqrt_1mp, 0.0),
200                ])),
201                // √(p/3) X
202                Array2::from_shape_vec(
203                    (2, 2),
204                    vec![
205                        Complex64::new(0.0, 0.0),
206                        Complex64::new(sqrt_p3, 0.0),
207                        Complex64::new(sqrt_p3, 0.0),
208                        Complex64::new(0.0, 0.0),
209                    ],
210                )
211                .unwrap(),
212                // √(p/3) Y
213                Array2::from_shape_vec(
214                    (2, 2),
215                    vec![
216                        Complex64::new(0.0, 0.0),
217                        Complex64::new(0.0, -sqrt_p3),
218                        Complex64::new(0.0, sqrt_p3),
219                        Complex64::new(0.0, 0.0),
220                    ],
221                )
222                .unwrap(),
223                // √(p/3) Z
224                Array2::from_shape_vec(
225                    (2, 2),
226                    vec![
227                        Complex64::new(sqrt_p3, 0.0),
228                        Complex64::new(0.0, 0.0),
229                        Complex64::new(0.0, 0.0),
230                        Complex64::new(-sqrt_p3, 0.0),
231                    ],
232                )
233                .unwrap(),
234            ]
235        } else {
236            // Two-qubit depolarizing (15 Pauli operators)
237            let p = self.probability;
238            let sqrt_1mp = (1.0 - p).sqrt();
239            let sqrt_p15 = (p / 15.0).sqrt();
240
241            let mut kraus_ops = Vec::new();
242
243            // Identity term
244            kraus_ops.push(Array2::from_diag(&Array1::from_vec(vec![
245                Complex64::new(
246                    sqrt_1mp, 0.0
247                );
248                4
249            ])));
250
251            // 15 two-qubit Pauli operators (excluding II)
252            // For brevity, we'll implement a subset
253            // In practice, you'd generate all 15 combinations
254            for _ in 0..15 {
255                kraus_ops.push(Array2::from_diag(&Array1::from_vec(vec![
256                    Complex64::new(
257                        sqrt_p15, 0.0
258                    );
259                    4
260                ])));
261            }
262
263            kraus_ops
264        }
265    }
266
267    fn name(&self) -> &str {
268        if self.num_qubits == 1 {
269            "DepolarizingNoise1Q"
270        } else {
271            "DepolarizingNoise2Q"
272        }
273    }
274
275    fn num_qubits(&self) -> usize {
276        self.num_qubits
277    }
278}
279
280/// Bit flip (X) error channel
281///
282/// Applies an X gate with probability p.
283/// Kraus operators: {√(1-p)I, √p X}
284pub struct BitFlipNoise {
285    pub probability: f64,
286}
287
288impl BitFlipNoise {
289    pub fn new(probability: f64) -> Self {
290        assert!(
291            (0.0..=1.0).contains(&probability),
292            "Probability must be between 0 and 1"
293        );
294        Self { probability }
295    }
296}
297
298impl NoiseChannel for BitFlipNoise {
299    fn kraus_operators(&self) -> Vec<Array2<Complex64>> {
300        let p = self.probability;
301        vec![
302            // √(1-p) I
303            Array2::from_diag(&Array1::from_vec(vec![
304                Complex64::new((1.0 - p).sqrt(), 0.0),
305                Complex64::new((1.0 - p).sqrt(), 0.0),
306            ])),
307            // √p X
308            Array2::from_shape_vec(
309                (2, 2),
310                vec![
311                    Complex64::new(0.0, 0.0),
312                    Complex64::new(p.sqrt(), 0.0),
313                    Complex64::new(p.sqrt(), 0.0),
314                    Complex64::new(0.0, 0.0),
315                ],
316            )
317            .unwrap(),
318        ]
319    }
320
321    fn name(&self) -> &str {
322        "BitFlipNoise"
323    }
324
325    fn num_qubits(&self) -> usize {
326        1
327    }
328}
329
330/// Phase flip (Z) error channel
331///
332/// Applies a Z gate with probability p.
333/// Kraus operators: {√(1-p)I, √p Z}
334pub struct PhaseFlipNoise {
335    pub probability: f64,
336}
337
338impl PhaseFlipNoise {
339    pub fn new(probability: f64) -> Self {
340        assert!(
341            (0.0..=1.0).contains(&probability),
342            "Probability must be between 0 and 1"
343        );
344        Self { probability }
345    }
346}
347
348impl NoiseChannel for PhaseFlipNoise {
349    fn kraus_operators(&self) -> Vec<Array2<Complex64>> {
350        let p = self.probability;
351        vec![
352            // √(1-p) I
353            Array2::from_diag(&Array1::from_vec(vec![
354                Complex64::new((1.0 - p).sqrt(), 0.0),
355                Complex64::new((1.0 - p).sqrt(), 0.0),
356            ])),
357            // √p Z
358            Array2::from_shape_vec(
359                (2, 2),
360                vec![
361                    Complex64::new(p.sqrt(), 0.0),
362                    Complex64::new(0.0, 0.0),
363                    Complex64::new(0.0, 0.0),
364                    Complex64::new(-p.sqrt(), 0.0),
365                ],
366            )
367            .unwrap(),
368        ]
369    }
370
371    fn name(&self) -> &str {
372        "PhaseFlipNoise"
373    }
374
375    fn num_qubits(&self) -> usize {
376        1
377    }
378}
379
380/// Amplitude damping channel
381///
382/// Models energy loss (T1 relaxation).
383/// Kraus operators: {K0 = [[1, 0], [0, √(1-γ)]], K1 = [[0, √γ], [0, 0]]}
384pub struct AmplitudeDampingNoise {
385    /// Damping parameter γ (0 ≤ γ ≤ 1)
386    pub gamma: f64,
387}
388
389impl AmplitudeDampingNoise {
390    pub fn new(gamma: f64) -> Self {
391        assert!(
392            (0.0..=1.0).contains(&gamma),
393            "Gamma must be between 0 and 1"
394        );
395        Self { gamma }
396    }
397}
398
399impl NoiseChannel for AmplitudeDampingNoise {
400    fn kraus_operators(&self) -> Vec<Array2<Complex64>> {
401        let g = self.gamma;
402        vec![
403            // K0 = [[1, 0], [0, √(1-γ)]]
404            Array2::from_shape_vec(
405                (2, 2),
406                vec![
407                    Complex64::new(1.0, 0.0),
408                    Complex64::new(0.0, 0.0),
409                    Complex64::new(0.0, 0.0),
410                    Complex64::new((1.0 - g).sqrt(), 0.0),
411                ],
412            )
413            .unwrap(),
414            // K1 = [[0, √γ], [0, 0]]
415            Array2::from_shape_vec(
416                (2, 2),
417                vec![
418                    Complex64::new(0.0, 0.0),
419                    Complex64::new(g.sqrt(), 0.0),
420                    Complex64::new(0.0, 0.0),
421                    Complex64::new(0.0, 0.0),
422                ],
423            )
424            .unwrap(),
425        ]
426    }
427
428    fn name(&self) -> &str {
429        "AmplitudeDampingNoise"
430    }
431
432    fn num_qubits(&self) -> usize {
433        1
434    }
435}
436
437/// Phase damping channel
438///
439/// Models dephasing without energy loss (T2 relaxation).
440/// Kraus operators: {√(1-λ)I, √λ Z-projection}
441pub struct PhaseDampingNoise {
442    /// Damping parameter λ (0 ≤ λ ≤ 1)
443    pub lambda: f64,
444}
445
446impl PhaseDampingNoise {
447    pub fn new(lambda: f64) -> Self {
448        assert!(
449            (0.0..=1.0).contains(&lambda),
450            "Lambda must be between 0 and 1"
451        );
452        Self { lambda }
453    }
454}
455
456impl NoiseChannel for PhaseDampingNoise {
457    fn kraus_operators(&self) -> Vec<Array2<Complex64>> {
458        let l = self.lambda;
459        vec![
460            // K0 = [[1, 0], [0, √(1-λ)]]
461            Array2::from_shape_vec(
462                (2, 2),
463                vec![
464                    Complex64::new(1.0, 0.0),
465                    Complex64::new(0.0, 0.0),
466                    Complex64::new(0.0, 0.0),
467                    Complex64::new((1.0 - l).sqrt(), 0.0),
468                ],
469            )
470            .unwrap(),
471            // K1 = [[0, 0], [0, √λ]]
472            Array2::from_shape_vec(
473                (2, 2),
474                vec![
475                    Complex64::new(0.0, 0.0),
476                    Complex64::new(0.0, 0.0),
477                    Complex64::new(0.0, 0.0),
478                    Complex64::new(l.sqrt(), 0.0),
479                ],
480            )
481            .unwrap(),
482        ]
483    }
484
485    fn name(&self) -> &str {
486        "PhaseDampingNoise"
487    }
488
489    fn num_qubits(&self) -> usize {
490        1
491    }
492}
493
494/// Thermal relaxation channel
495///
496/// Combines T1 and T2 relaxation processes.
497/// Models realistic qubit decoherence.
498pub struct ThermalRelaxationNoise {
499    /// T1 relaxation time (energy relaxation)
500    pub t1: f64,
501    /// T2 relaxation time (dephasing)
502    pub t2: f64,
503    /// Gate time
504    pub gate_time: f64,
505    /// Excited state population (thermal)
506    pub excited_state_pop: f64,
507}
508
509impl ThermalRelaxationNoise {
510    pub fn new(t1: f64, t2: f64, gate_time: f64) -> Self {
511        assert!(t1 > 0.0, "T1 must be positive");
512        assert!(t2 > 0.0, "T2 must be positive");
513        assert!(t2 <= 2.0 * t1, "T2 must satisfy T2 ≤ 2T1");
514        assert!(gate_time >= 0.0, "Gate time must be non-negative");
515
516        Self {
517            t1,
518            t2,
519            gate_time,
520            excited_state_pop: 0.0,
521        }
522    }
523
524    pub fn with_thermal_population(mut self, excited_state_pop: f64) -> Self {
525        assert!(
526            (0.0..=1.0).contains(&excited_state_pop),
527            "Excited state population must be between 0 and 1"
528        );
529        self.excited_state_pop = excited_state_pop;
530        self
531    }
532}
533
534impl NoiseChannel for ThermalRelaxationNoise {
535    fn kraus_operators(&self) -> Vec<Array2<Complex64>> {
536        let t = self.gate_time;
537        let t1 = self.t1;
538        let t2 = self.t2;
539        let p_reset = 1.0 - (-t / t1).exp();
540        let p_z = (1.0 - (-t / t2).exp()) - p_reset / 2.0;
541
542        // Combine amplitude damping and pure dephasing
543        let p_excited = self.excited_state_pop;
544
545        vec![
546            // K0: Identity-like (no relaxation)
547            Array2::from_shape_vec(
548                (2, 2),
549                vec![
550                    Complex64::new((1.0 - p_reset - p_z).sqrt(), 0.0),
551                    Complex64::new(0.0, 0.0),
552                    Complex64::new(0.0, 0.0),
553                    Complex64::new((1.0 - p_reset - p_z).sqrt(), 0.0),
554                ],
555            )
556            .unwrap(),
557            // K1: Amplitude damping to ground
558            Array2::from_shape_vec(
559                (2, 2),
560                vec![
561                    Complex64::new(0.0, 0.0),
562                    Complex64::new((p_reset * (1.0 - p_excited)).sqrt(), 0.0),
563                    Complex64::new(0.0, 0.0),
564                    Complex64::new(0.0, 0.0),
565                ],
566            )
567            .unwrap(),
568            // K2: Pure dephasing
569            Array2::from_shape_vec(
570                (2, 2),
571                vec![
572                    Complex64::new(p_z.sqrt(), 0.0),
573                    Complex64::new(0.0, 0.0),
574                    Complex64::new(0.0, 0.0),
575                    Complex64::new(-p_z.sqrt(), 0.0),
576                ],
577            )
578            .unwrap(),
579        ]
580    }
581
582    fn name(&self) -> &str {
583        "ThermalRelaxationNoise"
584    }
585
586    fn num_qubits(&self) -> usize {
587        1
588    }
589}
590
591/// Composite noise model
592///
593/// Manages multiple noise channels and applies them to quantum circuits.
594#[derive(Clone)]
595pub struct NoiseModel {
596    /// Global noise channels applied to all gates
597    global_channels: Vec<Arc<dyn NoiseChannel>>,
598    /// Gate-specific noise channels
599    gate_channels: HashMap<String, Vec<Arc<dyn NoiseChannel>>>,
600    /// Measurement noise (readout error)
601    measurement_noise: Option<Arc<dyn NoiseChannel>>,
602    /// Idle noise per time unit
603    idle_noise: Option<Arc<dyn NoiseChannel>>,
604}
605
606impl NoiseModel {
607    /// Create a new empty noise model
608    pub fn new() -> Self {
609        Self {
610            global_channels: Vec::new(),
611            gate_channels: HashMap::new(),
612            measurement_noise: None,
613            idle_noise: None,
614        }
615    }
616
617    /// Add a global noise channel applied to all gates
618    pub fn add_channel(&mut self, channel: Arc<dyn NoiseChannel>) {
619        self.global_channels.push(channel);
620    }
621
622    /// Add a gate-specific noise channel
623    pub fn add_gate_noise(&mut self, gate_name: &str, channel: Arc<dyn NoiseChannel>) {
624        self.gate_channels
625            .entry(gate_name.to_string())
626            .or_default()
627            .push(channel);
628    }
629
630    /// Set measurement noise
631    pub fn set_measurement_noise(&mut self, channel: Arc<dyn NoiseChannel>) {
632        self.measurement_noise = Some(channel);
633    }
634
635    /// Set idle noise
636    pub fn set_idle_noise(&mut self, channel: Arc<dyn NoiseChannel>) {
637        self.idle_noise = Some(channel);
638    }
639
640    /// Apply noise to a single-qubit state
641    pub fn apply_single_qubit(
642        &self,
643        state: &Array1<Complex64>,
644        _qubit: usize,
645    ) -> NoiseResult<Array1<Complex64>> {
646        let mut noisy_state = state.clone();
647
648        // Apply all global single-qubit channels
649        for channel in &self.global_channels {
650            if channel.num_qubits() == 1 {
651                noisy_state = channel.apply(&noisy_state.view())?;
652            }
653        }
654
655        Ok(noisy_state)
656    }
657
658    /// Apply gate-specific noise
659    pub fn apply_gate_noise(
660        &self,
661        state: &Array1<Complex64>,
662        gate_name: &str,
663        _qubit: usize,
664    ) -> NoiseResult<Array1<Complex64>> {
665        let mut noisy_state = state.clone();
666
667        if let Some(channels) = self.gate_channels.get(gate_name) {
668            for channel in channels {
669                noisy_state = channel.apply(&noisy_state.view())?;
670            }
671        }
672
673        Ok(noisy_state)
674    }
675
676    /// Get the number of global noise channels
677    pub fn num_global_channels(&self) -> usize {
678        self.global_channels.len()
679    }
680
681    /// Check if measurement noise is set
682    pub fn has_measurement_noise(&self) -> bool {
683        self.measurement_noise.is_some()
684    }
685}
686
687impl Default for NoiseModel {
688    fn default() -> Self {
689        Self::new()
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696
697    #[test]
698    fn test_depolarizing_noise_kraus() {
699        let noise = DepolarizingNoise::new(0.1);
700        let kraus = noise.kraus_operators();
701
702        // Should have 4 Kraus operators for single qubit
703        assert_eq!(kraus.len(), 4);
704
705        // Verify completeness
706        assert!(noise.verify_completeness());
707    }
708
709    #[test]
710    fn test_bit_flip_noise() {
711        let noise = BitFlipNoise::new(0.2);
712        let kraus = noise.kraus_operators();
713
714        assert_eq!(kraus.len(), 2);
715        assert!(noise.verify_completeness());
716    }
717
718    #[test]
719    fn test_phase_flip_noise() {
720        let noise = PhaseFlipNoise::new(0.15);
721        let kraus = noise.kraus_operators();
722
723        assert_eq!(kraus.len(), 2);
724        assert!(noise.verify_completeness());
725    }
726
727    #[test]
728    fn test_amplitude_damping() {
729        let noise = AmplitudeDampingNoise::new(0.05);
730        let kraus = noise.kraus_operators();
731
732        assert_eq!(kraus.len(), 2);
733        assert!(noise.verify_completeness());
734    }
735
736    #[test]
737    fn test_phase_damping() {
738        let noise = PhaseDampingNoise::new(0.1);
739        let kraus = noise.kraus_operators();
740
741        assert_eq!(kraus.len(), 2);
742        assert!(noise.verify_completeness());
743    }
744
745    #[test]
746    fn test_thermal_relaxation() {
747        let noise = ThermalRelaxationNoise::new(50.0, 40.0, 1.0);
748        let kraus = noise.kraus_operators();
749
750        assert_eq!(kraus.len(), 3);
751        // Note: Thermal relaxation may not satisfy exact completeness
752        // due to approximations
753    }
754
755    #[test]
756    fn test_noise_application() {
757        let noise = DepolarizingNoise::new(0.01);
758
759        // |0⟩ state
760        let state = Array1::from_vec(vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]);
761
762        // Apply noise multiple times and check that result is still normalized
763        for _ in 0..10 {
764            let noisy_state = noise.apply(&state.view()).unwrap();
765            let norm: f64 = noisy_state.iter().map(|c| c.norm_sqr()).sum();
766            assert!((norm - 1.0).abs() < 1e-10, "State not normalized: {}", norm);
767        }
768    }
769
770    #[test]
771    fn test_noise_model() {
772        let mut model = NoiseModel::new();
773
774        // Add depolarizing noise
775        model.add_channel(Arc::new(DepolarizingNoise::new(0.01)));
776
777        // Add bit flip noise to X gates
778        model.add_gate_noise("X", Arc::new(BitFlipNoise::new(0.02)));
779
780        assert_eq!(model.num_global_channels(), 1);
781        assert!(!model.has_measurement_noise());
782
783        // Apply noise to a state
784        let state = Array1::from_vec(vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]);
785        let noisy = model.apply_single_qubit(&state, 0).unwrap();
786
787        let norm: f64 = noisy.iter().map(|c| c.norm_sqr()).sum();
788        assert!((norm - 1.0).abs() < 1e-10);
789    }
790
791    #[test]
792    fn test_noise_model_composition() {
793        let mut model = NoiseModel::new();
794
795        // Combine multiple noise sources
796        model.add_channel(Arc::new(DepolarizingNoise::new(0.005)));
797        model.add_channel(Arc::new(AmplitudeDampingNoise::new(0.01)));
798        model.add_channel(Arc::new(PhaseDampingNoise::new(0.008)));
799
800        assert_eq!(model.num_global_channels(), 3);
801
802        // Apply composite noise
803        let state = Array1::from_vec(vec![
804            Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0),
805            Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0),
806        ]);
807        let noisy = model.apply_single_qubit(&state, 0).unwrap();
808
809        let norm: f64 = noisy.iter().map(|c| c.norm_sqr()).sum();
810        assert!((norm - 1.0).abs() < 1e-10);
811    }
812}