Skip to main content

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