Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 202 additions & 14 deletions crates/pecos-engines/src/noise/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -326,10 +326,23 @@ pub struct GeneralNoiseModel {
/// Random number generator for stochastic noise processes
rng: NoiseRng<ChaCha8Rng>,

/// 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<usize>,

/// 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 {
Expand Down Expand Up @@ -503,21 +516,24 @@ 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
let is_measure_leaked = gate.gate_type == GateType::MeasureLeaked;
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!(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<usize> = 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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment from Ciaran on the commit:

dcc5fb7#r164014518

Hmmmm, let's see. Measurements end up kinda of being two rounds... First the noise model sends a measurement request to the simulator and then noise model gets a result from the simulator. Then then noise model can intercept/forward/etc. results to the "classical engine". This is done here:

PECOS/crates/pecos-engines/src/noise/general.rs

Line 362 in 55994b8

 fn continue_processing( 

I kinda of had to deal with a similar situation of distinguishing measurement results for leakage... and I ended up just storing the measurement type here:

PECOS/crates/pecos-engines/src/noise/general.rs

Line 332 in 55994b8

 measured_qubits: Vec<(usize, bool)>, 

But I feel like what needs to be done is to add some tagging mechanism in ByteMessage

I might... try adding result tagging to ByteMessage...

Copy link
Collaborator Author

@PabloAndresCQ PabloAndresCQ Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I understand. Without tagging in ByteMessage, what I could do is add an extra bool to measured_qubits, so that's Vec<(usize, bool, bool)> where the second bool corresponds to is_crosstalk to indicate its origin.

For the naive crosstalk, I'd just throw away the measurement outcome at apply_noise_on_continue_processing here whenever is_crosstalk is True. In future versions, it could be used to make a decision on how to apply crosstalk.

That makes sense, I can do it tomorrow morning. I guess there's no need for tags for the first version, but if you get them in, I'll use them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sounds good. Later I can add a tag system and replace that.

// 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:
Expand Down Expand Up @@ -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::<GeneralNoiseModel>()
.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::<GeneralNoiseModel>()
.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
Expand Down
18 changes: 18 additions & 0 deletions crates/pecos-engines/src/noise/general/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion crates/pecos-engines/src/noise/general/default.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading