diff --git a/crates/pecos-engines/src/noise/general.rs b/crates/pecos-engines/src/noise/general.rs index 4905c7977..80eeb56b4 100644 --- a/crates/pecos-engines/src/noise/general.rs +++ b/crates/pecos-engines/src/noise/general.rs @@ -92,7 +92,7 @@ use pecos_core::QubitId; use pecos_core::errors::PecosError; use rand_chacha::ChaCha8Rng; use std::any::Any; -use std::collections::HashSet; +use std::collections::{BTreeSet, HashSet}; /// General noise model implementation that includes parameterized error channels for various quantum operations /// @@ -172,9 +172,9 @@ pub struct GeneralNoiseModel { /// qubit states after initialization. Ranges from 0 to 1. p_prep_leak_ratio: f64, - /// Probability of crosstalk during initialization operations + /// Probability of crosstalk during preparation operations /// - /// Models the probability that an initialization operation on one qubit affects nearby qubits. + /// Models the probability that a preparation operation on one qubit affects nearby qubits. /// In ion trap systems, this could represent scattered light during optical pumping affecting /// neighboring ions. p_prep_crosstalk: f64, @@ -326,10 +326,23 @@ pub struct GeneralNoiseModel { /// Random number generator for stochastic noise processes rng: NoiseRng, + /// Set of qubits that have been prepared at any point in the program. + /// + /// This is so that we know which qubits exists and we can apply crosstalk + /// to them. Qubits that are measured / discarded are not removed from here, since + /// PECOS does not assume measurements are destructive. This should not cause a + /// problem, since inactive qubits suffering error have no effect on the state, + /// and active qubits should always suffer errors under this naive crosstalk model. + /// + /// Using a `BTreeSet` because we will iterate over the qubits and we want determinism. + prepared_qubits: BTreeSet, + /// Track which qubits are being measured in the current batch and their gate types - /// This is needed to properly handle leakage during measurements - /// Each entry is (`qubit_id`, `is_measure_leaked`) - measured_qubits: Vec<(usize, bool)>, + /// This is needed to properly handle leakage during measurements as well + /// as crosstalk. + /// TODO: manage this via result tags. + /// Each entry is (`qubit_id`, `is_measure_leaked`, `is_crosstalk`) + measured_qubits: Vec<(usize, bool, bool)>, } impl ControlEngine for GeneralNoiseModel { @@ -503,9 +516,11 @@ impl GeneralNoiseModel { ); } GateType::Prep => { + for &q in &gate.qubits { + self.prepared_qubits.insert(usize::from(q)); + } self.apply_prep_faults(&gate, &mut builder); - - // TODO: Implement prep crosstalk when needed + self.apply_crosstalk_faults(&gate, self.p_prep_crosstalk, &mut builder); } GateType::Measure | GateType::MeasureLeaked => { // Track which qubits are being measured for leakage handling @@ -513,11 +528,12 @@ impl GeneralNoiseModel { self.measured_qubits.extend( gate.qubits .iter() - .map(|q| (usize::from(*q), is_measure_leaked)), + .map(|q| (usize::from(*q), is_measure_leaked, false)), ); // Measurement noise is handled in apply_noise_on_continue_processing // We still need to add the original gate here builder.add_gate_command(&gate); + self.apply_crosstalk_faults(&gate, self.p_meas_crosstalk, &mut builder); } GateType::I => { let err_msg = format!( @@ -589,15 +605,26 @@ impl GeneralNoiseModel { // Check if we have leaked qubits that were measured let has_leakage = !self.leaked_qubits.is_empty() - && self.measured_qubits.iter().any(|(q, _)| self.is_leaked(*q)); + && self + .measured_qubits + .iter() + .any(|(q, _, _)| self.is_leaked(*q)); for (idx, outcome) in measurement_outcomes.into_iter().enumerate() { let mut val = outcome; - // Check if this measurement corresponds to a leaked qubit - if has_leakage && idx < self.measured_qubits.len() { - let (qubit, is_measure_leaked) = self.measured_qubits[idx]; - if self.is_leaked(qubit) { + // Check if this measurement corresponds to a leaked qubit or comes from + // crosstalk + if idx < self.measured_qubits.len() { + let (qubit, is_measure_leaked, is_crosstalk) = self.measured_qubits[idx]; + + // Check if this measurement comes from crosstalk noise. If so, ignore it. + if is_crosstalk { + trace!("Qubit {qubit} was measured by crosstalk; outcome is ignored."); + continue; // Skip this iteration + } + + if has_leakage && self.is_leaked(qubit) { if is_measure_leaked { trace!("Qubit {qubit} is leaked, MeasureLeaked returns 2"); // For MeasureLeaked, return 2 for leaked qubits @@ -783,6 +810,41 @@ impl GeneralNoiseModel { } } + /// Apply crosstalk noise + /// + /// Naive crosstalk noise model: + /// 1. All qubits in the trap but the ones in the `gate` are subject to crosstalk + // error. The `gate` should be either qubit measurement or preparation. + // 2. *Each* qubit not in `gate` has the given `probability` to suffer an error. + /// 3. Affected qubits are collapsed into the computational basis (Z measurement). + /// + /// In ion trap systems, this could represent scattered light during optical pumping + /// affecting neighboring ions. + pub fn apply_crosstalk_faults( + &mut self, + gate: &Gate, + probability: f64, + builder: &mut ByteMessageBuilder, + ) { + let mut affected_qubits = Vec::new(); + let gate_qubits: Vec = gate.qubits.iter().map(|q| usize::from(*q)).collect(); + + for q in self.prepared_qubits.clone() { + if !gate_qubits.contains(&q) && self.rng.occurs(probability) { + affected_qubits.push(q); + trace!("Qubit {q} affected by crosstalk error"); + } + } + + builder.add_measurements(&affected_qubits); + // We need to mark these measurements as being introduced by crosstalk rather + // than the user's program so that we can discard the results in + // apply_noise_on_continue_processing. + self.measured_qubits.extend( + affected_qubits.iter().map(|&q| (q, false, true)), // (qubit, is_measure_leaked, is_crosstalk) + ); + } + /// Apply single-qubit gate noise faults /// /// Models errors that occur during single-qubit gate operations: @@ -2225,6 +2287,132 @@ mod tests { ); } + #[test] + fn test_prep_crosstalk() { + use crate::byte_message::ByteMessageBuilder; + + let mut model = GeneralNoiseModel::builder() + .with_p_prep_crosstalk(1.0) + .with_seed(42) + .build(); + let noise = model + .as_any_mut() + .downcast_mut::() + .unwrap(); + + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + // Prepare a bunch of |0> states + builder.add_prep(&[0, 1, 2, 3, 4]); + // Apply mid-circuit measurement and reset + builder.add_measurements(&[2]); + builder.add_prep(&[2]); + let _cmd = noise.apply_noise_on_start(&builder.build()).unwrap(); + + assert_eq!( + noise.measured_qubits.len(), + 5, + "There should be 5 measured qubits: one from MCMR and the others from + crosstalk got: {:?}", + noise.measured_qubits + ); + + let (q, _, is_crosstalk) = noise.measured_qubits[0]; + assert_eq!(q, 2, "The first measurement should be the MCMR on qubit 2"); + assert!(!is_crosstalk, "The first measurement should come from MCMR"); + + for (_, _, is_crosstalk) in &noise.measured_qubits[1..] { + assert!( + is_crosstalk, + "The other measurements should come from crosstalk" + ); + } + + // All results are 0 + let mut outcome_builder = ByteMessageBuilder::new(); + let _ = outcome_builder.for_outcomes(); + outcome_builder.add_outcomes(&[0, 0, 0, 0, 0]); + + let mcmr = noise + .apply_noise_on_continue_processing(outcome_builder.build()) + .unwrap(); + let results = mcmr.outcomes().unwrap(); + + assert_eq!( + noise.measured_qubits.len(), + 0, + "The list of measured_qubits should have been cleared." + ); + assert_eq!( + results.len(), + 1, + "There should only be one outcome: that of the mid-circ measurement" + ); + } + + #[test] + fn test_meas_crosstalk() { + use crate::byte_message::ByteMessageBuilder; + + let mut model = GeneralNoiseModel::builder() + .with_p_meas_crosstalk(1.0) + .with_seed(42) + .build(); + let noise = model + .as_any_mut() + .downcast_mut::() + .unwrap(); + + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + // Prepare a bunch of |0> states + builder.add_prep(&[0, 1, 2, 3, 4]); + // Apply mid-circuit measurement and reset + builder.add_measurements(&[2]); + builder.add_prep(&[2]); + let _cmd = noise.apply_noise_on_start(&builder.build()).unwrap(); + + assert_eq!( + noise.measured_qubits.len(), + 5, + "There should be 5 measured qubits: one from MCMR and the others from + crosstalk got: {:?}", + noise.measured_qubits + ); + + let (q, _, is_crosstalk) = noise.measured_qubits[0]; + assert_eq!(q, 2, "The first measurement should be the MCMR on qubit 2"); + assert!(!is_crosstalk, "The first measurement should come from MCMR"); + + for (_, _, is_crosstalk) in &noise.measured_qubits[1..] { + assert!( + is_crosstalk, + "The other measurements should come from crosstalk" + ); + } + + // All results are 0 + let mut outcome_builder = ByteMessageBuilder::new(); + let _ = outcome_builder.for_outcomes(); + outcome_builder.add_outcomes(&[0, 0, 0, 0, 0]); + + let mcmr = noise + .apply_noise_on_continue_processing(outcome_builder.build()) + .unwrap(); + let results = mcmr.outcomes().unwrap(); + + assert_eq!( + noise.measured_qubits.len(), + 0, + "The list of measured_qubits should have been cleared." + ); + assert_eq!( + results.len(), + 1, + "There should only be one outcome: that of the mid-circ measurement" + ); + } + #[test] fn test_parameter_scaling() { // Test that scaling factors are applied correctly - use builder pattern diff --git a/crates/pecos-engines/src/noise/general/builder.rs b/crates/pecos-engines/src/noise/general/builder.rs index 80b534ca2..2b6574a60 100644 --- a/crates/pecos-engines/src/noise/general/builder.rs +++ b/crates/pecos-engines/src/noise/general/builder.rs @@ -412,6 +412,15 @@ impl GeneralNoiseModelBuilder { self } + // TODO: See if we should put a average scaling... + /// Set the average prep crosstalk + #[must_use] + pub fn with_average_p_prep_crosstalk(mut self, prob: f64) -> Self { + let prob: f64 = prob * 18.0 / 5.0; + self.p_prep_crosstalk = Some(prob); + self + } + /// Set the scaling factor for initialization errors /// /// Multiplier for preparation error probabilities. Allows adjustment of the relative @@ -642,6 +651,15 @@ impl GeneralNoiseModelBuilder { self } + // TODO: See if we should put a average scaling... + /// Set the average measurement crosstalk + #[must_use] + pub fn with_average_p_meas_crosstalk(mut self, prob: f64) -> Self { + let prob: f64 = prob * 18.0 / 5.0; + self.p_meas_crosstalk = Some(prob); + self + } + /// Set the scaling factor for measurement faults /// /// Multiplier for measurement error probabilities. Allows adjustment of the relative diff --git a/crates/pecos-engines/src/noise/general/default.rs b/crates/pecos-engines/src/noise/general/default.rs index 24df765c2..76a6e3230 100644 --- a/crates/pecos-engines/src/noise/general/default.rs +++ b/crates/pecos-engines/src/noise/general/default.rs @@ -1,7 +1,7 @@ use crate::noise::{ GeneralNoiseModel, NoiseRng, SingleQubitWeightedSampler, TwoQubitWeightedSampler, }; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; impl Default for GeneralNoiseModel { /// Create a new noise model with default error parameters @@ -100,6 +100,7 @@ impl Default for GeneralNoiseModel { p2_idle: 0.0, leaked_qubits: HashSet::new(), rng: NoiseRng::default(), + prepared_qubits: BTreeSet::new(), measured_qubits: Vec::new(), p_meas_crosstalk: 0.0, p_prep_crosstalk: 0.0,