diff --git a/crates/pecos-core/src/gate_type.rs b/crates/pecos-core/src/gate_type.rs index 3b094fe2a..df2581cb3 100644 --- a/crates/pecos-core/src/gate_type.rs +++ b/crates/pecos-core/src/gate_type.rs @@ -72,6 +72,7 @@ pub enum GateType { // MZ = 104 Measure = 104, // MnZ = 105 + MeasureLeaked = 105, // TODO: MPauli instead of the other variants? // PX = 130 @@ -104,6 +105,7 @@ impl From for GateType { 58 => GateType::SZZdg, 82 => GateType::RZZ, 104 => GateType::Measure, + 105 => GateType::MeasureLeaked, 134 => GateType::Prep, 200 => GateType::Idle, _ => panic!("Invalid gate type ID: {value}"), @@ -134,6 +136,7 @@ impl GateType { | GateType::SZZ | GateType::SZZdg | GateType::Measure + | GateType::MeasureLeaked | GateType::Prep => 0, // Gates with one parameter @@ -170,6 +173,7 @@ impl GateType { | GateType::R1XY | GateType::U | GateType::Measure + | GateType::MeasureLeaked | GateType::Prep | GateType::Idle => 1, @@ -217,6 +221,7 @@ impl fmt::Display for GateType { GateType::SZZdg => write!(f, "SZZdg"), GateType::RZZ => write!(f, "RZZ"), GateType::Measure => write!(f, "Measure"), + GateType::MeasureLeaked => write!(f, "MeasureLeaked"), GateType::Prep => write!(f, "Prep"), GateType::Idle => write!(f, "Idle"), } @@ -239,6 +244,7 @@ mod tests { assert_eq!(GateType::RZ as u8, 32); assert_eq!(GateType::R1XY as u8, 36); assert_eq!(GateType::Measure as u8, 104); + assert_eq!(GateType::MeasureLeaked as u8, 105); assert_eq!(GateType::from(0u8), GateType::I); assert_eq!(GateType::from(1u8), GateType::X); @@ -250,6 +256,7 @@ mod tests { assert_eq!(GateType::from(32u8), GateType::RZ); assert_eq!(GateType::from(36u8), GateType::R1XY); assert_eq!(GateType::from(104u8), GateType::Measure); + assert_eq!(GateType::from(105u8), GateType::MeasureLeaked); } #[test] @@ -264,6 +271,7 @@ mod tests { assert_eq!(GateType::SZZ.classical_arity(), 0); assert_eq!(GateType::SZZdg.classical_arity(), 0); assert_eq!(GateType::Measure.classical_arity(), 0); + assert_eq!(GateType::MeasureLeaked.classical_arity(), 0); assert_eq!(GateType::Prep.classical_arity(), 0); // Gates with one parameter @@ -290,6 +298,7 @@ mod tests { assert_eq!(GateType::R1XY.quantum_arity(), 1); assert_eq!(GateType::U.quantum_arity(), 1); assert_eq!(GateType::Measure.quantum_arity(), 1); + assert_eq!(GateType::MeasureLeaked.quantum_arity(), 1); assert_eq!(GateType::Prep.quantum_arity(), 1); assert_eq!(GateType::Idle.quantum_arity(), 1); @@ -312,6 +321,7 @@ mod tests { assert!(!GateType::SZZ.is_parameterized()); assert!(!GateType::SZZdg.is_parameterized()); assert!(!GateType::Measure.is_parameterized()); + assert!(!GateType::MeasureLeaked.is_parameterized()); assert!(!GateType::Prep.is_parameterized()); // Parameterized gates @@ -334,6 +344,7 @@ mod tests { assert!(GateType::R1XY.is_single_qubit()); assert!(GateType::U.is_single_qubit()); assert!(GateType::Measure.is_single_qubit()); + assert!(GateType::MeasureLeaked.is_single_qubit()); assert!(GateType::Prep.is_single_qubit()); assert!(GateType::Idle.is_single_qubit()); @@ -356,6 +367,7 @@ mod tests { assert!(!GateType::R1XY.is_two_qubit()); assert!(!GateType::U.is_two_qubit()); assert!(!GateType::Measure.is_two_qubit()); + assert!(!GateType::MeasureLeaked.is_two_qubit()); assert!(!GateType::Prep.is_two_qubit()); assert!(!GateType::Idle.is_two_qubit()); diff --git a/crates/pecos-core/src/gates.rs b/crates/pecos-core/src/gates.rs index 88a5b1a8b..5d33900c5 100644 --- a/crates/pecos-core/src/gates.rs +++ b/crates/pecos-core/src/gates.rs @@ -250,6 +250,16 @@ impl Gate { ) } + /// Create `MeasureLeaked` gate on multiple qubits + #[must_use] + pub fn measure_leaked(qubits: &[impl Into + Copy]) -> Self { + Self::new( + GateType::MeasureLeaked, + vec![], + qubits.iter().map(|&q| q.into()).collect(), + ) + } + /// Create Prep gate on multiple qubits #[must_use] pub fn prep(qubits: &[impl Into + Copy]) -> Self { diff --git a/crates/pecos-engines/src/byte_message/builder.rs b/crates/pecos-engines/src/byte_message/builder.rs index 9adf71ac4..9e4b90a74 100644 --- a/crates/pecos-engines/src/byte_message/builder.rs +++ b/crates/pecos-engines/src/byte_message/builder.rs @@ -449,6 +449,24 @@ impl ByteMessageBuilder { self } + /// Add measure leakage operations for multiple qubits + /// + /// This behaves like `add_measurements()` but is intended for measuring qubits + /// that may be in a leaked state. In the future, this will output 0, 1, or 2 + /// (where 2 indicates the qubit is leaked). + /// + /// # Panics + /// + /// Panics if any qubit ID is too large to fit in a u32. + pub fn add_measure_leakages(&mut self, qubit_ids: &[usize]) -> &mut Self { + for &qubit in qubit_ids { + // Add a measure_leaked as a regular gate command + let gate = Gate::measure_leaked(&[qubit]); + self.add_gate_command(&gate); + } + self + } + /// Add a Prep gate pub fn add_prep(&mut self, qubits: &[usize]) -> &mut Self { let gate = Gate::prep(qubits); @@ -732,6 +750,30 @@ mod tests { } } + #[test] + fn test_add_measure_leakages() { + // Create a builder + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + + // Add measure_leakages for multiple qubits + let qubits = vec![0, 1, 2]; + builder.add_measure_leakages(&qubits); + + // Build the message + let message = builder.build(); + + // Parse the message + let commands = message.quantum_ops().unwrap(); + + // Verify the commands + assert_eq!(commands.len(), 3); + for i in 0..3 { + assert_eq!(commands[i].gate_type, GateType::MeasureLeaked); + assert_eq!(commands[i].qubits, vec![QubitId(qubits[i])]); + } + } + #[test] fn test_batch_structure() { // Create a builder diff --git a/crates/pecos-engines/src/noise/biased_depolarizing.rs b/crates/pecos-engines/src/noise/biased_depolarizing.rs index 8fb3a9019..8f26ab5ee 100644 --- a/crates/pecos-engines/src/noise/biased_depolarizing.rs +++ b/crates/pecos-engines/src/noise/biased_depolarizing.rs @@ -176,7 +176,7 @@ impl BiasedDepolarizingNoiseModel { trace!("Applying two-qubit gate with possible fault"); self.apply_tq_faults(&mut builder, gate); } - GateType::Measure => { + GateType::Measure | GateType::MeasureLeaked => { trace!("Applying measurement. Will apply bias after engine returns results."); // we apply biased measurement after the engine // returns the results, rather than before measurement diff --git a/crates/pecos-engines/src/noise/depolarizing.rs b/crates/pecos-engines/src/noise/depolarizing.rs index b7b0fcee8..fdfdd20a8 100644 --- a/crates/pecos-engines/src/noise/depolarizing.rs +++ b/crates/pecos-engines/src/noise/depolarizing.rs @@ -150,7 +150,7 @@ impl DepolarizingNoiseModel { GateType::RZ => { NoiseUtils::add_gate_to_builder(&mut builder, gate); } - GateType::Measure => { + GateType::Measure | GateType::MeasureLeaked => { trace!("Applying measurement with possible fault"); self.apply_meas_faults(&mut builder, gate); NoiseUtils::add_gate_to_builder(&mut builder, gate); diff --git a/crates/pecos-engines/src/noise/general.rs b/crates/pecos-engines/src/noise/general.rs index 531bf9ff7..d185f40d8 100644 --- a/crates/pecos-engines/src/noise/general.rs +++ b/crates/pecos-engines/src/noise/general.rs @@ -326,9 +326,10 @@ pub struct GeneralNoiseModel { /// Random number generator for stochastic noise processes rng: NoiseRng, - /// Track which qubits are being measured in the current batch + /// Track which qubits are being measured in the current batch and their gate types /// This is needed to properly handle leakage during measurements - measured_qubits: Vec, + /// Each entry is (`qubit_id`, `is_measure_leaked`) + measured_qubits: Vec<(usize, bool)>, } impl ControlEngine for GeneralNoiseModel { @@ -506,10 +507,14 @@ impl GeneralNoiseModel { // TODO: Implement prep crosstalk when needed } - GateType::Measure => { + GateType::Measure | GateType::MeasureLeaked => { // Track which qubits are being measured for leakage handling - self.measured_qubits - .extend(gate.qubits.iter().map(|q| usize::from(*q))); + let is_measure_leaked = gate.gate_type == GateType::MeasureLeaked; + self.measured_qubits.extend( + gate.qubits + .iter() + .map(|q| (usize::from(*q), is_measure_leaked)), + ); // Measurement noise is handled in apply_noise_on_continue_processing // We still need to add the original gate here builder.add_gate_command(&gate); @@ -584,23 +589,32 @@ 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 = self.measured_qubits[idx]; + let (qubit, is_measure_leaked) = self.measured_qubits[idx]; if self.is_leaked(qubit) { - trace!("Qubit {qubit} is leaked, measuring as 1"); - // Force the measurement outcome to be 1 for leaked qubits - val = 1; + if is_measure_leaked { + trace!("Qubit {qubit} is leaked, MeasureLeaked returns 2"); + // For MeasureLeaked, return 2 for leaked qubits + val = 2; + } else { + trace!("Qubit {qubit} is leaked, Measure returns 1"); + // For regular Measure, force the measurement outcome to be 1 + val = 1; + } } } - // Apply asymmetric measurement noise - if val == 1 { + // Apply asymmetric measurement noise (but not for leaked measurements returning 2) + if val == 2 { + // No noise applied to leaked measurements + trace!("No measurement noise applied to leaked qubit outcome"); + } else if val == 1 { if self.rng.occurs(self.p_meas_1) { trace!("Flipped measurement outcome 1->0"); val = 0; @@ -1001,7 +1015,32 @@ impl GeneralNoiseModel { } } - fn mark_as_leaked(&mut self, qubit: usize) { + /// Mark a qubit as leaked + /// + /// This method explicitly sets a qubit to the leaked state, simulating a qubit that has + /// transitioned outside the computational basis (e.g., to a higher energy level in ion traps). + /// + /// # Behavior of Leaked Qubits + /// + /// - Regular `Measure` operations on leaked qubits always return 1 + /// - `MeasureLeaked` operations on leaked qubits return 2 + /// - Leaked qubits remain leaked until a `Prep` operation is applied + /// - Gates applied to leaked qubits have no effect on the quantum state + /// + /// # Use Cases + /// + /// - Testing algorithm robustness against leakage errors + /// - Setting up specific initial conditions for simulations + /// - Research into leakage-aware quantum algorithms + /// - Debugging circuits with leakage errors + /// + /// # Example + /// + /// ```ignore + /// let mut noise_model = GeneralNoiseModel::default(); + /// noise_model.mark_as_leaked(0); // Mark qubit 0 as leaked + /// ``` + pub fn mark_as_leaked(&mut self, qubit: usize) { // TODO: see if some of the mark_as_leaked needs to move to self.leak() trace!("Marking qubit {qubit} as leaked"); self.leaked_qubits.insert(qubit); @@ -1934,6 +1973,182 @@ mod tests { } } + #[test] + fn test_measure_leaked_without_leakage() { + use crate::byte_message::ByteMessageBuilder; + + // Create a noise model with no errors (deterministic) + let mut model = GeneralNoiseModel::builder() + .with_prep_probability(0.0) + .with_meas_0_probability(0.0) + .with_meas_1_probability(0.0) + .with_p1_probability(0.0) + .with_p2_probability(0.0) + .build(); + + let noise = model + .as_any_mut() + .downcast_mut::() + .unwrap(); + + // No qubits are leaked + assert!(!noise.is_leaked(0)); + assert!(!noise.is_leaked(1)); + + // Create measurement gates with both Measure and MeasureLeaked + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + builder.add_measurements(&[0]); // Regular measure + builder.add_measure_leakages(&[1]); // MeasureLeaked + + let measurement_command = builder.build(); + let _noisy_command = noise.apply_noise_on_start(&measurement_command).unwrap(); + + // Create measurement results (both qubits in |0⟩ state) + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_outcomes(); + builder.add_outcomes(&[0, 0]); + + let results_message = noise + .apply_noise_on_continue_processing(builder.build()) + .unwrap(); + + let results = results_message.outcomes().unwrap(); + assert_eq!(results.len(), 2); + assert_eq!(results[0], 0, "Regular Measure of |0⟩ should return 0"); + assert_eq!( + results[1], 0, + "MeasureLeaked of |0⟩ should return 0 (not leaked)" + ); + + // Test with |1⟩ state + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_outcomes(); + builder.add_outcomes(&[1, 1]); + + let results_message = noise + .apply_noise_on_continue_processing(builder.build()) + .unwrap(); + + let results = results_message.outcomes().unwrap(); + assert_eq!(results[0], 1, "Regular Measure of |1⟩ should return 1"); + assert_eq!( + results[1], 1, + "MeasureLeaked of |1⟩ should return 1 (not leaked)" + ); + } + + #[test] + fn test_measure_leaked_with_leakage() { + use crate::byte_message::ByteMessageBuilder; + + // Create a noise model with no measurement errors (deterministic) + let mut model = GeneralNoiseModel::builder() + .with_prep_probability(0.0) + .with_meas_0_probability(0.0) + .with_meas_1_probability(0.0) + .with_p1_probability(0.0) + .with_p2_probability(0.0) + .build(); + + let noise = model + .as_any_mut() + .downcast_mut::() + .unwrap(); + + // Manually mark qubits 0 and 1 as leaked + noise.mark_as_leaked(0); + noise.mark_as_leaked(1); + + // Create measurement gates with both Measure and MeasureLeaked + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + builder.add_measurements(&[0]); // Regular measure on leaked qubit + builder.add_measure_leakages(&[1]); // MeasureLeaked on leaked qubit + + let measurement_command = builder.build(); + let _noisy_command = noise.apply_noise_on_start(&measurement_command).unwrap(); + + // Create measurement results (simulator returns 0, but noise model will override) + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_outcomes(); + builder.add_outcomes(&[0, 0]); + + let results_message = noise + .apply_noise_on_continue_processing(builder.build()) + .unwrap(); + + let results = results_message.outcomes().unwrap(); + assert_eq!(results.len(), 2); + assert_eq!( + results[0], 1, + "Regular Measure of leaked qubit should return 1" + ); + assert_eq!( + results[1], 2, + "MeasureLeaked of leaked qubit should return 2" + ); + } + + #[test] + fn test_measure_leaked_mixed_scenario() { + use crate::byte_message::ByteMessageBuilder; + + // Create a noise model with no measurement errors (deterministic) + let mut model = GeneralNoiseModel::builder() + .with_prep_probability(0.0) + .with_meas_0_probability(0.0) + .with_meas_1_probability(0.0) + .with_p1_probability(0.0) + .with_p2_probability(0.0) + .build(); + + let noise = model + .as_any_mut() + .downcast_mut::() + .unwrap(); + + // Mark only even qubits as leaked + noise.mark_as_leaked(0); + noise.mark_as_leaked(2); + + // Create mixed measurement gates + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + builder.add_measurements(&[0]); // Regular measure on leaked qubit 0 + builder.add_measure_leakages(&[1]); // MeasureLeaked on non-leaked qubit 1 + builder.add_measure_leakages(&[2]); // MeasureLeaked on leaked qubit 2 + builder.add_measurements(&[3]); // Regular measure on non-leaked qubit 3 + + let measurement_command = builder.build(); + let _noisy_command = noise.apply_noise_on_start(&measurement_command).unwrap(); + + // Create measurement results (mix of 0s and 1s from simulator) + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_outcomes(); + builder.add_outcomes(&[0, 1, 0, 1]); // Simulator results before noise + + let results_message = noise + .apply_noise_on_continue_processing(builder.build()) + .unwrap(); + + let results = results_message.outcomes().unwrap(); + assert_eq!(results.len(), 4); + assert_eq!(results[0], 1, "Measure on leaked qubit 0 should return 1"); + assert_eq!( + results[1], 1, + "MeasureLeaked on non-leaked qubit 1 should preserve simulator result 1" + ); + assert_eq!( + results[2], 2, + "MeasureLeaked on leaked qubit 2 should return 2" + ); + assert_eq!( + results[3], 1, + "Measure on non-leaked qubit 3 should preserve simulator result 1" + ); + } + #[test] fn test_measurement_bias_with_leakage() { use crate::byte_message::ByteMessageBuilder; diff --git a/crates/pecos-engines/src/quantum.rs b/crates/pecos-engines/src/quantum.rs index 686f098d0..3853fa2f9 100644 --- a/crates/pecos-engines/src/quantum.rs +++ b/crates/pecos-engines/src/quantum.rs @@ -213,7 +213,7 @@ impl Engine for StateVecEngine { } // TODO: Fix it so we have multiple result_ids or get rid of result ids... - GateType::Measure => { + GateType::Measure | GateType::MeasureLeaked => { for q in &cmd.qubits { debug!("Processing measurement on qubit {q:?}"); let meas_result = self.simulator.mz(**q); @@ -460,7 +460,7 @@ impl Engine for SparseStabEngine { self.process_two_qubit_gate(cmd.gate_type, &cmd.qubits); } // Special operations - GateType::Measure => { + GateType::Measure | GateType::MeasureLeaked => { for q in &cmd.qubits { debug!("Processing measurement on qubit {q:?}"); let meas_result = self.simulator.mz(**q); diff --git a/crates/pecos-engines/tests/measure_leaked_test.rs b/crates/pecos-engines/tests/measure_leaked_test.rs new file mode 100644 index 000000000..f75974852 --- /dev/null +++ b/crates/pecos-engines/tests/measure_leaked_test.rs @@ -0,0 +1,225 @@ +use pecos_engines::byte_message::ByteMessageBuilder; +use pecos_engines::noise::general::GeneralNoiseModel; +use pecos_engines::quantum::StateVecEngine; +use pecos_engines::{Engine, QuantumSystem}; + +#[test] +fn test_measure_leaked_basic_functionality() { + // Create a simple 2-qubit system with no noise + let engine = Box::new(StateVecEngine::new(2)); + let mut system = QuantumSystem::new_without_noise(engine); + + // Test 1: MeasureLeaked behaves like Measure without leakage + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + builder.add_h(&[0]); // Create superposition + builder.add_measure_leakages(&[0, 1]); // MeasureLeaked on both qubits + + let circuit = builder.build(); + let result = system.process(circuit).unwrap(); + let outcomes = result.outcomes().unwrap(); + + assert_eq!(outcomes.len(), 2); + assert!( + outcomes[0] <= 1, + "MeasureLeaked without leakage should return 0 or 1" + ); + assert_eq!(outcomes[1], 0, "Qubit 1 should be in |0⟩ state"); +} + +#[test] +fn test_measure_leaked_with_general_noise_model() { + // Create a noise model + let mut noise_model = GeneralNoiseModel::builder() + .with_prep_probability(0.0) + .with_meas_0_probability(0.0) + .with_meas_1_probability(0.0) + .with_p1_probability(0.0) + .with_p2_probability(0.0) + .with_seed(42) + .build(); + + // Manually mark qubits as leaked + noise_model.mark_as_leaked(0); + noise_model.mark_as_leaked(2); + + let engine = Box::new(StateVecEngine::new(3)); + let mut system = QuantumSystem::new(Box::new(noise_model), engine); + + // Create measurement circuit + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + + // Mix of regular Measure and MeasureLeaked + builder.add_measurements(&[0]); // Regular measure on leaked qubit 0 + builder.add_measure_leakages(&[1]); // MeasureLeaked on non-leaked qubit 1 + builder.add_measure_leakages(&[2]); // MeasureLeaked on leaked qubit 2 + + let circuit = builder.build(); + let result = system.process(circuit).unwrap(); + let outcomes = result.outcomes().unwrap(); + + assert_eq!(outcomes.len(), 3); + assert_eq!( + outcomes[0], 1, + "Regular Measure on leaked qubit should return 1" + ); + assert_eq!( + outcomes[1], 0, + "MeasureLeaked on non-leaked qubit should return 0" + ); + assert_eq!( + outcomes[2], 2, + "MeasureLeaked on leaked qubit should return 2" + ); +} + +#[test] +fn test_measure_leaked_preserves_quantum_state() { + // Verify that MeasureLeaked correctly measures quantum states when no leakage + let engine = Box::new(StateVecEngine::new(2)); + let mut system = QuantumSystem::new_without_noise(engine); + + // Create Bell state + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + builder.add_h(&[0]); + builder.add_cx(&[0], &[1]); + builder.add_measure_leakages(&[0, 1]); + + let circuit = builder.build(); + + // Run multiple times to check correlation + let mut same_results = 0; + let runs = 100; + + for _ in 0..runs { + system.reset().unwrap(); + let result = system.process(circuit.clone()).unwrap(); + let outcomes = result.outcomes().unwrap(); + + assert_eq!(outcomes.len(), 2); + assert!( + outcomes[0] <= 1 && outcomes[1] <= 1, + "No leakage should occur" + ); + + if outcomes[0] == outcomes[1] { + same_results += 1; + } + } + + // Bell state should have perfect correlation + assert_eq!( + same_results, runs, + "Bell state measurements should be perfectly correlated" + ); +} + +#[test] +fn test_measure_leaked_sequential_measurements() { + // Test that leaked state persists across multiple measurements + let mut noise_model = GeneralNoiseModel::builder() + .with_prep_probability(0.0) + .with_meas_0_probability(0.0) + .with_meas_1_probability(0.0) + .with_p1_probability(0.0) + .with_p2_probability(0.0) + .with_seed(42) + .build(); + + // Manually mark qubit as leaked + noise_model.mark_as_leaked(0); + + let engine = Box::new(StateVecEngine::new(1)); + let mut system = QuantumSystem::new(Box::new(noise_model), engine); + + // First circuit: measure the leaked qubit + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + builder.add_measure_leakages(&[0]); + + let circuit1 = builder.build(); + let result1 = system.process(circuit1).unwrap(); + let outcomes1 = result1.outcomes().unwrap(); + + assert_eq!( + outcomes1[0], 2, + "First MeasureLeaked should return 2 for leaked qubit" + ); + + // Second circuit: measure the same qubit again (should still be leaked) + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + builder.add_measure_leakages(&[0]); + + let circuit2 = builder.build(); + let result2 = system.process(circuit2).unwrap(); + let outcomes2 = result2.outcomes().unwrap(); + + assert_eq!( + outcomes2[0], 2, + "Second MeasureLeaked should still return 2" + ); + + // Third circuit: regular measurement (should return 1) + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + builder.add_measurements(&[0]); + + let circuit3 = builder.build(); + let result3 = system.process(circuit3).unwrap(); + let outcomes3 = result3.outcomes().unwrap(); + + assert_eq!( + outcomes3[0], 1, + "Regular Measure should return 1 for leaked qubit" + ); +} + +#[test] +fn test_measure_leaked_with_prep_unleaks() { + // Test that Prep operation unleaks qubits + let mut noise_model = GeneralNoiseModel::builder() + .with_prep_probability(0.0) + .with_meas_0_probability(0.0) + .with_meas_1_probability(0.0) + .with_p1_probability(0.0) + .with_p2_probability(0.0) + .with_seed(42) + .build(); + + // Manually mark qubit as leaked + noise_model.mark_as_leaked(0); + + let engine = Box::new(StateVecEngine::new(1)); + let mut system = QuantumSystem::new(Box::new(noise_model), engine); + + // First circuit: measure the leaked qubit + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + builder.add_measure_leakages(&[0]); // Should return 2 + + let circuit1 = builder.build(); + let result1 = system.process(circuit1).unwrap(); + let outcomes1 = result1.outcomes().unwrap(); + + assert_eq!(outcomes1.len(), 1); + assert_eq!(outcomes1[0], 2, "MeasureLeaked before prep should return 2"); + + // Second circuit: prep (unleak) and measure again + let mut builder = ByteMessageBuilder::new(); + let _ = builder.for_quantum_operations(); + builder.add_prep(&[0]); // Unleak the qubit + builder.add_measure_leakages(&[0]); // Should return 0 (back to |0⟩) + + let circuit2 = builder.build(); + let result2 = system.process(circuit2).unwrap(); + let outcomes2 = result2.outcomes().unwrap(); + + assert_eq!(outcomes2.len(), 1); + assert_eq!( + outcomes2[0], 0, + "MeasureLeaked after prep should return 0 (unleaked)" + ); +} diff --git a/crates/pecos-qasm/src/engine.rs b/crates/pecos-qasm/src/engine.rs index 6e0d27f41..ebf5542e3 100644 --- a/crates/pecos-qasm/src/engine.rs +++ b/crates/pecos-qasm/src/engine.rs @@ -564,8 +564,9 @@ impl QASMEngine { GateType::RZ | GateType::RZZ | GateType::R1XY | GateType::U => { self.process_parameterized_gate(gate.gate_type, &qubits, &gate.params) } - GateType::Measure => Err(PecosError::Processing( - "Measure gate should be handled by MeasureWithMapping operation".to_string(), + GateType::Measure | GateType::MeasureLeaked => Err(PecosError::Processing( + "Measure and MeasureLeaked gates should be handled by MeasureWithMapping operation" + .to_string(), )), } }