From c6f5c29c0815319c6a39a01f426af817c5521e36 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sat, 19 Sep 2026 23:43:23 -0600 Subject: [PATCH] Apply two-qubit gate noise to every pair of a batched command and judge leakage per pair --- .../src/noise/biased_depolarizing.rs | 235 +++++++++------- .../pecos-engines/src/noise/depolarizing.rs | 250 +++++++++++------- crates/pecos-engines/src/noise/general.rs | 69 ++++- exp/pecos-eeg/src/noise.rs | 47 +++- exp/pecos-neo/src/noise/per_gate_pauli.rs | 88 ++++-- exp/pecos-neo/src/noise/two_qubit.rs | 55 ++++ 6 files changed, 535 insertions(+), 209 deletions(-) diff --git a/crates/pecos-engines/src/noise/biased_depolarizing.rs b/crates/pecos-engines/src/noise/biased_depolarizing.rs index e8fb50ceb..6b3b4f1ac 100644 --- a/crates/pecos-engines/src/noise/biased_depolarizing.rs +++ b/crates/pecos-engines/src/noise/biased_depolarizing.rs @@ -352,97 +352,103 @@ impl BiasedDepolarizingNoiseModel { } fn apply_tq_faults(&mut self, builder: &mut ByteMessageBuilder, gate: &Gate) { - if self.rng.occurs(self.p2) { - let fault_type = self.rng.random_int(0..15); - let qubit0 = gate.qubits[0]; - let qubit1 = gate.qubits[1]; - - match fault_type { - // IX - 0 => { - trace!("Applying IX fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_x(builder, *qubit1); - } - // IY - 1 => { - trace!("Applying IY fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_y(builder, *qubit1); - } - // IZ - 2 => { - trace!("Applying IZ fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_z(builder, *qubit1); - } - // XI - 3 => { - trace!("Applying XI fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_x(builder, *qubit0); - } - // XX - 4 => { - trace!("Applying XX fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_x(builder, *qubit0); - NoiseUtils::apply_x(builder, *qubit1); - } - // XY - 5 => { - trace!("Applying XY fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_x(builder, *qubit0); - NoiseUtils::apply_y(builder, *qubit1); - } - // XZ - 6 => { - trace!("Applying XZ fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_x(builder, *qubit0); - NoiseUtils::apply_z(builder, *qubit1); - } - // YI - 7 => { - trace!("Applying YI fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_y(builder, *qubit0); - } - // YX - 8 => { - trace!("Applying YX fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_y(builder, *qubit0); - NoiseUtils::apply_x(builder, *qubit1); - } - // YY - 9 => { - trace!("Applying YY fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_y(builder, *qubit0); - NoiseUtils::apply_y(builder, *qubit1); - } - // YZ - 10 => { - trace!("Applying YZ fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_y(builder, *qubit0); - NoiseUtils::apply_z(builder, *qubit1); - } - // ZI - 11 => { - trace!("Applying ZI fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_z(builder, *qubit0); - } - // ZX - 12 => { - trace!("Applying ZX fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_z(builder, *qubit0); - NoiseUtils::apply_x(builder, *qubit1); - } - // ZY - 13 => { - trace!("Applying ZY fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_z(builder, *qubit0); - NoiseUtils::apply_y(builder, *qubit1); - } - // ZZ - _ => { - trace!("Applying ZZ fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_z(builder, *qubit0); - NoiseUtils::apply_z(builder, *qubit1); + // CCX retains its existing first-two-qubits fault behavior. + for qubits in gate.qubits.as_chunks::<2>().0 { + if self.rng.occurs(self.p2) { + let fault_type = self.rng.random_int(0..15); + let qubit0 = qubits[0]; + let qubit1 = qubits[1]; + + match fault_type { + // IX + 0 => { + trace!("Applying IX fault on qubits {qubits:?}"); + NoiseUtils::apply_x(builder, *qubit1); + } + // IY + 1 => { + trace!("Applying IY fault on qubits {qubits:?}"); + NoiseUtils::apply_y(builder, *qubit1); + } + // IZ + 2 => { + trace!("Applying IZ fault on qubits {qubits:?}"); + NoiseUtils::apply_z(builder, *qubit1); + } + // XI + 3 => { + trace!("Applying XI fault on qubits {qubits:?}"); + NoiseUtils::apply_x(builder, *qubit0); + } + // XX + 4 => { + trace!("Applying XX fault on qubits {qubits:?}"); + NoiseUtils::apply_x(builder, *qubit0); + NoiseUtils::apply_x(builder, *qubit1); + } + // XY + 5 => { + trace!("Applying XY fault on qubits {qubits:?}"); + NoiseUtils::apply_x(builder, *qubit0); + NoiseUtils::apply_y(builder, *qubit1); + } + // XZ + 6 => { + trace!("Applying XZ fault on qubits {qubits:?}"); + NoiseUtils::apply_x(builder, *qubit0); + NoiseUtils::apply_z(builder, *qubit1); + } + // YI + 7 => { + trace!("Applying YI fault on qubits {qubits:?}"); + NoiseUtils::apply_y(builder, *qubit0); + } + // YX + 8 => { + trace!("Applying YX fault on qubits {qubits:?}"); + NoiseUtils::apply_y(builder, *qubit0); + NoiseUtils::apply_x(builder, *qubit1); + } + // YY + 9 => { + trace!("Applying YY fault on qubits {qubits:?}"); + NoiseUtils::apply_y(builder, *qubit0); + NoiseUtils::apply_y(builder, *qubit1); + } + // YZ + 10 => { + trace!("Applying YZ fault on qubits {qubits:?}"); + NoiseUtils::apply_y(builder, *qubit0); + NoiseUtils::apply_z(builder, *qubit1); + } + // ZI + 11 => { + trace!("Applying ZI fault on qubits {qubits:?}"); + NoiseUtils::apply_z(builder, *qubit0); + } + // ZX + 12 => { + trace!("Applying ZX fault on qubits {qubits:?}"); + NoiseUtils::apply_z(builder, *qubit0); + NoiseUtils::apply_x(builder, *qubit1); + } + // ZY + 13 => { + trace!("Applying ZY fault on qubits {qubits:?}"); + NoiseUtils::apply_z(builder, *qubit0); + NoiseUtils::apply_y(builder, *qubit1); + } + // ZZ + _ => { + trace!("Applying ZZ fault on qubits {qubits:?}"); + NoiseUtils::apply_z(builder, *qubit0); + NoiseUtils::apply_z(builder, *qubit1); + } } } + if gate.gate_type == GateType::CCX { + break; + } } } } @@ -658,6 +664,57 @@ impl crate::noise::IntoNoiseModel for BiasedDepolarizingNoiseModelBuilder { mod tests { use super::*; + #[test] + fn test_ccx_faults_retain_first_pair_behavior() { + for seed in 0..16 { + for triples in [vec![(0, 1, 2)], vec![(0, 1, 2), (3, 4, 5)]] { + let mut noise = BiasedDepolarizingNoiseModel::new(0.0, 0.0, 0.0, 0.0, 1.0); + noise.set_seed(seed); + let mut reference = noise.clone(); + let mut ccx = ByteMessage::quantum_operations_builder(); + ccx.add_gate_command(&Gate::ccx(&triples)); + ccx.cx(&[(6, 7)]); + let mut cx = ByteMessage::quantum_operations_builder(); + cx.cx(&[(0, 1)]); + cx.cx(&[(6, 7)]); + let EngineStage::NeedsProcessing(actual) = noise.start(ccx.build()).unwrap() else { + panic!("Expected NeedsProcessing stage"); + }; + let EngineStage::NeedsProcessing(expected) = reference.start(cx.build()).unwrap() + else { + panic!("Expected NeedsProcessing stage"); + }; + // The following CX also verifies that CCX consumed only one fault draw. + assert_eq!( + actual.quantum_ops().unwrap()[1..], + expected.quantum_ops().unwrap()[1..] + ); + } + } + } + + #[test] + fn test_batched_two_qubit_faults_are_independent() { + for seed in 0..32 { + let mut noise = BiasedDepolarizingNoiseModel::new(0.0, 0.0, 0.0, 0.0, 0.5); + noise.set_seed(seed); + let mut separate = noise.clone(); + // Compare fault bytes: ideal gate serialization differs between batching forms. + let mut batched_output = ByteMessage::quantum_operations_builder(); + noise.apply_tq_faults(&mut batched_output, &Gate::cx(&[(0, 1), (2, 3)])); + noise.apply_tq_faults(&mut batched_output, &Gate::cx(&[(4, 5)])); + let mut separate_output = ByteMessage::quantum_operations_builder(); + for pair in [(0, 1), (2, 3), (4, 5)] { + separate.apply_tq_faults(&mut separate_output, &Gate::cx(&[pair])); + } + assert_eq!( + batched_output.build().as_bytes(), + separate_output.build().as_bytes(), + "batched fault stream differs at seed {seed}" + ); + } + } + #[test] fn test_probabilities_getter_and_setter() { // Create a noise model with initial probabilities diff --git a/crates/pecos-engines/src/noise/depolarizing.rs b/crates/pecos-engines/src/noise/depolarizing.rs index 4689ba2f5..3e89ab1fe 100644 --- a/crates/pecos-engines/src/noise/depolarizing.rs +++ b/crates/pecos-engines/src/noise/depolarizing.rs @@ -324,97 +324,103 @@ impl DepolarizingNoiseModel { builder: &mut ByteMessageBuilder, gate: &Gate, ) { - // Use fused noise sampling: probability check + Pauli selection in one call - if let Some(fault_type) = rng.inner_mut().noise_sample_2q(p2_threshold) { - let qubit0 = gate.qubits[0]; - let qubit1 = gate.qubits[1]; - - match fault_type { - // IX - 0 => { - trace!("Applying IX fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_x(builder, *qubit1); - } - // IY - 1 => { - trace!("Applying IY fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_y(builder, *qubit1); - } - // IZ - 2 => { - trace!("Applying IZ fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_z(builder, *qubit1); - } - // XI - 3 => { - trace!("Applying XI fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_x(builder, *qubit0); - } - // XX - 4 => { - trace!("Applying XX fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_x(builder, *qubit0); - NoiseUtils::apply_x(builder, *qubit1); - } - // XY - 5 => { - trace!("Applying XY fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_x(builder, *qubit0); - NoiseUtils::apply_y(builder, *qubit1); - } - // XZ - 6 => { - trace!("Applying XZ fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_x(builder, *qubit0); - NoiseUtils::apply_z(builder, *qubit1); - } - // YI - 7 => { - trace!("Applying YI fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_y(builder, *qubit0); - } - // YX - 8 => { - trace!("Applying YX fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_y(builder, *qubit0); - NoiseUtils::apply_x(builder, *qubit1); - } - // YY - 9 => { - trace!("Applying YY fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_y(builder, *qubit0); - NoiseUtils::apply_y(builder, *qubit1); - } - // YZ - 10 => { - trace!("Applying YZ fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_y(builder, *qubit0); - NoiseUtils::apply_z(builder, *qubit1); - } - // ZI - 11 => { - trace!("Applying ZI fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_z(builder, *qubit0); - } - // ZX - 12 => { - trace!("Applying ZX fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_z(builder, *qubit0); - NoiseUtils::apply_x(builder, *qubit1); - } - // ZY - 13 => { - trace!("Applying ZY fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_z(builder, *qubit0); - NoiseUtils::apply_y(builder, *qubit1); - } - // ZZ - _ => { - trace!("Applying ZZ fault on qubits {:?}", gate.qubits); - NoiseUtils::apply_z(builder, *qubit0); - NoiseUtils::apply_z(builder, *qubit1); + // CCX retains its existing first-two-qubits fault behavior. + for qubits in gate.qubits.as_chunks::<2>().0 { + // Use fused noise sampling: probability check + Pauli selection in one call + if let Some(fault_type) = rng.inner_mut().noise_sample_2q(p2_threshold) { + let qubit0 = qubits[0]; + let qubit1 = qubits[1]; + + match fault_type { + // IX + 0 => { + trace!("Applying IX fault on qubits {qubits:?}"); + NoiseUtils::apply_x(builder, *qubit1); + } + // IY + 1 => { + trace!("Applying IY fault on qubits {qubits:?}"); + NoiseUtils::apply_y(builder, *qubit1); + } + // IZ + 2 => { + trace!("Applying IZ fault on qubits {qubits:?}"); + NoiseUtils::apply_z(builder, *qubit1); + } + // XI + 3 => { + trace!("Applying XI fault on qubits {qubits:?}"); + NoiseUtils::apply_x(builder, *qubit0); + } + // XX + 4 => { + trace!("Applying XX fault on qubits {qubits:?}"); + NoiseUtils::apply_x(builder, *qubit0); + NoiseUtils::apply_x(builder, *qubit1); + } + // XY + 5 => { + trace!("Applying XY fault on qubits {qubits:?}"); + NoiseUtils::apply_x(builder, *qubit0); + NoiseUtils::apply_y(builder, *qubit1); + } + // XZ + 6 => { + trace!("Applying XZ fault on qubits {qubits:?}"); + NoiseUtils::apply_x(builder, *qubit0); + NoiseUtils::apply_z(builder, *qubit1); + } + // YI + 7 => { + trace!("Applying YI fault on qubits {qubits:?}"); + NoiseUtils::apply_y(builder, *qubit0); + } + // YX + 8 => { + trace!("Applying YX fault on qubits {qubits:?}"); + NoiseUtils::apply_y(builder, *qubit0); + NoiseUtils::apply_x(builder, *qubit1); + } + // YY + 9 => { + trace!("Applying YY fault on qubits {qubits:?}"); + NoiseUtils::apply_y(builder, *qubit0); + NoiseUtils::apply_y(builder, *qubit1); + } + // YZ + 10 => { + trace!("Applying YZ fault on qubits {qubits:?}"); + NoiseUtils::apply_y(builder, *qubit0); + NoiseUtils::apply_z(builder, *qubit1); + } + // ZI + 11 => { + trace!("Applying ZI fault on qubits {qubits:?}"); + NoiseUtils::apply_z(builder, *qubit0); + } + // ZX + 12 => { + trace!("Applying ZX fault on qubits {qubits:?}"); + NoiseUtils::apply_z(builder, *qubit0); + NoiseUtils::apply_x(builder, *qubit1); + } + // ZY + 13 => { + trace!("Applying ZY fault on qubits {qubits:?}"); + NoiseUtils::apply_z(builder, *qubit0); + NoiseUtils::apply_y(builder, *qubit1); + } + // ZZ + _ => { + trace!("Applying ZZ fault on qubits {qubits:?}"); + NoiseUtils::apply_z(builder, *qubit0); + NoiseUtils::apply_z(builder, *qubit1); + } } } + if gate.gate_type == GateType::CCX { + break; + } } } } @@ -647,6 +653,72 @@ mod tests { use super::*; use crate::engine_system::{ControlEngine, EngineStage}; + #[test] + fn test_ccx_faults_retain_first_pair_behavior() { + for seed in 0..16 { + for triples in [vec![(0, 1, 2)], vec![(0, 1, 2), (3, 4, 5)]] { + let mut noise = DepolarizingNoiseModel::new(0.0, 0.0, 0.0, 1.0); + noise.set_seed(seed); + let mut reference = noise.clone(); + let mut ccx = ByteMessage::quantum_operations_builder(); + ccx.add_gate_command(&Gate::ccx(&triples)); + ccx.cx(&[(6, 7)]); + let mut cx = ByteMessage::quantum_operations_builder(); + cx.cx(&[(0, 1)]); + cx.cx(&[(6, 7)]); + let EngineStage::NeedsProcessing(actual) = noise.start(ccx.build()).unwrap() else { + panic!("Expected NeedsProcessing stage"); + }; + let EngineStage::NeedsProcessing(expected) = reference.start(cx.build()).unwrap() + else { + panic!("Expected NeedsProcessing stage"); + }; + // The following CX also verifies that CCX consumed only one fault draw. + assert_eq!( + actual.quantum_ops().unwrap()[1..], + expected.quantum_ops().unwrap()[1..] + ); + } + } + } + + #[test] + fn test_batched_two_qubit_faults_are_independent() { + for seed in 0..32 { + let mut noise = DepolarizingNoiseModel::new(0.0, 0.0, 0.0, 0.5); + noise.set_seed(seed); + let mut separate = noise.clone(); + // Compare fault bytes: ideal gate serialization differs between batching forms. + let mut batched_output = ByteMessage::quantum_operations_builder(); + DepolarizingNoiseModel::apply_tq_faults( + &mut noise.rng, + noise.p2_threshold, + &mut batched_output, + &Gate::cx(&[(0, 1), (2, 3)]), + ); + DepolarizingNoiseModel::apply_tq_faults( + &mut noise.rng, + noise.p2_threshold, + &mut batched_output, + &Gate::cx(&[(4, 5)]), + ); + let mut separate_output = ByteMessage::quantum_operations_builder(); + for pair in [(0, 1), (2, 3), (4, 5)] { + DepolarizingNoiseModel::apply_tq_faults( + &mut separate.rng, + separate.p2_threshold, + &mut separate_output, + &Gate::cx(&[pair]), + ); + } + assert_eq!( + batched_output.build().as_bytes(), + separate_output.build().as_bytes(), + "batched fault stream differs at seed {seed}" + ); + } + } + #[test] fn test_probabilities_getter_and_setter() { // Create a noise model with initial probabilities diff --git a/crates/pecos-engines/src/noise/general.rs b/crates/pecos-engines/src/noise/general.rs index 5152da777..6666276d5 100644 --- a/crates/pecos-engines/src/noise/general.rs +++ b/crates/pecos-engines/src/noise/general.rs @@ -1199,10 +1199,9 @@ impl GeneralNoiseModel { for qubits in gate.qubits.as_chunks::<2>().0 { let mut add_original_gate = true; - // Check if the gate is acting on a leaked qubit in a way to + // Check whether this pair acts on a leaked qubit. let has_leakage = !self.leaked_qubits.is_empty() - && gate - .qubits + && qubits .iter() .any(|&qubit| self.is_leaked(usize::from(qubit))); @@ -1214,7 +1213,7 @@ impl GeneralNoiseModel { if self.rng.occurs(self.p2_emission_ratio) { if has_leakage { // potentially seep qubits - for qubit in &gate.qubits { + for qubit in qubits { if self.is_leaked(usize::from(*qubit)) && let Some(gates) = self.seep(usize::from(*qubit), self.p2_seepage_prob) @@ -1566,6 +1565,68 @@ mod tests { ); } + #[test] + fn test_batched_two_qubit_seepage_is_pair_local() { + for seed in 0..16 { + let mut noise = GeneralNoiseModel::builder() + .with_p2(1.0) + .with_p2_emission_ratio(1.0) + .with_p2_seepage_prob(0.5) + .build(); + noise.set_seed(seed); + noise.mark_as_leaked(0); + noise.mark_as_leaked(2); + let mut separate = noise.clone(); + let mut batched_output = ByteMessage::quantum_operations_builder(); + noise.apply_tq_faults(&Gate::cx(&[(0, 1), (2, 3)]), 1.0, &mut batched_output); + let mut separate_output = ByteMessage::quantum_operations_builder(); + for pair in [(0, 1), (2, 3)] { + separate.apply_tq_faults(&Gate::cx(&[pair]), 1.0, &mut separate_output); + } + assert_eq!( + batched_output.build().as_bytes(), + separate_output.build().as_bytes() + ); + assert_eq!(noise.leaked_qubits, separate.leaked_qubits); + } + } + + #[test] + fn test_batched_two_qubit_leakage_is_pair_local() { + for p2 in [0.0, 1.0] { + let mut noise = GeneralNoiseModel::builder() + .with_p_prep(1.0) + .with_prep_leak_ratio(1.0) + .with_p2(p2) + .build(); + noise.set_seed(42); + let mut prep = ByteMessage::quantum_operations_builder(); + prep.pz(&[0]); + noise.start(prep.build()).unwrap(); + assert!(noise.is_leaked(0)); + + let mut builder = ByteMessage::quantum_operations_builder(); + builder.cx(&[(0, 1), (2, 3)]); + let EngineStage::NeedsProcessing(output) = noise.start(builder.build()).unwrap() else { + panic!("Expected NeedsProcessing stage"); + }; + let gates = output.quantum_ops().unwrap(); + assert_eq!(gates[0], Gate::cx(&[(2, 3)])); + assert!( + gates + .iter() + .all(|gate| gate.qubits.iter().all(|q| **q >= 2)) + ); + if p2 == 0.0 { + assert_eq!(gates.len(), 1); + } else { + assert!(gates[1..].iter().any(|gate| { + matches!(gate.gate_type, GateType::X | GateType::Y | GateType::Z) + })); + } + } + } + #[test] fn test_default() { let model = GeneralNoiseModel::default(); diff --git a/exp/pecos-eeg/src/noise.rs b/exp/pecos-eeg/src/noise.rs index 57df69c18..451a54fdc 100644 --- a/exp/pecos-eeg/src/noise.rs +++ b/exp/pecos-eeg/src/noise.rs @@ -118,18 +118,20 @@ impl NoiseSpec for UniformNoise { | GateType::SXXdg | GateType::SYY | GateType::SYYdg => { - if self.idle_rz.abs() > 0.0 && qubits.len() >= 2 { - for &q in &qubits[..2] { - injections.push(NoiseInjection { - eeg_type: EegType::H, - label: Bm::z(q), - label2: None, - rate: self.idle_rz / 2.0, - }); + for qubits in qubits.as_chunks::<2>().0 { + if self.idle_rz.abs() > 0.0 { + for &q in qubits { + injections.push(NoiseInjection { + eeg_type: EegType::H, + label: Bm::z(q), + label2: None, + rate: self.idle_rz / 2.0, + }); + } + } + if self.p2 > 0.0 { + inject_depol_2q(qubits[0], qubits[1], self.p2, &mut injections); } - } - if self.p2 > 0.0 && qubits.len() >= 2 { - inject_depol_2q(qubits[0], qubits[1], self.p2, &mut injections); } } @@ -218,3 +220,26 @@ fn inject_depol_2q(qa: usize, qb: usize, prob: f64, out: &mut Vec = [[0, 1], [2, 3]] + .iter() + .flat_map(|pair| noise.noise_after_gate(0, GateType::CX, pair)) + .collect(); + assert_eq!(batched.len(), 34); + assert_eq!(batched.len(), separate.len()); + for (actual, expected) in batched.iter().zip(&separate) { + assert_eq!(actual.eeg_type, expected.eeg_type); + assert_eq!(actual.label, expected.label); + assert_eq!(actual.label2, expected.label2); + assert_eq!(actual.rate.to_bits(), expected.rate.to_bits()); + } + } +} diff --git a/exp/pecos-neo/src/noise/per_gate_pauli.rs b/exp/pecos-neo/src/noise/per_gate_pauli.rs index 89c925f0b..7d3d04050 100644 --- a/exp/pecos-neo/src/noise/per_gate_pauli.rs +++ b/exp/pecos-neo/src/noise/per_gate_pauli.rs @@ -237,23 +237,25 @@ impl PerGatePauliChannel { }; gates.push(GateCommand::new(pauli, smallvec::smallvec![qubit])); } - } else if qubits.len() == 2 { - let (first, second) = (qubits[0], qubits[1]); - if !ctx.is_leaked(first) && !ctx.is_leaked(second) { - let rates = self.rates_2q_for(gate_type, first, second); - let r = rng.random::(); - let mut cumulative = 0.0; - for (idx, &p) in rates.iter().enumerate() { - cumulative += p; - if r < cumulative { - let (pauli0, pauli1) = TWO_QUBIT_PAULIS[idx]; - if pauli0 != GateType::I { - gates.push(GateCommand::new(pauli0, smallvec::smallvec![first])); - } - if pauli1 != GateType::I { - gates.push(GateCommand::new(pauli1, smallvec::smallvec![second])); + } else if gate_type.is_two_qubit() { + for qubits in qubits.as_chunks::<2>().0 { + let (first, second) = (qubits[0], qubits[1]); + if !ctx.is_leaked(first) && !ctx.is_leaked(second) { + let rates = self.rates_2q_for(gate_type, first, second); + let r = rng.random::(); + let mut cumulative = 0.0; + for (idx, &p) in rates.iter().enumerate() { + cumulative += p; + if r < cumulative { + let (pauli0, pauli1) = TWO_QUBIT_PAULIS[idx]; + if pauli0 != GateType::I { + gates.push(GateCommand::new(pauli0, smallvec::smallvec![first])); + } + if pauli1 != GateType::I { + gates.push(GateCommand::new(pauli1, smallvec::smallvec![second])); + } + break; } - break; } } } @@ -397,6 +399,60 @@ mod tests { use crate::prelude::*; use pecos_simulators::SparseStab; + fn collect_gates(response: NoiseResponse) -> Vec { + match response { + NoiseResponse::InjectGates(gates) => (*gates).into_vec(), + NoiseResponse::None => Vec::new(), + _ => panic!("Expected Pauli faults or no fault"), + } + } + + #[test] + fn test_batched_two_qubit_faults() { + let channel = PerGatePauliChannel::new().with_base(0.0, 0.5); + let qubits = [QubitId(0), QubitId(1), QubitId(2), QubitId(3)]; + let trailing = [QubitId(4), QubitId(5)]; + for seed in 0..32 { + let mut ctx = NoiseContext::new(); + let mut rng = PecosRng::seed_from_u64(seed); + let mut separate_ctx = NoiseContext::new(); + let mut separate_rng = PecosRng::seed_from_u64(seed); + let mut batched_gates = Vec::new(); + for pair_batch in [qubits.as_slice(), trailing.as_slice()] { + let event = NoiseEvent::AfterGate { + gate_type: GateType::CX, + qubits: pair_batch, + angles: &[], + gate_id: None, + }; + batched_gates.extend(collect_gates(channel.apply(&event, &mut ctx, &mut rng))); + } + let mut separate_gates = Vec::new(); + for pair in qubits + .as_chunks::<2>() + .0 + .iter() + .chain(std::iter::once(&trailing)) + { + let event = NoiseEvent::AfterGate { + gate_type: GateType::CX, + qubits: pair, + angles: &[], + gate_id: None, + }; + separate_gates.extend(collect_gates(channel.apply( + &event, + &mut separate_ctx, + &mut separate_rng, + ))); + } + assert_eq!( + batched_gates, separate_gates, + "batched fault stream differs at seed {seed}" + ); + } + } + const SHOTS: usize = 20_000; fn flip_rate(model: ComposableNoiseModel, commands: &CommandQueue, qubit: usize) -> f64 { diff --git a/exp/pecos-neo/src/noise/two_qubit.rs b/exp/pecos-neo/src/noise/two_qubit.rs index 2646acef5..618eb7489 100644 --- a/exp/pecos-neo/src/noise/two_qubit.rs +++ b/exp/pecos-neo/src/noise/two_qubit.rs @@ -534,6 +534,15 @@ impl TwoQubitChannel { ctx: &mut NoiseContext, rng: &mut PecosRng, ) -> NoiseResponse { + if qubits.len() > 2 { + let mut response = NoiseResponse::None; + for pair in qubits.as_chunks::<2>().0 { + response = + response.combine(self.handle_after_gate(gate_type, pair, angles, ctx, rng)); + } + return response; + } + if qubits.len() < 2 { return NoiseResponse::None; } @@ -660,6 +669,52 @@ mod tests { } } + #[test] + fn test_batched_two_qubit_faults() { + let channel = TwoQubitChannel::depolarizing(0.5); + let qubits = [QubitId(0), QubitId(1), QubitId(2), QubitId(3)]; + let trailing = [QubitId(4), QubitId(5)]; + for seed in 0..32 { + let mut ctx = NoiseContext::new(); + let mut rng = PecosRng::seed_from_u64(seed); + let mut separate_ctx = NoiseContext::new(); + let mut separate_rng = PecosRng::seed_from_u64(seed); + let mut batched_gates = Vec::new(); + for pair_batch in [qubits.as_slice(), trailing.as_slice()] { + let event = NoiseEvent::AfterGate { + gate_type: GateType::CX, + qubits: pair_batch, + angles: &[], + gate_id: None, + }; + batched_gates.extend(collect_gates(channel.apply(&event, &mut ctx, &mut rng))); + } + let mut separate_gates = Vec::new(); + for pair in qubits + .as_chunks::<2>() + .0 + .iter() + .chain(std::iter::once(&trailing)) + { + let event = NoiseEvent::AfterGate { + gate_type: GateType::CX, + qubits: pair, + angles: &[], + gate_id: None, + }; + separate_gates.extend(collect_gates(channel.apply( + &event, + &mut separate_ctx, + &mut separate_rng, + ))); + } + assert_eq!( + batched_gates, separate_gates, + "batched fault stream differs at seed {seed}" + ); + } + } + #[test] fn test_depolarizing_channel() { let channel = TwoQubitChannel::depolarizing(1.0);