1use glam::IVec2;
31use rustc_hash::{FxHashMap, FxHashSet};
32use smallvec::SmallVec;
33use thiserror::Error;
34
35use crate::{GateType, Op, Pauli, PauliBasis};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct RegionTerm {
40 pub qubit: IVec2,
42 pub pauli: PauliBasis,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct SliceDetector {
50 pub id: u32,
52 pub measurements: Vec<u32>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct SliceRegionSeed {
64 pub id: u32,
66 pub boundary: usize,
68 pub terms: Vec<RegionTerm>,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum RegionBreakKind {
75 AnticommutesWithReset,
77 AnticommutesWithMeasurement,
79 AnticommutesWithStart,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct RegionBreak {
86 pub detector: u32,
88 pub segment: usize,
90 pub qubit: IVec2,
92 pub kind: RegionBreakKind,
94}
95
96pub type DetectorRegion = (u32, Vec<RegionTerm>);
98
99#[derive(Debug, Clone, Default, PartialEq, Eq)]
101pub struct DetectorSlices {
102 pub slices: Vec<Vec<DetectorRegion>>,
106 pub breaks: Vec<RegionBreak>,
108}
109
110#[derive(Debug, Error, Clone, PartialEq, Eq)]
112pub enum DetsliceError {
113 #[error(
115 "detector-slice tracking requires a straight-line circuit; flatten REPEAT blocks first"
116 )]
117 RepeatNotFlattened,
118 #[error(
119 "region seed boundary {boundary} is outside the {segments}-segment circuit (maximum {segments})"
120 )]
121 SeedBoundaryOutOfRange {
123 boundary: usize,
125 segments: usize,
127 },
128}
129pub fn detector_slices_with_seeds(
140 segments: &[&[Op]],
141 detectors: &[SliceDetector],
142 seeds: &[SliceRegionSeed],
143) -> Result<DetectorSlices, DetsliceError> {
144 if segments
145 .iter()
146 .flat_map(|segment| segment.iter())
147 .any(|op| matches!(op, Op::Repeat { .. }))
148 {
149 return Err(DetsliceError::RepeatNotFlattened);
150 }
151
152 let mut seeds_by_boundary = vec![Vec::new(); segments.len() + 1];
153 for seed in seeds {
154 let Some(slot) = seeds_by_boundary.get_mut(seed.boundary) else {
155 return Err(DetsliceError::SeedBoundaryOutOfRange {
156 boundary: seed.boundary,
157 segments: segments.len(),
158 });
159 };
160 slot.push(seed);
161 }
162
163 let mut tracker = Tracker::default();
164 for detector in detectors {
165 for &measurement in &detector.measurements {
166 tracker
167 .rec_bits
168 .entry(measurement)
169 .or_default()
170 .xor_id(detector.id);
171 }
172 }
173
174 let mut slices = vec![Vec::new(); segments.len()];
175 for k in (0..segments.len()).rev() {
176 for seed in &seeds_by_boundary[k + 1] {
177 tracker.xor_region(seed.id, &seed.terms);
178 }
179 slices[k] = tracker.snapshot();
180 for op in segments[k].iter().rev() {
181 tracker.undo_op(op);
182 }
183 tracker.process_anticommutations(k);
184 }
185 for seed in &seeds_by_boundary[0] {
186 tracker.xor_region(seed.id, &seed.terms);
187 }
188 tracker.undo_start_of_circuit();
189
190 let mut breaks = tracker.broken;
191 breaks.sort_by_key(|brk| (brk.segment, brk.detector, brk.qubit.x, brk.qubit.y));
192 Ok(DetectorSlices { slices, breaks })
193}
194
195#[derive(Debug, Clone, Default, PartialEq, Eq)]
201struct DetSet(SmallVec<[u32; 4]>);
202
203impl DetSet {
204 fn is_empty(&self) -> bool {
205 self.0.is_empty()
206 }
207
208 fn contains(&self, id: u32) -> bool {
209 self.0.binary_search(&id).is_ok()
210 }
211
212 fn iter(&self) -> impl Iterator<Item = u32> + '_ {
213 self.0.iter().copied()
214 }
215
216 fn xor_id(&mut self, id: u32) {
218 match self.0.binary_search(&id) {
219 Ok(index) => {
220 self.0.remove(index);
221 }
222 Err(index) => self.0.insert(index, id),
223 }
224 }
225
226 fn remove(&mut self, id: u32) {
228 if let Ok(index) = self.0.binary_search(&id) {
229 self.0.remove(index);
230 }
231 }
232
233 fn xor_merge(&mut self, other: &DetSet) {
235 if other.is_empty() {
236 return;
237 }
238 let mut merged = SmallVec::with_capacity(self.0.len() + other.0.len());
239 let (mut i, mut j) = (0, 0);
240 let (a, b) = (&self.0, &other.0);
241 while i < a.len() && j < b.len() {
242 match a[i].cmp(&b[j]) {
243 std::cmp::Ordering::Less => {
244 merged.push(a[i]);
245 i += 1;
246 }
247 std::cmp::Ordering::Greater => {
248 merged.push(b[j]);
249 j += 1;
250 }
251 std::cmp::Ordering::Equal => {
252 i += 1;
253 j += 1;
254 }
255 }
256 }
257 merged.extend_from_slice(&a[i..]);
258 merged.extend_from_slice(&b[j..]);
259 self.0 = merged;
260 }
261}
262
263fn symmetric_difference(a: &DetSet, b: &DetSet) -> SmallVec<[u32; 4]> {
266 let mut out = a.clone();
267 out.xor_merge(b);
268 out.0
269}
270
271struct Pending {
274 detector: u32,
275 qubit: IVec2,
276 kind: RegionBreakKind,
277}
278
279#[derive(Default)]
280struct Tracker {
281 xs: FxHashMap<IVec2, DetSet>,
283 zs: FxHashMap<IVec2, DetSet>,
285 rec_bits: FxHashMap<u32, DetSet>,
287 pending: Vec<Pending>,
289 broken: Vec<RegionBreak>,
290}
291
292impl Tracker {
293 fn take_frame(&mut self, qubit: IVec2) -> (DetSet, DetSet) {
296 (
297 self.xs.remove(&qubit).unwrap_or_default(),
298 self.zs.remove(&qubit).unwrap_or_default(),
299 )
300 }
301
302 fn put_frame(&mut self, qubit: IVec2, x: DetSet, z: DetSet) {
305 if x.is_empty() {
306 self.xs.remove(&qubit);
307 } else {
308 self.xs.insert(qubit, x);
309 }
310 if z.is_empty() {
311 self.zs.remove(&qubit);
312 } else {
313 self.zs.insert(qubit, z);
314 }
315 }
316
317 fn record_gauge(
318 &mut self,
319 detectors: impl IntoIterator<Item = u32>,
320 qubit: IVec2,
321 kind: RegionBreakKind,
322 ) {
323 for detector in detectors {
324 self.pending.push(Pending {
325 detector,
326 qubit,
327 kind,
328 });
329 }
330 }
331
332 fn xor_region(&mut self, id: u32, terms: &[RegionTerm]) {
334 for term in terms {
335 let (mut x, mut z) = self.take_frame(term.qubit);
336 match term.pauli {
337 PauliBasis::X => x.xor_id(id),
338 PauliBasis::Y => {
339 x.xor_id(id);
340 z.xor_id(id);
341 }
342 PauliBasis::Z => z.xor_id(id),
343 }
344 self.put_frame(term.qubit, x, z);
345 }
346 }
347
348 fn undo_op(&mut self, op: &Op) {
349 match op {
350 Op::Gate { gate, qubits } => self.undo_gate(*gate, qubits),
351 Op::Measure {
352 basis,
353 qubits,
354 measurements,
355 ..
356 } => self.undo_measure(*basis, qubits, measurements),
357 Op::MPP {
358 products,
359 measurements,
360 } => self.undo_mpp(products, measurements),
361 Op::ConditionalPauli(corrections) => {
362 for correction in corrections {
363 self.undo_conditional_pauli(
364 correction.target,
365 correction.pauli,
366 correction.control,
367 );
368 }
369 }
370 Op::Tick
372 | Op::Depolarize1 { .. }
373 | Op::Depolarize2 { .. }
374 | Op::PauliError { .. }
375 | Op::Repeat { .. } => {}
376 }
377 }
378
379 fn undo_conditional_pauli(&mut self, target: IVec2, pauli: PauliBasis, control: u32) {
383 let x = self.xs.get(&target).cloned().unwrap_or_default();
384 let z = self.zs.get(&target).cloned().unwrap_or_default();
385 let anticommuting = match pauli {
386 PauliBasis::X => z,
387 PauliBasis::Z => x,
388 PauliBasis::Y => DetSet(symmetric_difference(&x, &z)),
389 };
390 if anticommuting.is_empty() {
391 return;
392 }
393 self.rec_bits
394 .entry(control)
395 .or_default()
396 .xor_merge(&anticommuting);
397 }
398
399 fn undo_gate(&mut self, gate: GateType, qubits: &[IVec2]) {
400 if gate.is_reset() {
401 for &q in qubits {
402 self.undo_reset(gate, q);
403 }
404 } else if gate.is_two_qubit_gate() {
405 for pair in qubits.as_chunks::<2>().0.iter().rev() {
406 self.undo_two_qubit(gate, pair[0], pair[1]);
407 }
408 } else {
409 for &q in qubits {
415 self.undo_single_clifford(gate.clifford_proxy(), q);
416 }
417 }
418 }
419
420 fn undo_reset(&mut self, gate: GateType, q: IVec2) {
423 let (x, z) = self.take_frame(q);
424 match gate {
425 GateType::RZ => self.record_gauge(x.iter(), q, RegionBreakKind::AnticommutesWithReset),
427 GateType::RX => self.record_gauge(z.iter(), q, RegionBreakKind::AnticommutesWithReset),
429 GateType::RY => self.record_gauge(
431 symmetric_difference(&x, &z),
432 q,
433 RegionBreakKind::AnticommutesWithReset,
434 ),
435 _ => unreachable!("undo_reset only sees reset gates"),
436 }
437 }
439
440 fn undo_measure(&mut self, basis: crate::PauliBasis, qubits: &[IVec2], measurements: &[u32]) {
444 use crate::PauliBasis::{X, Y, Z};
445 for (&q, &m) in qubits.iter().zip(measurements) {
446 let (mut x, mut z) = self.take_frame(q);
447 match basis {
448 Z => self.record_gauge(x.iter(), q, RegionBreakKind::AnticommutesWithMeasurement),
449 X => self.record_gauge(z.iter(), q, RegionBreakKind::AnticommutesWithMeasurement),
450 Y => self.record_gauge(
451 symmetric_difference(&x, &z),
452 q,
453 RegionBreakKind::AnticommutesWithMeasurement,
454 ),
455 }
456 if let Some(dets) = self.rec_bits.remove(&m) {
457 match basis {
458 Z => z.xor_merge(&dets),
459 X => x.xor_merge(&dets),
460 Y => {
461 x.xor_merge(&dets);
462 z.xor_merge(&dets);
463 }
464 }
465 }
466 self.put_frame(q, x, z);
467 }
468 }
469
470 fn undo_mpp(&mut self, products: &[crate::PauliMap], measurements: &[u32]) {
473 for (product, &m) in products.iter().zip(measurements).rev() {
474 self.gauge_check_product(product);
475 if let Some(dets) = self.rec_bits.remove(&m) {
476 for (&q, &pauli) in product {
477 let (mut x, mut z) = self.take_frame(q);
478 if pauli & Pauli::X {
479 x.xor_merge(&dets);
480 }
481 if pauli & Pauli::Z {
482 z.xor_merge(&dets);
483 }
484 self.put_frame(q, x, z);
485 }
486 }
487 }
488 }
489
490 fn gauge_check_product(&mut self, product: &crate::PauliMap) {
494 let mut candidates: FxHashSet<u32> = FxHashSet::default();
495 for (&q, _) in product {
496 if let Some(set) = self.xs.get(&q) {
497 candidates.extend(set.iter());
498 }
499 if let Some(set) = self.zs.get(&q) {
500 candidates.extend(set.iter());
501 }
502 }
503 let qubit = product.representative_coord().unwrap_or(IVec2::ZERO);
504 let mut breaks = Vec::new();
505 for detector in candidates {
506 let mut parity = false;
507 for (&q, &pauli) in product {
508 let has_x = self.xs.get(&q).is_some_and(|s| s.contains(detector));
509 let has_z = self.zs.get(&q).is_some_and(|s| s.contains(detector));
510 parity ^= (has_x && pauli & Pauli::Z) ^ (has_z && pauli & Pauli::X);
512 }
513 if parity {
514 breaks.push(detector);
515 }
516 }
517 self.record_gauge(breaks, qubit, RegionBreakKind::AnticommutesWithMeasurement);
518 }
519
520 fn undo_single_clifford(&mut self, gate: GateType, q: IVec2) {
525 use GateType::*;
526 let (mut x, mut z) = self.take_frame(q);
527 match gate {
528 H | H_NXZ | SQRT_Y | SQRT_Y_DAG => std::mem::swap(&mut x, &mut z),
530 H_YZ | H_NYZ | SQRT_X | SQRT_X_DAG => x.xor_merge(&z),
532 H_XY | H_NXY | S | S_DAG => z.xor_merge(&x),
534 C_XYZ | C_NXYZ | C_XNYZ | C_XYNZ => {
536 z.xor_merge(&x);
537 x.xor_merge(&z);
538 }
539 C_ZYX | C_NZYX | C_ZNYX | C_ZYNX => {
541 x.xor_merge(&z);
542 z.xor_merge(&x);
543 }
544 I | X | Y | Z => {}
546 _ => unreachable!("undo_single_clifford only sees single-qubit Cliffords"),
547 }
548 self.put_frame(q, x, z);
549 }
550
551 fn undo_two_qubit(&mut self, gate: GateType, qa: IVec2, qb: IVec2) {
556 use GateType::*;
557 let (mut xa, mut za) = self.take_frame(qa);
558 let (mut xb, mut zb) = self.take_frame(qb);
559 match gate {
560 XCX => {
561 xa.xor_merge(&zb);
562 xb.xor_merge(&za);
563 }
564 XCY => {
565 xa.xor_merge(&xb);
566 xa.xor_merge(&zb);
567 xb.xor_merge(&za);
568 zb.xor_merge(&za);
569 }
570 XCZ => {
571 zb.xor_merge(&za);
572 xa.xor_merge(&xb);
573 }
574 YCX => {
575 xb.xor_merge(&xa);
576 xb.xor_merge(&za);
577 xa.xor_merge(&zb);
578 za.xor_merge(&zb);
579 }
580 YCY => {
581 za.xor_merge(&xb);
582 za.xor_merge(&zb);
583 xa.xor_merge(&xb);
584 xa.xor_merge(&zb);
585 zb.xor_merge(&xa);
586 zb.xor_merge(&za);
587 xb.xor_merge(&xa);
588 xb.xor_merge(&za);
589 }
590 YCZ => {
591 zb.xor_merge(&za);
592 zb.xor_merge(&xa);
593 xa.xor_merge(&xb);
594 za.xor_merge(&xb);
595 }
596 CX => {
597 za.xor_merge(&zb);
598 xb.xor_merge(&xa);
599 }
600 CY => {
601 za.xor_merge(&zb);
602 za.xor_merge(&xb);
603 xb.xor_merge(&xa);
604 zb.xor_merge(&xa);
605 }
606 CZ => {
607 za.xor_merge(&xb);
608 zb.xor_merge(&xa);
609 }
610 _ => unreachable!("undo_two_qubit only sees controlled gates"),
611 }
612 self.put_frame(qa, xa, za);
613 self.put_frame(qb, xb, zb);
614 }
615
616 fn undo_start_of_circuit(&mut self) {
619 let breaks: Vec<(u32, IVec2)> = self
620 .xs
621 .iter()
622 .flat_map(|(&q, set)| set.iter().map(move |d| (d, q)))
623 .collect();
624 for (detector, qubit) in breaks {
625 self.pending.push(Pending {
626 detector,
627 qubit,
628 kind: RegionBreakKind::AnticommutesWithStart,
629 });
630 }
631 self.process_anticommutations(0);
632 }
633
634 fn process_anticommutations(&mut self, segment: usize) {
640 if self.pending.is_empty() {
641 return;
642 }
643 let mut pending = std::mem::take(&mut self.pending);
644 pending.sort_by_key(|p| (p.detector, p.qubit.x, p.qubit.y));
645
646 let mut stripped: FxHashSet<u32> = FxHashSet::default();
647 let mut last: Option<(u32, IVec2)> = None;
648 for p in pending {
649 if last == Some((p.detector, p.qubit)) {
652 continue;
653 }
654 last = Some((p.detector, p.qubit));
655 self.broken.push(RegionBreak {
656 detector: p.detector,
657 segment,
658 qubit: p.qubit,
659 kind: p.kind,
660 });
661 if stripped.insert(p.detector) {
662 self.strip_detector(p.detector);
663 }
664 }
665 }
666
667 fn strip_detector(&mut self, detector: u32) {
668 self.xs.retain(|_, set| {
669 set.remove(detector);
670 !set.is_empty()
671 });
672 self.zs.retain(|_, set| {
673 set.remove(detector);
674 !set.is_empty()
675 });
676 }
677
678 fn snapshot(&self) -> Vec<DetectorRegion> {
681 let mut per_detector: FxHashMap<u32, Vec<RegionTerm>> = FxHashMap::default();
682 for (&qubit, xset) in &self.xs {
683 let zset = self.zs.get(&qubit);
684 for detector in xset.iter() {
685 per_detector.entry(detector).or_default().push(RegionTerm {
686 qubit,
687 pauli: if zset.is_some_and(|set| set.contains(detector)) {
688 PauliBasis::Y
689 } else {
690 PauliBasis::X
691 },
692 });
693 }
694 }
695 for (&qubit, zset) in &self.zs {
696 let xset = self.xs.get(&qubit);
697 for detector in zset
698 .iter()
699 .filter(|&detector| xset.is_none_or(|set| !set.contains(detector)))
700 {
701 per_detector.entry(detector).or_default().push(RegionTerm {
702 qubit,
703 pauli: PauliBasis::Z,
704 });
705 }
706 }
707
708 let mut out: Vec<DetectorRegion> = per_detector.into_iter().collect();
709 for (_, terms) in &mut out {
710 terms.sort_by_key(|term| (term.qubit.x, term.qubit.y));
711 }
712 out.sort_by_key(|(detector, _)| *detector);
713 out
714 }
715}
716
717#[cfg(test)]
718mod tests {
719 use super::*;
720 use glam::ivec2;
721
722 fn gate(gate: GateType, qubits: &[IVec2]) -> Op {
723 Op::Gate {
724 gate,
725 qubits: qubits.to_vec(),
726 }
727 }
728
729 fn measure(basis: PauliBasis, qubit: IVec2, id: u32) -> Op {
730 Op::Measure {
731 basis,
732 qubits: vec![qubit],
733 measurements: vec![id],
734 flip_probability: 0.0,
735 }
736 }
737
738 fn det(id: u32, measurements: &[u32]) -> SliceDetector {
739 SliceDetector {
740 id,
741 measurements: measurements.to_vec(),
742 }
743 }
744
745 fn render(slice: &[DetectorRegion]) -> String {
747 slice
748 .iter()
749 .map(|(id, terms)| {
750 let body: String = terms
751 .iter()
752 .map(|t| format!("{:?}({},{})", t.pauli, t.qubit.x, t.qubit.y))
753 .collect();
754 format!("d{id}:{body}")
755 })
756 .collect::<Vec<_>>()
757 .join(" ")
758 }
759
760 fn run(segments: &[Vec<Op>], detectors: &[SliceDetector]) -> DetectorSlices {
761 let refs: Vec<&[Op]> = segments.iter().map(Vec::as_slice).collect();
762 detector_slices_with_seeds(&refs, detectors, &[]).expect("straight-line input tracks")
763 }
764
765 #[test]
766 fn reversing_batched_operations_matches_separate_instructions() {
767 let [a, b, c] = [ivec2(0, 0), ivec2(1, 0), ivec2(2, 0)];
768 let batched = [gate(GateType::CX, &[a, b, b, c])];
769 let split = [gate(GateType::CX, &[a, b]), gate(GateType::CX, &[b, c])];
770 let seeds = [SliceRegionSeed {
771 id: 0,
772 boundary: 2,
773 terms: vec![RegionTerm {
774 qubit: c,
775 pauli: PauliBasis::Z,
776 }],
777 }];
778 let batched = detector_slices_with_seeds(&[&[], &batched], &[], &seeds).unwrap();
779 let split = detector_slices_with_seeds(&[&[], &split], &[], &seeds).unwrap();
780 assert_eq!(batched, split);
781 assert_eq!(render(&batched.slices[0]), "d0:Z(0,0)Z(1,0)Z(2,0)");
782
783 let products = [
784 crate::PauliMap::from_unique_entries([(a, Pauli::X)]),
785 crate::PauliMap::from_unique_entries([(a, Pauli::Z)]),
786 ];
787 let batched = vec![Op::MPP {
788 products: products.to_vec(),
789 measurements: vec![0, 1],
790 }];
791 let split = products
792 .into_iter()
793 .enumerate()
794 .map(|(m, product)| Op::MPP {
795 products: vec![product],
796 measurements: vec![m as u32],
797 })
798 .collect();
799 let batched = run(&[batched], &[det(0, &[1])]);
800 let split = run(&[split], &[det(0, &[1])]);
801 assert_eq!(batched, split);
802 assert!(
803 !batched.breaks.is_empty(),
804 "MPP X0 Z0 leaves the final Z outcome random"
805 );
806 }
807
808 #[test]
809 fn single_round_z_memory_holds_region_until_measurement() {
810 let q = ivec2(0, 0);
811 let segments = vec![
812 vec![gate(GateType::RZ, &[q])],
813 vec![Op::Tick],
814 vec![measure(PauliBasis::Z, q, 0)],
815 ];
816 let slices = run(&segments, &[det(9, &[0])]);
817 assert_eq!(render(&slices.slices[0]), "d9:Z(0,0)");
818 assert_eq!(render(&slices.slices[1]), "d9:Z(0,0)");
819 assert_eq!(render(&slices.slices[2]), "");
820 assert!(slices.breaks.is_empty());
821 }
822
823 #[test]
824 fn two_round_memory_region_lives_only_between_measurements() {
825 let q = ivec2(0, 0);
826 let segments = vec![
827 vec![gate(GateType::RZ, &[q])],
828 vec![measure(PauliBasis::Z, q, 0)],
829 vec![measure(PauliBasis::Z, q, 1)],
830 ];
831 let slices = run(&segments, &[det(1, &[0, 1])]);
833 assert_eq!(render(&slices.slices[0]), "");
834 assert_eq!(render(&slices.slices[1]), "d1:Z(0,0)");
835 assert_eq!(render(&slices.slices[2]), "");
836 assert!(slices.breaks.is_empty());
837 }
838
839 #[test]
840 fn hadamard_swaps_region_basis_across_the_gate() {
841 let q = ivec2(0, 0);
842 let segments = vec![
843 vec![gate(GateType::RZ, &[q])],
844 vec![gate(GateType::H, &[q])],
845 vec![measure(PauliBasis::X, q, 0)],
846 ];
847 let slices = run(&segments, &[det(0, &[0])]);
848 assert_eq!(render(&slices.slices[0]), "d0:Z(0,0)");
850 assert_eq!(render(&slices.slices[1]), "d0:X(0,0)");
851 assert_eq!(render(&slices.slices[2]), "");
852 assert!(slices.breaks.is_empty());
853 }
854
855 #[test]
856 fn positioned_pauli_seed_enters_at_its_exact_boundary() {
857 let q = ivec2(0, 0);
858 let segments = [vec![gate(GateType::H, &[q])], vec![gate(GateType::I, &[q])]];
859 let refs: Vec<&[Op]> = segments.iter().map(Vec::as_slice).collect();
860 let seed = SliceRegionSeed {
861 id: 4,
862 boundary: 1,
863 terms: vec![RegionTerm {
864 qubit: q,
865 pauli: PauliBasis::Z,
866 }],
867 };
868
869 let slices = detector_slices_with_seeds(&refs, &[], &[seed]).unwrap();
870
871 assert_eq!(render(&slices.slices[0]), "d4:Z(0,0)");
872 assert_eq!(render(&slices.slices[1]), "");
873 assert_eq!(
874 slices.breaks,
875 vec![RegionBreak {
876 detector: 4,
877 segment: 0,
878 qubit: q,
879 kind: RegionBreakKind::AnticommutesWithStart,
880 }]
881 );
882 }
883
884 #[test]
885 fn cx_plaquette_region_grows_across_coupled_data_qubits() {
886 let a = ivec2(1, 0);
887 let d0 = ivec2(0, 0);
888 let d1 = ivec2(2, 0);
889 let segments = vec![
890 vec![gate(GateType::RZ, &[a])],
891 vec![gate(GateType::CX, &[d0, a])],
892 vec![gate(GateType::CX, &[d1, a])],
893 vec![measure(PauliBasis::Z, a, 0)],
894 ];
895 let slices = run(&segments, &[det(0, &[0])]);
896 assert_eq!(render(&slices.slices[3]), "");
897 assert_eq!(render(&slices.slices[2]), "d0:Z(1,0)");
898 assert_eq!(render(&slices.slices[1]), "d0:Z(1,0)Z(2,0)");
899 assert_eq!(render(&slices.slices[0]), "d0:Z(0,0)Z(1,0)Z(2,0)");
900 assert!(slices.breaks.is_empty());
901 }
902
903 #[test]
904 fn phase_gate_produces_a_y_region() {
905 let q = ivec2(0, 0);
906 let segments = vec![
908 vec![gate(GateType::RY, &[q])],
909 vec![gate(GateType::S, &[q])],
910 vec![measure(PauliBasis::X, q, 0)],
911 ];
912 let slices = run(&segments, &[det(0, &[0])]);
913 assert_eq!(render(&slices.slices[0]), "d0:Y(0,0)");
914 assert_eq!(render(&slices.slices[1]), "d0:X(0,0)");
915 assert_eq!(render(&slices.slices[2]), "");
916 assert!(slices.breaks.is_empty());
917 }
918
919 #[test]
920 fn segment_ops_are_undone_in_reverse_order() {
921 let q = ivec2(0, 0);
922 let segments = vec![
923 vec![gate(GateType::H, &[q]), gate(GateType::S, &[q])],
924 vec![measure(PauliBasis::X, q, 0)],
925 ];
926 let slices = run(&segments, &[det(0, &[0])]);
927
928 assert_eq!(
929 slices.breaks,
930 vec![RegionBreak {
931 detector: 0,
932 segment: 0,
933 qubit: q,
934 kind: RegionBreakKind::AnticommutesWithStart,
935 }]
936 );
937 }
938
939 #[test]
940 fn mpp_product_region_is_the_product_support() {
941 let q0 = ivec2(0, 0);
942 let q1 = ivec2(1, 0);
943 let product = crate::PauliMap::from_unique_entries([(q0, Pauli::X), (q1, Pauli::X)]);
944 let segments = vec![
945 vec![gate(GateType::RX, &[q0]), gate(GateType::RX, &[q1])],
946 vec![Op::MPP {
947 products: vec![product],
948 measurements: vec![0],
949 }],
950 ];
951 let slices = run(&segments, &[det(0, &[0])]);
952 assert_eq!(render(&slices.slices[0]), "d0:X(0,0)X(1,0)");
953 assert_eq!(render(&slices.slices[1]), "");
954 assert!(slices.breaks.is_empty());
955 }
956
957 #[test]
958 fn x_region_anticommuting_with_z_reset_breaks() {
959 let q = ivec2(0, 0);
960 let segments = vec![
961 vec![gate(GateType::I, &[q])],
962 vec![gate(GateType::RZ, &[q])],
963 vec![measure(PauliBasis::X, q, 0)],
964 ];
965 let slices = run(&segments, &[det(7, &[0])]);
966 assert_eq!(render(&slices.slices[0]), "");
968 assert_eq!(render(&slices.slices[1]), "d7:X(0,0)");
969 assert_eq!(render(&slices.slices[2]), "");
970 assert_eq!(
971 slices.breaks,
972 vec![RegionBreak {
973 detector: 7,
974 segment: 1,
975 qubit: q,
976 kind: RegionBreakKind::AnticommutesWithReset,
977 }]
978 );
979 }
980
981 #[test]
982 fn t_gate_tracks_through_its_clifford_proxy_like_s() {
983 let q = ivec2(0, 0);
984 let build = |g: GateType| {
988 vec![
989 vec![gate(GateType::RY, &[q])],
990 vec![gate(g, &[q])],
991 vec![measure(PauliBasis::X, q, 0)],
992 ]
993 };
994 let via_t = run(&build(GateType::T), &[det(0, &[0])]);
995 let via_s = run(&build(GateType::S), &[det(0, &[0])]);
996 assert_eq!(via_t.slices, via_s.slices);
997 assert!(via_t.breaks.is_empty(), "T keeps the region alive");
998 assert_eq!(render(&via_t.slices[1]), "d0:X(0,0)");
1000 assert_eq!(render(&via_t.slices[0]), "d0:Y(0,0)");
1001 }
1002
1003 #[test]
1004 fn surviving_x_support_breaks_against_implicit_start() {
1005 let q = ivec2(0, 0);
1006 let segments = vec![
1008 vec![gate(GateType::I, &[q])],
1009 vec![measure(PauliBasis::X, q, 0)],
1010 ];
1011 let slices = run(&segments, &[det(5, &[0])]);
1012 assert_eq!(render(&slices.slices[0]), "d5:X(0,0)");
1013 assert_eq!(render(&slices.slices[1]), "");
1014 assert_eq!(
1015 slices.breaks,
1016 vec![RegionBreak {
1017 detector: 5,
1018 segment: 0,
1019 qubit: q,
1020 kind: RegionBreakKind::AnticommutesWithStart,
1021 }]
1022 );
1023 }
1024
1025 #[test]
1026 fn repeat_op_is_rejected() {
1027 let body = crate::BodyId(0);
1028 let segments = [vec![Op::Repeat {
1029 body,
1030 repetitions: 2,
1031 }]];
1032 let refs: Vec<&[Op]> = segments.iter().map(Vec::as_slice).collect();
1033 assert_eq!(
1034 detector_slices_with_seeds(&refs, &[], &[]),
1035 Err(DetsliceError::RepeatNotFlattened)
1036 );
1037 }
1038}