Skip to main content

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