quantrs2_device/
noise_model.rs

1//! Device-specific noise models based on calibration data
2//!
3//! This module creates realistic noise models from device calibration data,
4//! enabling accurate simulation of quantum hardware behavior.
5
6use scirs2_core::random::Rng;
7use scirs2_core::Complex64;
8use std::collections::HashMap;
9
10use quantrs2_core::{
11    error::{QuantRS2Error, QuantRS2Result},
12    gate::GateOp,
13    qubit::QubitId,
14};
15
16use crate::calibration::{
17    DeviceCalibration, QubitCalibration, QubitReadoutData, SingleQubitGateData,
18    TwoQubitGateCalibration,
19};
20use scirs2_core::random::prelude::*;
21
22/// Noise model derived from device calibration
23#[derive(Debug, Clone)]
24pub struct CalibrationNoiseModel {
25    /// Device identifier
26    pub device_id: String,
27    /// Qubit noise parameters
28    pub qubit_noise: HashMap<QubitId, QubitNoiseParams>,
29    /// Gate noise parameters
30    pub gate_noise: HashMap<String, GateNoiseParams>,
31    /// Two-qubit gate noise
32    pub two_qubit_noise: HashMap<(QubitId, QubitId), TwoQubitNoiseParams>,
33    /// Readout noise parameters
34    pub readout_noise: HashMap<QubitId, ReadoutNoiseParams>,
35    /// Crosstalk noise
36    pub crosstalk: CrosstalkNoise,
37    /// Temperature (mK)
38    pub temperature: f64,
39}
40
41/// Noise parameters for individual qubits
42#[derive(Debug, Clone)]
43pub struct QubitNoiseParams {
44    /// T1 decay rate (1/μs)
45    pub gamma_1: f64,
46    /// T2 dephasing rate (1/μs)
47    pub gamma_phi: f64,
48    /// Thermal excitation probability
49    pub thermal_population: f64,
50    /// Frequency drift (Hz)
51    pub frequency_drift: f64,
52    /// Amplitude of 1/f noise
53    pub flicker_noise: f64,
54}
55
56/// Gate-specific noise parameters
57#[derive(Debug, Clone)]
58pub struct GateNoiseParams {
59    /// Coherent error (over/under rotation)
60    pub coherent_error: f64,
61    /// Incoherent error rate
62    pub incoherent_error: f64,
63    /// Gate duration (ns)
64    pub duration: f64,
65    /// Depolarizing rate during gate
66    pub depolarizing_rate: f64,
67    /// Amplitude noise
68    pub amplitude_noise: f64,
69    /// Phase noise
70    pub phase_noise: f64,
71}
72
73/// Two-qubit gate noise parameters
74#[derive(Debug, Clone)]
75pub struct TwoQubitNoiseParams {
76    /// Base gate noise
77    pub gate_noise: GateNoiseParams,
78    /// ZZ coupling strength (MHz)
79    pub zz_coupling: f64,
80    /// Crosstalk to spectator qubits
81    pub spectator_crosstalk: HashMap<QubitId, f64>,
82    /// Directional bias
83    pub directional_error: f64,
84}
85
86/// Readout noise parameters
87#[derive(Debug, Clone)]
88pub struct ReadoutNoiseParams {
89    /// Assignment error matrix [[p(0|0), p(0|1)], [p(1|0), p(1|1)]]
90    pub assignment_matrix: [[f64; 2]; 2],
91    /// Readout-induced excitation
92    pub readout_excitation: f64,
93    /// Readout-induced relaxation
94    pub readout_relaxation: f64,
95}
96
97/// Crosstalk noise model
98#[derive(Debug, Clone)]
99pub struct CrosstalkNoise {
100    /// Crosstalk matrix
101    pub crosstalk_matrix: Vec<Vec<f64>>,
102    /// Threshold for significant crosstalk
103    pub threshold: f64,
104    /// Crosstalk during single-qubit gates
105    pub single_qubit_crosstalk: f64,
106    /// Crosstalk during two-qubit gates
107    pub two_qubit_crosstalk: f64,
108}
109
110impl CalibrationNoiseModel {
111    /// Create noise model from device calibration
112    pub fn from_calibration(calibration: &DeviceCalibration) -> Self {
113        let mut qubit_noise = HashMap::new();
114        let mut readout_noise = HashMap::new();
115
116        // Extract qubit noise parameters
117        for (qubit_id, qubit_cal) in &calibration.qubit_calibrations {
118            qubit_noise.insert(
119                *qubit_id,
120                QubitNoiseParams {
121                    gamma_1: 1.0 / qubit_cal.t1,
122                    gamma_phi: 1.0 / qubit_cal.t2 - 0.5 / qubit_cal.t1,
123                    thermal_population: qubit_cal.thermal_population,
124                    frequency_drift: 1e3, // 1 kHz drift (typical)
125                    flicker_noise: 1e-6,  // Typical 1/f noise amplitude
126                },
127            );
128        }
129
130        // Extract readout noise
131        for (qubit_id, readout_data) in &calibration.readout_calibration.qubit_readout {
132            let p00 = readout_data.p0_given_0;
133            let p11 = readout_data.p1_given_1;
134
135            readout_noise.insert(
136                *qubit_id,
137                ReadoutNoiseParams {
138                    assignment_matrix: [[p00, 1.0 - p11], [1.0 - p00, p11]],
139                    readout_excitation: 0.001, // Typical value
140                    readout_relaxation: 0.002, // Typical value
141                },
142            );
143        }
144
145        // Extract gate noise
146        let mut gate_noise = HashMap::new();
147        for (gate_name, gate_cal) in &calibration.single_qubit_gates {
148            // Average over all qubits
149            let mut total_error = 0.0;
150            let mut total_duration = 0.0;
151            let mut count = 0;
152
153            for gate_data in gate_cal.qubit_data.values() {
154                total_error += gate_data.error_rate;
155                total_duration += gate_data.duration;
156                count += 1;
157            }
158
159            if count > 0 {
160                let avg_error = total_error / count as f64;
161                let avg_duration = total_duration / count as f64;
162
163                gate_noise.insert(
164                    gate_name.clone(),
165                    GateNoiseParams {
166                        coherent_error: avg_error * 0.3,   // 30% coherent
167                        incoherent_error: avg_error * 0.7, // 70% incoherent
168                        duration: avg_duration,
169                        depolarizing_rate: avg_error / avg_duration,
170                        amplitude_noise: 0.001, // Typical
171                        phase_noise: 0.002,     // Typical
172                    },
173                );
174            }
175        }
176
177        // Extract two-qubit gate noise
178        let mut two_qubit_noise = HashMap::new();
179        for ((control, target), gate_cal) in &calibration.two_qubit_gates {
180            let base_noise = GateNoiseParams {
181                coherent_error: gate_cal.error_rate * 0.4,
182                incoherent_error: gate_cal.error_rate * 0.6,
183                duration: gate_cal.duration,
184                depolarizing_rate: gate_cal.error_rate / gate_cal.duration,
185                amplitude_noise: 0.002,
186                phase_noise: 0.003,
187            };
188
189            // Add to gate_noise map as well
190            gate_noise.insert(gate_cal.gate_name.clone(), base_noise.clone());
191
192            // Estimate spectator crosstalk
193            let mut spectator_crosstalk = HashMap::new();
194
195            // Add neighboring qubits
196            for offset in [-1, 1] {
197                let spectator_id = control.id() as i32 + offset;
198                if spectator_id >= 0 && spectator_id < calibration.topology.num_qubits as i32 {
199                    spectator_crosstalk.insert(QubitId(spectator_id as u32), 0.001);
200                }
201
202                let spectator_id = target.id() as i32 + offset;
203                if spectator_id >= 0 && spectator_id < calibration.topology.num_qubits as i32 {
204                    spectator_crosstalk.insert(QubitId(spectator_id as u32), 0.001);
205                }
206            }
207
208            two_qubit_noise.insert(
209                (*control, *target),
210                TwoQubitNoiseParams {
211                    gate_noise: base_noise,
212                    zz_coupling: 0.1, // Typical residual coupling
213                    spectator_crosstalk,
214                    directional_error: if gate_cal.directional { 0.01 } else { 0.0 },
215                },
216            );
217        }
218
219        // Extract crosstalk
220        let crosstalk = CrosstalkNoise {
221            crosstalk_matrix: calibration.crosstalk_matrix.matrix.clone(),
222            threshold: calibration.crosstalk_matrix.significance_threshold,
223            single_qubit_crosstalk: 0.001,
224            two_qubit_crosstalk: 0.01,
225        };
226
227        // Get temperature
228        let temperature = calibration
229            .qubit_calibrations
230            .values()
231            .filter_map(|q| q.temperature)
232            .sum::<f64>()
233            / calibration.qubit_calibrations.len() as f64;
234
235        Self {
236            device_id: calibration.device_id.clone(),
237            qubit_noise,
238            gate_noise,
239            two_qubit_noise,
240            readout_noise,
241            crosstalk,
242            temperature,
243        }
244    }
245
246    /// Apply noise to a quantum state after a gate operation
247    pub fn apply_gate_noise(
248        &self,
249        state: &mut Vec<Complex64>,
250        gate: &dyn GateOp,
251        qubits: &[QubitId],
252        duration_ns: f64,
253        rng: &mut impl Rng,
254    ) -> QuantRS2Result<()> {
255        let gate_name = gate.name();
256
257        match qubits.len() {
258            1 => self.apply_single_qubit_noise(state, gate_name, qubits[0], duration_ns, rng),
259            2 => {
260                self.apply_two_qubit_noise(state, gate_name, qubits[0], qubits[1], duration_ns, rng)
261            }
262            _ => {
263                // For multi-qubit gates, apply as decomposed single/two-qubit noise
264                for &qubit in qubits {
265                    self.apply_single_qubit_noise(
266                        state,
267                        gate_name,
268                        qubit,
269                        duration_ns / qubits.len() as f64,
270                        rng,
271                    )?;
272                }
273                Ok(())
274            }
275        }
276    }
277
278    /// Apply single-qubit gate noise
279    fn apply_single_qubit_noise(
280        &self,
281        state: &mut Vec<Complex64>,
282        gate_name: &str,
283        qubit: QubitId,
284        duration_ns: f64,
285        rng: &mut impl Rng,
286    ) -> QuantRS2Result<()> {
287        // Get gate noise parameters
288        let gate_params = self.gate_noise.get(gate_name);
289        let qubit_params = self.qubit_noise.get(&qubit);
290
291        if let Some(params) = gate_params {
292            // Apply coherent error (over/under rotation)
293            if params.coherent_error > 0.0 {
294                let error_angle = rng.gen_range(-params.coherent_error..params.coherent_error);
295                self.apply_rotation_error(state, qubit, error_angle)?;
296            }
297
298            // Apply amplitude noise
299            if params.amplitude_noise > 0.0 {
300                let amplitude_error =
301                    1.0 + rng.gen_range(-params.amplitude_noise..params.amplitude_noise);
302                self.apply_amplitude_scaling(state, qubit, amplitude_error)?;
303            }
304
305            // Apply phase noise
306            if params.phase_noise > 0.0 {
307                let phase_error = rng.gen_range(-params.phase_noise..params.phase_noise);
308                self.apply_phase_error(state, qubit, phase_error)?;
309            }
310        }
311
312        // Apply decoherence during gate
313        if let Some(qubit_params) = qubit_params {
314            let actual_duration = duration_ns / 1000.0; // Convert to μs
315
316            // T1 decay
317            let decay_prob = 1.0 - (-actual_duration * qubit_params.gamma_1).exp();
318            if rng.gen::<f64>() < decay_prob {
319                self.apply_amplitude_damping(state, qubit, decay_prob)?;
320            }
321
322            // T2 dephasing
323            let dephase_prob = 1.0 - (-actual_duration * qubit_params.gamma_phi).exp();
324            if rng.gen::<f64>() < dephase_prob {
325                self.apply_phase_damping(state, qubit, dephase_prob)?;
326            }
327        }
328
329        Ok(())
330    }
331
332    /// Apply two-qubit gate noise
333    fn apply_two_qubit_noise(
334        &self,
335        state: &mut Vec<Complex64>,
336        gate_name: &str,
337        control: QubitId,
338        target: QubitId,
339        duration_ns: f64,
340        rng: &mut impl Rng,
341    ) -> QuantRS2Result<()> {
342        if let Some(params) = self.two_qubit_noise.get(&(control, target)) {
343            // Apply base gate noise
344            let gate_params = &params.gate_noise;
345
346            // Coherent errors
347            if gate_params.coherent_error > 0.0 {
348                let error = rng.gen_range(-gate_params.coherent_error..gate_params.coherent_error);
349                self.apply_two_qubit_rotation_error(state, control, target, error)?;
350            }
351
352            // ZZ coupling during idle
353            if params.zz_coupling > 0.0 {
354                let zz_angle = params.zz_coupling * duration_ns / 1000.0; // MHz * μs = radians
355                self.apply_zz_interaction(state, control, target, zz_angle)?;
356            }
357
358            // Spectator crosstalk
359            for (&spectator, &coupling) in &params.spectator_crosstalk {
360                if rng.gen::<f64>() < coupling {
361                    self.apply_crosstalk_error(state, spectator, coupling * 0.1)?;
362                }
363            }
364
365            // Apply decoherence to both qubits
366            self.apply_single_qubit_noise(state, gate_name, control, duration_ns / 2.0, rng)?;
367            self.apply_single_qubit_noise(state, gate_name, target, duration_ns / 2.0, rng)?;
368        }
369
370        Ok(())
371    }
372
373    /// Apply readout noise
374    pub fn apply_readout_noise(&self, measurement: u8, qubit: QubitId, rng: &mut impl Rng) -> u8 {
375        if let Some(params) = self.readout_noise.get(&qubit) {
376            let prob = rng.gen::<f64>();
377
378            if measurement == 0 {
379                if prob > params.assignment_matrix[0][0] {
380                    return 1;
381                }
382            } else if prob > params.assignment_matrix[1][1] {
383                return 0;
384            }
385        }
386
387        measurement
388    }
389
390    // Helper methods for applying specific noise channels
391
392    fn apply_rotation_error(
393        &self,
394        state: &mut Vec<Complex64>,
395        qubit: QubitId,
396        angle: f64,
397    ) -> QuantRS2Result<()> {
398        // Apply Z rotation error
399        let n_qubits = (state.len() as f64).log2() as usize;
400        let qubit_idx = qubit.id() as usize;
401
402        for i in 0..state.len() {
403            if (i >> qubit_idx) & 1 == 1 {
404                state[i] *= Complex64::from_polar(1.0, angle);
405            }
406        }
407
408        Ok(())
409    }
410
411    fn apply_amplitude_scaling(
412        &self,
413        state: &mut Vec<Complex64>,
414        qubit: QubitId,
415        scale: f64,
416    ) -> QuantRS2Result<()> {
417        // This is a simplified model - in reality would need more sophisticated approach
418        for amp in state.iter_mut() {
419            *amp *= scale;
420        }
421
422        // Renormalize
423        let norm = state.iter().map(|a| a.norm_sqr()).sum::<f64>().sqrt();
424        for amp in state.iter_mut() {
425            *amp /= norm;
426        }
427
428        Ok(())
429    }
430
431    fn apply_phase_error(
432        &self,
433        state: &mut Vec<Complex64>,
434        qubit: QubitId,
435        phase: f64,
436    ) -> QuantRS2Result<()> {
437        let qubit_idx = qubit.id() as usize;
438
439        for i in 0..state.len() {
440            if (i >> qubit_idx) & 1 == 1 {
441                state[i] *= Complex64::from_polar(1.0, phase);
442            }
443        }
444
445        Ok(())
446    }
447
448    fn apply_amplitude_damping(
449        &self,
450        state: &mut Vec<Complex64>,
451        qubit: QubitId,
452        gamma: f64,
453    ) -> QuantRS2Result<()> {
454        // Simplified amplitude damping
455        let qubit_idx = qubit.id() as usize;
456        let damping_factor = (1.0 - gamma).sqrt();
457
458        for i in 0..state.len() {
459            if (i >> qubit_idx) & 1 == 1 {
460                state[i] *= damping_factor;
461            }
462        }
463
464        Ok(())
465    }
466
467    fn apply_phase_damping(
468        &self,
469        state: &mut Vec<Complex64>,
470        qubit: QubitId,
471        gamma: f64,
472    ) -> QuantRS2Result<()> {
473        // Apply random phase to superposition states
474        let qubit_idx = qubit.id() as usize;
475
476        // This is simplified - proper implementation would track density matrix
477        for i in 0..state.len() {
478            if (i >> qubit_idx) & 1 == 1 {
479                state[i] *= (1.0 - gamma).sqrt();
480            }
481        }
482
483        Ok(())
484    }
485
486    fn apply_two_qubit_rotation_error(
487        &self,
488        state: &mut Vec<Complex64>,
489        control: QubitId,
490        target: QubitId,
491        angle: f64,
492    ) -> QuantRS2Result<()> {
493        // Apply ZZ rotation error
494        let control_idx = control.id() as usize;
495        let target_idx = target.id() as usize;
496
497        for i in 0..state.len() {
498            let control_bit = (i >> control_idx) & 1;
499            let target_bit = (i >> target_idx) & 1;
500
501            if control_bit == 1 && target_bit == 1 {
502                state[i] *= Complex64::from_polar(1.0, angle);
503            }
504        }
505
506        Ok(())
507    }
508
509    fn apply_zz_interaction(
510        &self,
511        state: &mut Vec<Complex64>,
512        qubit1: QubitId,
513        qubit2: QubitId,
514        angle: f64,
515    ) -> QuantRS2Result<()> {
516        let idx1 = qubit1.id() as usize;
517        let idx2 = qubit2.id() as usize;
518
519        for i in 0..state.len() {
520            let bit1 = (i >> idx1) & 1;
521            let bit2 = (i >> idx2) & 1;
522
523            // ZZ interaction: |00⟩ and |11⟩ get +angle, |01⟩ and |10⟩ get -angle
524            let phase = if bit1 == bit2 { angle } else { -angle };
525            state[i] *= Complex64::from_polar(1.0, phase / 2.0);
526        }
527
528        Ok(())
529    }
530
531    fn apply_crosstalk_error(
532        &self,
533        state: &mut Vec<Complex64>,
534        qubit: QubitId,
535        strength: f64,
536    ) -> QuantRS2Result<()> {
537        // Apply small random rotation due to crosstalk
538        self.apply_rotation_error(state, qubit, strength)
539    }
540}
541
542/// Create a noise model from calibration with custom parameters
543pub struct NoiseModelBuilder {
544    calibration: DeviceCalibration,
545    coherent_factor: f64,
546    thermal_factor: f64,
547    crosstalk_factor: f64,
548    readout_factor: f64,
549}
550
551impl NoiseModelBuilder {
552    /// Create builder from calibration
553    pub const fn from_calibration(calibration: DeviceCalibration) -> Self {
554        Self {
555            calibration,
556            coherent_factor: 1.0,
557            thermal_factor: 1.0,
558            crosstalk_factor: 1.0,
559            readout_factor: 1.0,
560        }
561    }
562
563    /// Scale coherent errors
564    #[must_use]
565    pub const fn coherent_factor(mut self, factor: f64) -> Self {
566        self.coherent_factor = factor;
567        self
568    }
569
570    /// Scale thermal noise
571    #[must_use]
572    pub const fn thermal_factor(mut self, factor: f64) -> Self {
573        self.thermal_factor = factor;
574        self
575    }
576
577    /// Scale crosstalk
578    #[must_use]
579    pub const fn crosstalk_factor(mut self, factor: f64) -> Self {
580        self.crosstalk_factor = factor;
581        self
582    }
583
584    /// Scale readout errors
585    #[must_use]
586    pub const fn readout_factor(mut self, factor: f64) -> Self {
587        self.readout_factor = factor;
588        self
589    }
590
591    /// Build the noise model
592    pub fn build(self) -> CalibrationNoiseModel {
593        let mut model = CalibrationNoiseModel::from_calibration(&self.calibration);
594
595        // Apply scaling factors
596        for noise in model.gate_noise.values_mut() {
597            noise.coherent_error *= self.coherent_factor;
598        }
599
600        for noise in model.qubit_noise.values_mut() {
601            noise.thermal_population *= self.thermal_factor;
602        }
603
604        for row in &mut model.crosstalk.crosstalk_matrix {
605            for val in row.iter_mut() {
606                *val *= self.crosstalk_factor;
607            }
608        }
609
610        for readout in model.readout_noise.values_mut() {
611            // Scale off-diagonal elements (errors)
612            readout.assignment_matrix[0][1] *= self.readout_factor;
613            readout.assignment_matrix[1][0] *= self.readout_factor;
614            // Adjust diagonal to maintain normalization
615            readout.assignment_matrix[0][0] = 1.0 - readout.assignment_matrix[0][1];
616            readout.assignment_matrix[1][1] = 1.0 - readout.assignment_matrix[1][0];
617        }
618
619        model
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use crate::calibration::create_ideal_calibration;
627
628    #[test]
629    fn test_noise_model_from_calibration() {
630        let cal = create_ideal_calibration("test".to_string(), 5);
631        let noise_model = CalibrationNoiseModel::from_calibration(&cal);
632
633        assert_eq!(noise_model.device_id, "test");
634        assert_eq!(noise_model.qubit_noise.len(), 5);
635        assert!(noise_model.gate_noise.contains_key("X"));
636        assert!(noise_model.gate_noise.contains_key("CNOT"));
637    }
638
639    #[test]
640    fn test_noise_model_builder() {
641        let cal = create_ideal_calibration("test".to_string(), 3);
642        let noise_model = NoiseModelBuilder::from_calibration(cal)
643            .coherent_factor(0.5)
644            .thermal_factor(2.0)
645            .crosstalk_factor(0.1)
646            .readout_factor(0.5)
647            .build();
648
649        // Check that factors were applied
650        let x_noise = noise_model
651            .gate_noise
652            .get("X")
653            .expect("X gate noise should exist");
654        assert!(x_noise.coherent_error < 0.001); // Should be reduced by factor
655    }
656}