logosq/noise/
mod.rs

1// This file defines the noise module for simulating noise in quantum systems.
2// It exports types and functions for adding noise to quantum operations.
3
4use crate::states::State;
5use ndarray::Array1;
6use num_complex::Complex64;
7use rand::Rng;
8
9/// Trait for quantum noise models
10pub trait NoiseModel {
11    /// Apply noise to a quantum state
12    fn apply(&self, state: &mut State);
13
14    /// Return a descriptive name for this noise model
15    fn name(&self) -> String;
16}
17
18/// Represents the common parameters for noise models
19pub struct NoiseParams {
20    /// The probability of an error occurring (0.0 to 1.0)
21    pub error_probability: f64,
22
23    /// Which qubits this noise applies to (None means all qubits)
24    pub target_qubits: Option<Vec<usize>>,
25}
26
27impl NoiseParams {
28    pub fn new(error_probability: f64) -> Self {
29        Self {
30            error_probability,
31            target_qubits: None,
32        }
33    }
34
35    pub fn with_target_qubits(mut self, qubits: Vec<usize>) -> Self {
36        self.target_qubits = Some(qubits);
37        self
38    }
39}
40
41/// Depolarizing noise model - replaces the state with a completely mixed state with probability p
42pub struct DepolarizingNoise {
43    params: NoiseParams,
44}
45
46impl DepolarizingNoise {
47    pub fn new(error_probability: f64) -> Self {
48        Self {
49            params: NoiseParams::new(error_probability),
50        }
51    }
52
53    pub fn with_target_qubits(self, qubits: Vec<usize>) -> Self {
54        Self {
55            params: self.params.with_target_qubits(qubits),
56        }
57    }
58}
59
60impl NoiseModel for DepolarizingNoise {
61    fn apply(&self, state: &mut State) {
62        let mut rng = rand::thread_rng();
63
64        // Apply noise to specified qubits or all qubits
65        let target_qubits = match &self.params.target_qubits {
66            Some(qubits) => qubits.clone(),
67            None => (0..state.num_qubits()).collect(),
68        };
69
70        for &qubit in &target_qubits {
71            if rng.gen::<f64>() < self.params.error_probability {
72                // Apply a random Pauli error (X, Y, or Z)
73                let error_type = rng.gen_range(0..3);
74                match error_type {
75                    0 => apply_x_error(state, qubit), // X error
76                    1 => apply_y_error(state, qubit), // Y error
77                    2 => apply_z_error(state, qubit), // Z error
78                    _ => unreachable!(),
79                }
80            }
81        }
82    }
83
84    fn name(&self) -> String {
85        format!("Depolarizing(p={})", self.params.error_probability)
86    }
87}
88
89/// Amplitude damping noise - models energy dissipation (e.g., |1⟩ → |0⟩ transitions)
90pub struct AmplitudeDampingNoise {
91    params: NoiseParams,
92}
93
94impl AmplitudeDampingNoise {
95    pub fn new(error_probability: f64) -> Self {
96        Self {
97            params: NoiseParams::new(error_probability),
98        }
99    }
100
101    pub fn with_target_qubits(self, qubits: Vec<usize>) -> Self {
102        Self {
103            params: self.params.with_target_qubits(qubits),
104        }
105    }
106}
107
108impl NoiseModel for AmplitudeDampingNoise {
109    fn apply(&self, state: &mut State) {
110        let dimension = state.vector().len();
111
112        // Apply noise to specified qubits or all qubits
113        let target_qubits = match &self.params.target_qubits {
114            Some(qubits) => qubits.clone(),
115            None => (0..state.num_qubits()).collect(),
116        };
117
118        // Create a new vector to store the updated state
119        let mut new_vector = Array1::<Complex64>::zeros(dimension);
120        let state_vec = state.vector();
121
122        for &qubit in &target_qubits {
123            let gamma = self.params.error_probability;
124            let sqrt_gamma = gamma.sqrt();
125
126            // Apply the Kraus operators for amplitude damping
127            // K0 = |0⟩⟨0| + sqrt(1-γ)|1⟩⟨1|
128            // K1 = sqrt(γ)|0⟩⟨1|
129
130            for i in 0..dimension {
131                if (i & (1 << qubit)) == 0 {
132                    // If qubit is |0⟩, apply K0 part |0⟩⟨0|
133                    new_vector[i] += state_vec[i];
134                } else {
135                    // If qubit is |1⟩, apply K0 part sqrt(1-γ)|1⟩⟨1|
136                    new_vector[i] += state_vec[i] * Complex64::new((1.0 - gamma).sqrt(), 0.0);
137
138                    // Apply K1 = sqrt(γ)|0⟩⟨1|
139                    let j = i & !(1 << qubit); // Flip qubit from |1⟩ to |0⟩
140                    new_vector[j] += state_vec[i] * Complex64::new(sqrt_gamma, 0.0);
141                }
142            }
143        }
144
145        *state.vector_mut() = new_vector;
146        state.normalize();
147    }
148
149    fn name(&self) -> String {
150        format!("AmplitudeDamping(p={})", self.params.error_probability)
151    }
152}
153
154/// Phase damping noise - causes phase decoherence without energy dissipation
155pub struct PhaseDampingNoise {
156    params: NoiseParams,
157}
158
159impl PhaseDampingNoise {
160    pub fn new(error_probability: f64) -> Self {
161        Self {
162            params: NoiseParams::new(error_probability),
163        }
164    }
165
166    pub fn with_target_qubits(self, qubits: Vec<usize>) -> Self {
167        Self {
168            params: self.params.with_target_qubits(qubits),
169        }
170    }
171}
172
173impl NoiseModel for PhaseDampingNoise {
174    fn apply(&self, state: &mut State) {
175        let dimension = state.vector().len();
176
177        // Apply noise to specified qubits or all qubits
178        let target_qubits = match &self.params.target_qubits {
179            Some(qubits) => qubits.clone(),
180            None => (0..state.num_qubits()).collect(),
181        };
182
183        // Create a new vector to store the updated state
184        let mut new_vector = ndarray::Array1::zeros(dimension);
185        let state_vec = state.vector();
186
187        for &qubit in &target_qubits {
188            // Phase damping Kraus operators:
189            // K0 = |0⟩⟨0| + sqrt(1-λ)|1⟩⟨1|
190            // K1 = sqrt(λ)|1⟩⟨1|
191
192            let lambda = self.params.error_probability;
193            let sqrt_1_minus_lambda = (1.0 - lambda).sqrt();
194
195            for i in 0..dimension {
196                // Copy original state
197                new_vector[i] = state_vec[i];
198
199                // If qubit is in state |1⟩, apply the phase damping
200                if (i & (1 << qubit)) != 0 {
201                    // The amplitude is reduced by sqrt(1-λ) but phase is preserved
202                    new_vector[i] *= Complex64::new(sqrt_1_minus_lambda, 0.0);
203                }
204            }
205        }
206
207        // Update the state vector and normalize
208        *state.vector_mut() = new_vector;
209        state.normalize();
210    }
211
212    fn name(&self) -> String {
213        format!("PhaseDamping(p={})", self.params.error_probability)
214    }
215}
216
217/// Thermal relaxation noise model - combines amplitude and phase damping
218pub struct ThermalRelaxationNoise {
219    /// T1 relaxation time (amplitude damping)
220    t1: f64,
221
222    /// T2 relaxation time (phase damping)
223    t2: f64,
224
225    /// Gate time (how long the gate takes to execute)
226    gate_time: f64,
227
228    /// Target qubits
229    target_qubits: Option<Vec<usize>>,
230}
231
232impl ThermalRelaxationNoise {
233    pub fn new(t1: f64, t2: f64, gate_time: f64) -> Self {
234        // Ensure t2 <= 2*t1 (physical constraint)
235        let t2 = t2.min(2.0 * t1);
236
237        Self {
238            t1,
239            t2,
240            gate_time,
241            target_qubits: None,
242        }
243    }
244
245    pub fn with_target_qubits(mut self, qubits: Vec<usize>) -> Self {
246        self.target_qubits = Some(qubits);
247        self
248    }
249}
250
251impl NoiseModel for ThermalRelaxationNoise {
252    fn apply(&self, state: &mut State) {
253        // Calculate probabilities from relaxation times
254        // For amplitude damping, p_a = 1 - exp(-gate_time/T1)
255        let p_amplitude = 1.0 - (-self.gate_time / self.t1).exp();
256
257        // For pure dephasing (not including relaxation-induced dephasing)
258        // Pure dephasing rate is calculated as 1/T_phi = 1/T2 - 1/(2*T1)
259        let pure_dephasing_rate = if self.t2 <= 2.0 * self.t1 {
260            1.0 / self.t2 - 1.0 / (2.0 * self.t1)
261        } else {
262            // If T2 > 2*T1, which is physically impossible, default to 0
263            0.0
264        };
265
266        // Phase damping probability p_p = 1 - exp(-gate_time/T_phi)
267        let p_phase = if pure_dephasing_rate > 0.0 {
268            1.0 - (-self.gate_time * pure_dephasing_rate).exp()
269        } else {
270            0.0
271        };
272
273        // Target qubits for noise application
274        let target_qubits = self
275            .target_qubits
276            .clone()
277            .unwrap_or_else(|| (0..state.num_qubits()).collect());
278
279        // Apply amplitude damping first
280        if p_amplitude > 0.0 {
281            let amplitude_noise =
282                AmplitudeDampingNoise::new(p_amplitude).with_target_qubits(target_qubits.clone());
283            amplitude_noise.apply(state);
284        }
285
286        // Then apply phase damping
287        if p_phase > 0.0 {
288            let phase_noise = PhaseDampingNoise::new(p_phase).with_target_qubits(target_qubits);
289            phase_noise.apply(state);
290        }
291    }
292
293    fn name(&self) -> String {
294        format!(
295            "ThermalRelaxation(T1={}, T2={}, gate_time={})",
296            self.t1, self.t2, self.gate_time
297        )
298    }
299}
300
301/// Composite noise model - combines multiple noise models
302pub struct CompositeNoise {
303    noise_models: Vec<Box<dyn NoiseModel>>,
304}
305
306impl CompositeNoise {
307    pub fn new() -> Self {
308        Self {
309            noise_models: Vec::new(),
310        }
311    }
312
313    pub fn add_noise<N: NoiseModel + 'static>(&mut self, noise_model: N) -> &mut Self {
314        self.noise_models.push(Box::new(noise_model));
315        self
316    }
317}
318
319impl NoiseModel for CompositeNoise {
320    fn apply(&self, state: &mut State) {
321        // Apply each noise model in sequence
322        for noise_model in &self.noise_models {
323            // Create a copy of the state to avoid borrow issues
324            let mut temp_state = state.clone();
325            noise_model.apply(&mut temp_state);
326            *state = temp_state;
327        }
328    }
329
330    fn name(&self) -> String {
331        let names: Vec<String> = self.noise_models.iter().map(|model| model.name()).collect();
332        format!("Composite({})", names.join(", "))
333    }
334}
335
336// Helper functions to apply specific Pauli errors
337fn apply_x_error(state: &mut State, qubit: usize) {
338    let dimension = state.vector().len();
339    let state_vec = state.vector();
340    let mut new_vector = state_vec.clone();
341
342    for i in 0..dimension {
343        let j = i ^ (1 << qubit); // Flip the qubit bit
344        new_vector[i] = state_vec[j];
345    }
346
347    *state.vector_mut() = new_vector;
348}
349
350fn apply_y_error(state: &mut State, qubit: usize) {
351    let dimension = state.vector().len();
352    let state_vec = state.vector();
353    let mut new_vector = state_vec.clone();
354    let imag_i = Complex64::new(0.0, 1.0);
355
356    for i in 0..dimension {
357        let j = i ^ (1 << qubit); // Flip the qubit bit
358
359        // Check if the qubit is 1 in state i
360        if (i & (1 << qubit)) != 0 {
361            new_vector[i] = -imag_i * state_vec[j];
362        } else {
363            new_vector[i] = imag_i * state_vec[j];
364        }
365    }
366
367    *state.vector_mut() = new_vector;
368}
369
370fn apply_z_error(state: &mut State, qubit: usize) {
371    let dimension = state.vector().len();
372    let vector_slice = state.vector_mut().as_slice_mut().unwrap();
373
374    for i in 0..dimension {
375        // Apply phase flip if qubit is in state |1⟩
376        if (i & (1 << qubit)) != 0 {
377            vector_slice[i] = -vector_slice[i];
378        }
379    }
380}