@@ -92,7 +92,7 @@ use pecos_core::QubitId;
9292use pecos_core:: errors:: PecosError ;
9393use rand_chacha:: ChaCha8Rng ;
9494use std:: any:: Any ;
95- use std:: collections:: HashSet ;
95+ use std:: collections:: { BTreeSet , HashSet } ;
9696
9797/// General noise model implementation that includes parameterized error channels for various quantum operations
9898///
@@ -172,9 +172,9 @@ pub struct GeneralNoiseModel {
172172 /// qubit states after initialization. Ranges from 0 to 1.
173173 p_prep_leak_ratio : f64 ,
174174
175- /// Probability of crosstalk during initialization operations
175+ /// Probability of crosstalk during preparation operations
176176 ///
177- /// Models the probability that an initialization operation on one qubit affects nearby qubits.
177+ /// Models the probability that a preparation operation on one qubit affects nearby qubits.
178178 /// In ion trap systems, this could represent scattered light during optical pumping affecting
179179 /// neighboring ions.
180180 p_prep_crosstalk : f64 ,
@@ -326,10 +326,23 @@ pub struct GeneralNoiseModel {
326326 /// Random number generator for stochastic noise processes
327327 rng : NoiseRng < ChaCha8Rng > ,
328328
329+ /// Set of qubits that have been prepared at any point in the program.
330+ ///
331+ /// This is so that we know which qubits exists and we can apply crosstalk
332+ /// to them. Qubits that are measured / discarded are not removed from here, since
333+ /// PECOS does not assume measurements are destructive. This should not cause a
334+ /// problem, since inactive qubits suffering error have no effect on the state,
335+ /// and active qubits should always suffer errors under this naive crosstalk model.
336+ ///
337+ /// Using a `BTreeSet` because we will iterate over the qubits and we want determinism.
338+ prepared_qubits : BTreeSet < usize > ,
339+
329340 /// Track which qubits are being measured in the current batch and their gate types
330- /// This is needed to properly handle leakage during measurements
331- /// Each entry is (`qubit_id`, `is_measure_leaked`)
332- measured_qubits : Vec < ( usize , bool ) > ,
341+ /// This is needed to properly handle leakage during measurements as well
342+ /// as crosstalk.
343+ /// TODO: manage this via result tags.
344+ /// Each entry is (`qubit_id`, `is_measure_leaked`, `is_crosstalk`)
345+ measured_qubits : Vec < ( usize , bool , bool ) > ,
333346}
334347
335348impl ControlEngine for GeneralNoiseModel {
@@ -503,21 +516,24 @@ impl GeneralNoiseModel {
503516 ) ;
504517 }
505518 GateType :: Prep => {
519+ for & q in & gate. qubits {
520+ self . prepared_qubits . insert ( usize:: from ( q) ) ;
521+ }
506522 self . apply_prep_faults ( & gate, & mut builder) ;
507-
508- // TODO: Implement prep crosstalk when needed
523+ self . apply_crosstalk_faults ( & gate, self . p_prep_crosstalk , & mut builder) ;
509524 }
510525 GateType :: Measure | GateType :: MeasureLeaked => {
511526 // Track which qubits are being measured for leakage handling
512527 let is_measure_leaked = gate. gate_type == GateType :: MeasureLeaked ;
513528 self . measured_qubits . extend (
514529 gate. qubits
515530 . iter ( )
516- . map ( |q| ( usize:: from ( * q) , is_measure_leaked) ) ,
531+ . map ( |q| ( usize:: from ( * q) , is_measure_leaked, false ) ) ,
517532 ) ;
518533 // Measurement noise is handled in apply_noise_on_continue_processing
519534 // We still need to add the original gate here
520535 builder. add_gate_command ( & gate) ;
536+ self . apply_crosstalk_faults ( & gate, self . p_meas_crosstalk , & mut builder) ;
521537 }
522538 GateType :: I => {
523539 let err_msg = format ! (
@@ -589,15 +605,26 @@ impl GeneralNoiseModel {
589605
590606 // Check if we have leaked qubits that were measured
591607 let has_leakage = !self . leaked_qubits . is_empty ( )
592- && self . measured_qubits . iter ( ) . any ( |( q, _) | self . is_leaked ( * q) ) ;
608+ && self
609+ . measured_qubits
610+ . iter ( )
611+ . any ( |( q, _, _) | self . is_leaked ( * q) ) ;
593612
594613 for ( idx, outcome) in measurement_outcomes. into_iter ( ) . enumerate ( ) {
595614 let mut val = outcome;
596615
597- // Check if this measurement corresponds to a leaked qubit
598- if has_leakage && idx < self . measured_qubits . len ( ) {
599- let ( qubit, is_measure_leaked) = self . measured_qubits [ idx] ;
600- if self . is_leaked ( qubit) {
616+ // Check if this measurement corresponds to a leaked qubit or comes from
617+ // crosstalk
618+ if idx < self . measured_qubits . len ( ) {
619+ let ( qubit, is_measure_leaked, is_crosstalk) = self . measured_qubits [ idx] ;
620+
621+ // Check if this measurement comes from crosstalk noise. If so, ignore it.
622+ if is_crosstalk {
623+ trace ! ( "Qubit {qubit} was measured by crosstalk; outcome is ignored." ) ;
624+ continue ; // Skip this iteration
625+ }
626+
627+ if has_leakage && self . is_leaked ( qubit) {
601628 if is_measure_leaked {
602629 trace ! ( "Qubit {qubit} is leaked, MeasureLeaked returns 2" ) ;
603630 // For MeasureLeaked, return 2 for leaked qubits
@@ -783,6 +810,41 @@ impl GeneralNoiseModel {
783810 }
784811 }
785812
813+ /// Apply crosstalk noise
814+ ///
815+ /// Naive crosstalk noise model:
816+ /// 1. All qubits in the trap but the ones in the `gate` are subject to crosstalk
817+ // error. The `gate` should be either qubit measurement or preparation.
818+ // 2. *Each* qubit not in `gate` has the given `probability` to suffer an error.
819+ /// 3. Affected qubits are collapsed into the computational basis (Z measurement).
820+ ///
821+ /// In ion trap systems, this could represent scattered light during optical pumping
822+ /// affecting neighboring ions.
823+ pub fn apply_crosstalk_faults (
824+ & mut self ,
825+ gate : & Gate ,
826+ probability : f64 ,
827+ builder : & mut ByteMessageBuilder ,
828+ ) {
829+ let mut affected_qubits = Vec :: new ( ) ;
830+ let gate_qubits: Vec < usize > = gate. qubits . iter ( ) . map ( |q| usize:: from ( * q) ) . collect ( ) ;
831+
832+ for q in self . prepared_qubits . clone ( ) {
833+ if !gate_qubits. contains ( & q) && self . rng . occurs ( probability) {
834+ affected_qubits. push ( q) ;
835+ trace ! ( "Qubit {q} affected by crosstalk error" ) ;
836+ }
837+ }
838+
839+ builder. add_measurements ( & affected_qubits) ;
840+ // We need to mark these measurements as being introduced by crosstalk rather
841+ // than the user's program so that we can discard the results in
842+ // apply_noise_on_continue_processing.
843+ self . measured_qubits . extend (
844+ affected_qubits. iter ( ) . map ( |& q| ( q, false , true ) ) , // (qubit, is_measure_leaked, is_crosstalk)
845+ ) ;
846+ }
847+
786848 /// Apply single-qubit gate noise faults
787849 ///
788850 /// Models errors that occur during single-qubit gate operations:
@@ -2225,6 +2287,132 @@ mod tests {
22252287 ) ;
22262288 }
22272289
2290+ #[ test]
2291+ fn test_prep_crosstalk ( ) {
2292+ use crate :: byte_message:: ByteMessageBuilder ;
2293+
2294+ let mut model = GeneralNoiseModel :: builder ( )
2295+ . with_p_prep_crosstalk ( 1.0 )
2296+ . with_seed ( 42 )
2297+ . build ( ) ;
2298+ let noise = model
2299+ . as_any_mut ( )
2300+ . downcast_mut :: < GeneralNoiseModel > ( )
2301+ . unwrap ( ) ;
2302+
2303+ let mut builder = ByteMessageBuilder :: new ( ) ;
2304+ let _ = builder. for_quantum_operations ( ) ;
2305+ // Prepare a bunch of |0> states
2306+ builder. add_prep ( & [ 0 , 1 , 2 , 3 , 4 ] ) ;
2307+ // Apply mid-circuit measurement and reset
2308+ builder. add_measurements ( & [ 2 ] ) ;
2309+ builder. add_prep ( & [ 2 ] ) ;
2310+ let _cmd = noise. apply_noise_on_start ( & builder. build ( ) ) . unwrap ( ) ;
2311+
2312+ assert_eq ! (
2313+ noise. measured_qubits. len( ) ,
2314+ 5 ,
2315+ "There should be 5 measured qubits: one from MCMR and the others from
2316+ crosstalk got: {:?}" ,
2317+ noise. measured_qubits
2318+ ) ;
2319+
2320+ let ( q, _, is_crosstalk) = noise. measured_qubits [ 0 ] ;
2321+ assert_eq ! ( q, 2 , "The first measurement should be the MCMR on qubit 2" ) ;
2322+ assert ! ( !is_crosstalk, "The first measurement should come from MCMR" ) ;
2323+
2324+ for ( _, _, is_crosstalk) in & noise. measured_qubits [ 1 ..] {
2325+ assert ! (
2326+ is_crosstalk,
2327+ "The other measurements should come from crosstalk"
2328+ ) ;
2329+ }
2330+
2331+ // All results are 0
2332+ let mut outcome_builder = ByteMessageBuilder :: new ( ) ;
2333+ let _ = outcome_builder. for_outcomes ( ) ;
2334+ outcome_builder. add_outcomes ( & [ 0 , 0 , 0 , 0 , 0 ] ) ;
2335+
2336+ let mcmr = noise
2337+ . apply_noise_on_continue_processing ( outcome_builder. build ( ) )
2338+ . unwrap ( ) ;
2339+ let results = mcmr. outcomes ( ) . unwrap ( ) ;
2340+
2341+ assert_eq ! (
2342+ noise. measured_qubits. len( ) ,
2343+ 0 ,
2344+ "The list of measured_qubits should have been cleared."
2345+ ) ;
2346+ assert_eq ! (
2347+ results. len( ) ,
2348+ 1 ,
2349+ "There should only be one outcome: that of the mid-circ measurement"
2350+ ) ;
2351+ }
2352+
2353+ #[ test]
2354+ fn test_meas_crosstalk ( ) {
2355+ use crate :: byte_message:: ByteMessageBuilder ;
2356+
2357+ let mut model = GeneralNoiseModel :: builder ( )
2358+ . with_p_meas_crosstalk ( 1.0 )
2359+ . with_seed ( 42 )
2360+ . build ( ) ;
2361+ let noise = model
2362+ . as_any_mut ( )
2363+ . downcast_mut :: < GeneralNoiseModel > ( )
2364+ . unwrap ( ) ;
2365+
2366+ let mut builder = ByteMessageBuilder :: new ( ) ;
2367+ let _ = builder. for_quantum_operations ( ) ;
2368+ // Prepare a bunch of |0> states
2369+ builder. add_prep ( & [ 0 , 1 , 2 , 3 , 4 ] ) ;
2370+ // Apply mid-circuit measurement and reset
2371+ builder. add_measurements ( & [ 2 ] ) ;
2372+ builder. add_prep ( & [ 2 ] ) ;
2373+ let _cmd = noise. apply_noise_on_start ( & builder. build ( ) ) . unwrap ( ) ;
2374+
2375+ assert_eq ! (
2376+ noise. measured_qubits. len( ) ,
2377+ 5 ,
2378+ "There should be 5 measured qubits: one from MCMR and the others from
2379+ crosstalk got: {:?}" ,
2380+ noise. measured_qubits
2381+ ) ;
2382+
2383+ let ( q, _, is_crosstalk) = noise. measured_qubits [ 0 ] ;
2384+ assert_eq ! ( q, 2 , "The first measurement should be the MCMR on qubit 2" ) ;
2385+ assert ! ( !is_crosstalk, "The first measurement should come from MCMR" ) ;
2386+
2387+ for ( _, _, is_crosstalk) in & noise. measured_qubits [ 1 ..] {
2388+ assert ! (
2389+ is_crosstalk,
2390+ "The other measurements should come from crosstalk"
2391+ ) ;
2392+ }
2393+
2394+ // All results are 0
2395+ let mut outcome_builder = ByteMessageBuilder :: new ( ) ;
2396+ let _ = outcome_builder. for_outcomes ( ) ;
2397+ outcome_builder. add_outcomes ( & [ 0 , 0 , 0 , 0 , 0 ] ) ;
2398+
2399+ let mcmr = noise
2400+ . apply_noise_on_continue_processing ( outcome_builder. build ( ) )
2401+ . unwrap ( ) ;
2402+ let results = mcmr. outcomes ( ) . unwrap ( ) ;
2403+
2404+ assert_eq ! (
2405+ noise. measured_qubits. len( ) ,
2406+ 0 ,
2407+ "The list of measured_qubits should have been cleared."
2408+ ) ;
2409+ assert_eq ! (
2410+ results. len( ) ,
2411+ 1 ,
2412+ "There should only be one outcome: that of the mid-circ measurement"
2413+ ) ;
2414+ }
2415+
22282416 #[ test]
22292417 fn test_parameter_scaling ( ) {
22302418 // Test that scaling factors are applied correctly - use builder pattern
0 commit comments