1mod camps_prefix;
36#[cfg(feature = "bench-internal")]
43pub mod cut_selection;
44mod decoder;
45mod dem;
46mod noise;
47pub mod observable_reroute;
48mod parse;
49mod result;
50mod runner;
51mod t_sampler;
52
53pub use decoder::UnionFindDecoder;
54pub use dem::{DetectorErrorModel, ErrorMechanism};
55pub use parse::parse_qec_program;
56pub use result::{QecObservableEstimate, QecSampleResult};
57#[cfg(feature = "bench-internal")]
58pub use runner::{QecProfiledCounts, QecProfiledSampler, compile_qec_profiled_sampler};
59pub use runner::{run_qec_program, run_qec_program_reference};
60pub use t_sampler::{
61 QecObservableReroute, QecTStrategy, run_qec_program_spd_rerouted, run_qec_program_with_strategy,
62};
63
64use crate::circuit::{
65 Circuit, append_axis_to_z_rotation, append_parity_rotations, append_z_to_axis_rotation,
66};
67use crate::error::{PrismError, Result};
68use crate::gates::Gate;
69use crate::sim::compiled::{PackedShots, PauliVec, get_bit, set_bit};
70use crate::sim::unified_pauli::{PauliAxis, PauliTerm};
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum QecBasis {
75 X,
76 Y,
77 Z,
78}
79
80impl From<QecBasis> for PauliAxis {
81 fn from(basis: QecBasis) -> Self {
82 match basis {
83 QecBasis::X => PauliAxis::X,
84 QecBasis::Y => PauliAxis::Y,
85 QecBasis::Z => PauliAxis::Z,
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub struct QecPauli {
93 pub basis: QecBasis,
94 pub qubit: usize,
95}
96
97impl QecPauli {
98 pub fn new(basis: QecBasis, qubit: usize) -> Self {
99 Self { basis, qubit }
100 }
101
102 pub fn x(qubit: usize) -> Self {
103 Self::new(QecBasis::X, qubit)
104 }
105
106 pub fn y(qubit: usize) -> Self {
107 Self::new(QecBasis::Y, qubit)
108 }
109
110 pub fn z(qubit: usize) -> Self {
111 Self::new(QecBasis::Z, qubit)
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
122pub enum QecRecordRef {
123 Absolute(usize),
124 Lookback(usize),
125}
126
127impl QecRecordRef {
128 pub fn absolute(index: usize) -> Self {
129 Self::Absolute(index)
130 }
131
132 pub fn lookback(distance: usize) -> Result<Self> {
133 if distance == 0 {
134 return Err(PrismError::InvalidParameter {
135 message: "measurement lookback distance must be at least 1".to_string(),
136 });
137 }
138 Ok(Self::Lookback(distance))
139 }
140
141 fn resolve(self, next_measurement: usize) -> Result<usize> {
142 match self {
143 Self::Absolute(index) if index < next_measurement => Ok(index),
144 Self::Absolute(index) => Err(PrismError::InvalidParameter {
145 message: format!(
146 "measurement record {index} out of bounds for {next_measurement} existing records"
147 ),
148 }),
149 Self::Lookback(distance) if distance > 0 && distance <= next_measurement => {
150 Ok(next_measurement - distance)
151 }
152 Self::Lookback(distance) => Err(PrismError::InvalidParameter {
153 message: format!(
154 "measurement lookback {distance} out of bounds for {next_measurement} existing records"
155 ),
156 }),
157 }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq)]
167pub enum QecNoise {
168 XError(f64),
170 ZError(f64),
172 Depolarize1(f64),
176 Depolarize2(f64),
181}
182
183impl QecNoise {
184 pub fn probability(self) -> f64 {
185 match self {
186 Self::XError(p) | Self::ZError(p) | Self::Depolarize1(p) | Self::Depolarize2(p) => p,
187 }
188 }
189
190 pub fn name(self) -> &'static str {
192 match self {
193 Self::XError(_) => "X_ERROR",
194 Self::ZError(_) => "Z_ERROR",
195 Self::Depolarize1(_) => "DEPOLARIZE1",
196 Self::Depolarize2(_) => "DEPOLARIZE2",
197 }
198 }
199}
200
201#[derive(Debug, Clone, PartialEq)]
203pub enum QecOp {
204 Gate { gate: Gate, targets: Vec<usize> },
208 Measure { basis: QecBasis, qubit: usize },
211 MeasurePauliProduct { terms: Vec<QecPauli> },
214 Reset { basis: QecBasis, qubit: usize },
216 Detector {
220 records: Vec<QecRecordRef>,
221 coords: Vec<f64>,
222 },
223 ObservableInclude {
226 observable: usize,
227 records: Vec<QecRecordRef>,
228 },
229 ExpectationValue {
237 terms: Vec<QecPauli>,
238 coefficient: f64,
239 },
240 Postselect {
243 records: Vec<QecRecordRef>,
244 expected: bool,
245 },
246 Feedforward {
255 records: Vec<QecRecordRef>,
256 expected: bool,
257 body: Vec<QecOp>,
258 },
259 Noise {
262 channel: QecNoise,
263 targets: Vec<usize>,
264 },
265 Tick,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct QecMeasurementRow {
279 num_qubits: usize,
280 pauli: PauliVec,
281 weight: usize,
282}
283
284impl QecMeasurementRow {
285 pub fn from_terms(num_qubits: usize, terms: &[QecPauli]) -> Result<Self> {
287 if terms.is_empty() {
288 return Err(PrismError::InvalidParameter {
289 message: "QEC measurement row requires at least one Pauli term".to_string(),
290 });
291 }
292 validate_pauli_terms(terms, num_qubits)?;
293
294 let row_words = num_qubits.div_ceil(64);
295 let mut pauli = PauliVec::new(row_words);
296
297 for term in terms {
298 match term.basis {
299 QecBasis::X => set_bit(&mut pauli.x, term.qubit, true),
300 QecBasis::Y => {
301 set_bit(&mut pauli.x, term.qubit, true);
302 set_bit(&mut pauli.z, term.qubit, true);
303 }
304 QecBasis::Z => set_bit(&mut pauli.z, term.qubit, true),
305 }
306 }
307
308 Ok(Self {
309 num_qubits,
310 pauli,
311 weight: terms.len(),
312 })
313 }
314
315 pub fn single(num_qubits: usize, basis: QecBasis, qubit: usize) -> Result<Self> {
317 Self::from_terms(num_qubits, &[QecPauli::new(basis, qubit)])
318 }
319
320 pub fn num_qubits(&self) -> usize {
322 self.num_qubits
323 }
324
325 pub fn weight(&self) -> usize {
327 self.weight
328 }
329
330 pub fn x_mask(&self) -> &[u64] {
332 &self.pauli.x
333 }
334
335 pub fn z_mask(&self) -> &[u64] {
337 &self.pauli.z
338 }
339
340 pub fn pauli_at(&self, qubit: usize) -> Option<QecBasis> {
343 if qubit >= self.num_qubits {
344 return None;
345 }
346 match (get_bit(&self.pauli.x, qubit), get_bit(&self.pauli.z, qubit)) {
347 (true, false) => Some(QecBasis::X),
348 (true, true) => Some(QecBasis::Y),
349 (false, true) => Some(QecBasis::Z),
350 (false, false) => None,
351 }
352 }
353
354 pub fn terms(&self) -> Vec<QecPauli> {
356 let mut terms = Vec::with_capacity(self.weight);
357 for qubit in 0..self.num_qubits {
358 if let Some(basis) = self.pauli_at(qubit) {
359 terms.push(QecPauli::new(basis, qubit));
360 }
361 }
362 terms
363 }
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
368pub struct QecCompiledRows {
369 num_qubits: usize,
370 measurement_rows: Vec<QecMeasurementRow>,
371 detector_rows: Vec<Vec<usize>>,
372 observable_rows: Vec<Vec<usize>>,
373 postselection_rows: Vec<Vec<usize>>,
374 postselection_expected: Vec<bool>,
375}
376
377impl QecCompiledRows {
378 pub fn num_qubits(&self) -> usize {
379 self.num_qubits
380 }
381
382 pub fn measurement_rows(&self) -> &[QecMeasurementRow] {
384 &self.measurement_rows
385 }
386
387 pub fn detector_rows(&self) -> &[Vec<usize>] {
389 &self.detector_rows
390 }
391
392 pub fn observable_rows(&self) -> &[Vec<usize>] {
394 &self.observable_rows
395 }
396
397 pub fn postselection_rows(&self) -> &[Vec<usize>] {
398 &self.postselection_rows
399 }
400
401 pub fn postselection_expected(&self) -> &[bool] {
403 &self.postselection_expected
404 }
405
406 pub fn postselection_predicates(&self) -> impl ExactSizeIterator<Item = (&[usize], bool)> + '_ {
408 self.postselection_rows
409 .iter()
410 .map(Vec::as_slice)
411 .zip(self.postselection_expected.iter().copied())
412 }
413
414 pub fn num_measurements(&self) -> usize {
415 self.measurement_rows.len()
416 }
417
418 pub fn num_detectors(&self) -> usize {
419 self.detector_rows.len()
420 }
421
422 pub fn num_observables(&self) -> usize {
423 self.observable_rows.len()
424 }
425
426 pub fn num_postselections(&self) -> usize {
427 self.postselection_rows.len()
428 }
429
430 pub fn packed_row_words(&self) -> usize {
432 self.num_qubits.div_ceil(64)
433 }
434
435 pub fn measurement_mask_bytes(&self) -> usize {
437 self.measurement_rows
438 .len()
439 .saturating_mul(self.packed_row_words())
440 .saturating_mul(2)
441 .saturating_mul(std::mem::size_of::<u64>())
442 }
443
444 pub fn detector_parities(&self, measurements: &PackedShots) -> Result<PackedShots> {
445 measurements.parity_rows(&self.detector_rows)
446 }
447
448 pub fn observable_parities(&self, measurements: &PackedShots) -> Result<PackedShots> {
449 measurements.parity_rows(&self.observable_rows)
450 }
451
452 pub fn postselection_parities(&self, measurements: &PackedShots) -> Result<PackedShots> {
453 measurements.parity_rows(&self.postselection_rows)
454 }
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
459pub struct QecOptions {
460 pub shots: usize,
461 pub seed: u64,
463 pub chunk_size: Option<usize>,
469 pub keep_measurements: bool,
475}
476
477impl Default for QecOptions {
478 fn default() -> Self {
479 Self {
480 shots: 1024,
481 seed: 42,
482 chunk_size: None,
483 keep_measurements: true,
484 }
485 }
486}
487
488#[derive(Debug, Clone, PartialEq)]
490pub struct QecProgram {
491 num_qubits: usize,
492 ops: Vec<QecOp>,
493 options: QecOptions,
494}
495
496impl QecProgram {
497 pub fn new(num_qubits: usize) -> Self {
498 Self::with_options(num_qubits, QecOptions::default())
499 }
500
501 pub fn with_options(num_qubits: usize, options: QecOptions) -> Self {
502 Self {
503 num_qubits,
504 ops: Vec::new(),
505 options,
506 }
507 }
508
509 pub fn from_ops(num_qubits: usize, options: QecOptions, ops: Vec<QecOp>) -> Result<Self> {
512 let mut program = Self::with_options(num_qubits, options);
513 let mut next_measurement = 0usize;
514 for op in ops {
515 program.validate_op(&op, next_measurement)?;
516 if matches!(
517 op,
518 QecOp::Measure { .. } | QecOp::MeasurePauliProduct { .. }
519 ) {
520 next_measurement += 1;
521 }
522 program.ops.push(op);
523 }
524 Ok(program)
525 }
526
527 pub fn from_text(input: &str) -> Result<Self> {
529 parse_qec_program(input)
530 }
531
532 pub fn num_qubits(&self) -> usize {
533 self.num_qubits
534 }
535
536 pub fn options(&self) -> QecOptions {
537 self.options
538 }
539
540 pub fn set_options(&mut self, options: QecOptions) {
541 self.options = options;
542 }
543
544 pub fn ops(&self) -> &[QecOp] {
545 &self.ops
546 }
547
548 pub fn num_measurements(&self) -> usize {
550 self.ops
551 .iter()
552 .filter(|op| {
553 matches!(
554 op,
555 QecOp::Measure { .. } | QecOp::MeasurePauliProduct { .. }
556 )
557 })
558 .count()
559 }
560
561 pub fn num_detectors(&self) -> usize {
562 self.ops
563 .iter()
564 .filter(|op| matches!(op, QecOp::Detector { .. }))
565 .count()
566 }
567
568 pub fn num_observables(&self) -> usize {
570 self.ops
571 .iter()
572 .filter_map(|op| match op {
573 QecOp::ObservableInclude { observable, .. } => Some(*observable),
574 _ => None,
575 })
576 .max()
577 .map_or(0, |max_idx| max_idx + 1)
578 }
579
580 pub fn num_expectation_values(&self) -> usize {
582 self.ops
583 .iter()
584 .filter(|op| matches!(op, QecOp::ExpectationValue { .. }))
585 .count()
586 }
587
588 pub fn expectation_value_ops(&self) -> Vec<(&[QecPauli], f64)> {
590 self.ops
591 .iter()
592 .filter_map(|op| match op {
593 QecOp::ExpectationValue { terms, coefficient } => {
594 Some((terms.as_slice(), *coefficient))
595 }
596 _ => None,
597 })
598 .collect()
599 }
600
601 pub fn push_op(&mut self, op: QecOp) -> Result<()> {
602 self.validate_op(&op, self.num_measurements())?;
603 self.ops.push(op);
604 Ok(())
605 }
606
607 pub fn push_gate(&mut self, gate: Gate, targets: &[usize]) -> Result<()> {
608 self.push_op(QecOp::Gate {
609 gate,
610 targets: targets.to_vec(),
611 })
612 }
613
614 pub fn reset(&mut self, basis: QecBasis, qubit: usize) -> Result<()> {
615 self.push_op(QecOp::Reset { basis, qubit })
616 }
617
618 pub fn measure(&mut self, basis: QecBasis, qubit: usize) -> Result<usize> {
620 let record = self.num_measurements();
621 self.push_op(QecOp::Measure { basis, qubit })?;
622 Ok(record)
623 }
624
625 pub fn measure_z(&mut self, qubit: usize) -> Result<usize> {
627 self.measure(QecBasis::Z, qubit)
628 }
629
630 pub fn measure_x(&mut self, qubit: usize) -> Result<usize> {
632 self.measure(QecBasis::X, qubit)
633 }
634
635 pub fn measure_pauli_product(&mut self, terms: &[QecPauli]) -> Result<usize> {
637 let record = self.num_measurements();
638 self.push_op(QecOp::MeasurePauliProduct {
639 terms: terms.to_vec(),
640 })?;
641 Ok(record)
642 }
643
644 pub fn detector(&mut self, records: &[QecRecordRef]) -> Result<usize> {
646 self.detector_with_coords(records, &[])
647 }
648
649 pub fn detector_with_coords(
651 &mut self,
652 records: &[QecRecordRef],
653 coords: &[f64],
654 ) -> Result<usize> {
655 let detector = self.num_detectors();
656 self.push_op(QecOp::Detector {
657 records: records.to_vec(),
658 coords: coords.to_vec(),
659 })?;
660 Ok(detector)
661 }
662
663 pub fn observable_include(
664 &mut self,
665 observable: usize,
666 records: &[QecRecordRef],
667 ) -> Result<()> {
668 self.push_op(QecOp::ObservableInclude {
669 observable,
670 records: records.to_vec(),
671 })
672 }
673
674 pub fn expectation_value(&mut self, terms: &[QecPauli], coefficient: f64) -> Result<()> {
675 self.push_op(QecOp::ExpectationValue {
676 terms: terms.to_vec(),
677 coefficient,
678 })
679 }
680
681 pub fn postselect(&mut self, records: &[QecRecordRef], expected: bool) -> Result<()> {
682 self.push_op(QecOp::Postselect {
683 records: records.to_vec(),
684 expected,
685 })
686 }
687
688 pub fn noise(&mut self, channel: QecNoise, targets: &[usize]) -> Result<()> {
689 self.push_op(QecOp::Noise {
690 channel,
691 targets: targets.to_vec(),
692 })
693 }
694
695 pub fn feedforward(
700 &mut self,
701 records: &[QecRecordRef],
702 expected: bool,
703 body: Vec<QecOp>,
704 ) -> Result<()> {
705 self.push_op(QecOp::Feedforward {
706 records: records.to_vec(),
707 expected,
708 body,
709 })
710 }
711
712 fn visit_ops_with_measurement_count(
716 &self,
717 mut visit: impl FnMut(&QecOp, usize) -> Result<()>,
718 ) -> Result<()> {
719 let mut next_measurement = 0;
720 for op in &self.ops {
721 if matches!(
722 op,
723 QecOp::Measure { .. } | QecOp::MeasurePauliProduct { .. }
724 ) {
725 next_measurement += 1;
726 continue;
727 }
728 visit(op, next_measurement)?;
729 }
730 Ok(())
731 }
732
733 pub fn detector_rows(&self) -> Result<Vec<Vec<usize>>> {
735 let mut rows = Vec::new();
736 self.visit_ops_with_measurement_count(|op, next_measurement| {
737 if let QecOp::Detector { records, .. } = op {
738 rows.push(resolve_records(records, next_measurement)?);
739 }
740 Ok(())
741 })?;
742 Ok(rows)
743 }
744
745 pub fn observable_rows(&self) -> Result<Vec<Vec<usize>>> {
747 let mut rows: Vec<Vec<usize>> = Vec::new();
748 self.visit_ops_with_measurement_count(|op, next_measurement| {
749 if let QecOp::ObservableInclude {
750 observable,
751 records,
752 } = op
753 {
754 if rows.len() <= *observable {
755 rows.resize_with(*observable + 1, Vec::new);
756 }
757 rows[*observable].extend(resolve_records(records, next_measurement)?);
758 }
759 Ok(())
760 })?;
761 Ok(rows)
762 }
763
764 pub fn postselection_rows(&self) -> Result<Vec<(Vec<usize>, bool)>> {
766 let mut rows = Vec::new();
767 self.visit_ops_with_measurement_count(|op, next_measurement| {
768 if let QecOp::Postselect { records, expected } = op {
769 rows.push((resolve_records(records, next_measurement)?, *expected));
770 }
771 Ok(())
772 })?;
773 Ok(rows)
774 }
775
776 pub fn empty_result(&self) -> QecSampleResult {
778 QecSampleResult::empty(
779 self.num_measurements(),
780 self.num_detectors(),
781 self.num_observables(),
782 )
783 }
784
785 fn validate_op(&self, op: &QecOp, next_measurement: usize) -> Result<()> {
786 match op {
787 QecOp::Gate { gate, targets } => {
788 if gate.num_qubits() != targets.len() {
789 return Err(PrismError::GateArity {
790 gate: gate.name().to_string(),
791 expected: gate.num_qubits(),
792 got: targets.len(),
793 });
794 }
795 validate_qubits(targets.iter().copied(), self.num_qubits)?;
796 }
797 QecOp::Measure { qubit, .. } | QecOp::Reset { qubit, .. } => {
798 validate_qubit(*qubit, self.num_qubits)?;
799 }
800 QecOp::MeasurePauliProduct { terms } => {
801 if terms.is_empty() {
802 return Err(PrismError::InvalidParameter {
803 message: "Pauli-product measurement requires at least one term".to_string(),
804 });
805 }
806 validate_pauli_terms(terms, self.num_qubits)?;
807 }
808 QecOp::Detector { records, coords } => {
809 resolve_records(records, next_measurement)?;
810 validate_finite_values(coords, "detector coordinate")?;
811 }
812 QecOp::ObservableInclude { records, .. } | QecOp::Postselect { records, .. } => {
813 resolve_records(records, next_measurement)?;
814 }
815 QecOp::ExpectationValue { terms, coefficient } => {
816 if terms.is_empty() {
817 return Err(PrismError::InvalidParameter {
818 message: "expectation value requires at least one Pauli term".to_string(),
819 });
820 }
821 validate_pauli_terms(terms, self.num_qubits)?;
822 if !coefficient.is_finite() {
823 return Err(PrismError::InvalidParameter {
824 message: "expectation-value coefficient must be finite".to_string(),
825 });
826 }
827 }
828 QecOp::Feedforward {
829 records,
830 body,
831 expected: _,
832 } => {
833 if records.is_empty() {
834 return Err(PrismError::InvalidParameter {
835 message: "feed-forward predicate requires at least one record".to_string(),
836 });
837 }
838 if body.is_empty() {
839 return Err(PrismError::InvalidParameter {
840 message: "feed-forward body requires at least one operation".to_string(),
841 });
842 }
843 resolve_records(records, next_measurement)?;
844 for inner in body {
845 if !matches!(inner, QecOp::Gate { .. } | QecOp::Reset { .. }) {
846 return Err(PrismError::InvalidParameter {
847 message: format!(
848 "feed-forward body admits gates and resets only, got `{}`",
849 qec_op_name(inner)
850 ),
851 });
852 }
853 self.validate_op(inner, next_measurement)?;
854 }
855 }
856 QecOp::Noise { channel, targets } => {
857 validate_noise(*channel, targets, self.num_qubits)?;
858 }
859 QecOp::Tick => {}
860 }
861 Ok(())
862 }
863}
864
865fn qec_op_name(op: &QecOp) -> &'static str {
867 match op {
868 QecOp::Gate { .. } => "gate",
869 QecOp::Measure { .. } => "M",
870 QecOp::MeasurePauliProduct { .. } => "MPP",
871 QecOp::Reset { .. } => "R",
872 QecOp::Detector { .. } => "DETECTOR",
873 QecOp::ObservableInclude { .. } => "OBSERVABLE_INCLUDE",
874 QecOp::ExpectationValue { .. } => "EXP_VAL",
875 QecOp::Postselect { .. } => "POSTSELECT",
876 QecOp::Feedforward { .. } => "FEEDFORWARD",
877 QecOp::Noise { .. } => "noise",
878 QecOp::Tick => "TICK",
879 }
880}
881
882pub fn compile_qec_program_rows(program: &QecProgram) -> Result<QecCompiledRows> {
895 let mut measurement_rows = Vec::with_capacity(program.num_measurements());
896
897 for op in program.ops() {
898 match op {
899 QecOp::Gate { gate, .. } => {
900 return Err(PrismError::IncompatibleBackend {
901 backend: "QEC row compiler".to_string(),
902 reason: format!(
903 "QEC row compilation does not lower gates yet, got `{}`",
904 gate.name()
905 ),
906 });
907 }
908 QecOp::Measure { basis, qubit } => {
909 measurement_rows.push(QecMeasurementRow::single(
910 program.num_qubits(),
911 *basis,
912 *qubit,
913 )?);
914 }
915 QecOp::MeasurePauliProduct { terms } => {
916 measurement_rows.push(QecMeasurementRow::from_terms(program.num_qubits(), terms)?);
917 }
918 QecOp::Reset { .. } => {
919 return Err(PrismError::IncompatibleBackend {
920 backend: "QEC row compiler".to_string(),
921 reason: "QEC row compilation does not lower resets yet".to_string(),
922 });
923 }
924 QecOp::ExpectationValue { .. } => {
925 return Err(PrismError::IncompatibleBackend {
926 backend: "QEC row compiler".to_string(),
927 reason: "QEC row compilation has no row representation for `EXP_VAL`; \
928 use `run_qec_program`"
929 .to_string(),
930 });
931 }
932 QecOp::Feedforward { .. } => {
933 return Err(PrismError::IncompatibleBackend {
934 backend: "QEC row compiler".to_string(),
935 reason: "QEC row compilation has no row representation for `FEEDFORWARD`; \
936 use `run_qec_program_reference`"
937 .to_string(),
938 });
939 }
940 QecOp::Detector { .. }
941 | QecOp::ObservableInclude { .. }
942 | QecOp::Postselect { .. }
943 | QecOp::Tick => {}
944 QecOp::Noise { channel, .. } if channel.probability() == 0.0 => {}
945 QecOp::Noise { .. } => {
946 return Err(PrismError::IncompatibleBackend {
947 backend: "QEC row compiler".to_string(),
948 reason: "QEC row compilation does not support active noise annotations yet"
949 .to_string(),
950 });
951 }
952 }
953 }
954
955 let postselection_predicates = program.postselection_rows()?;
956 let mut postselection_rows = Vec::with_capacity(postselection_predicates.len());
957 let mut postselection_expected = Vec::with_capacity(postselection_predicates.len());
958 for (row, expected) in postselection_predicates {
959 postselection_rows.push(row);
960 postselection_expected.push(expected);
961 }
962
963 Ok(QecCompiledRows {
964 num_qubits: program.num_qubits(),
965 measurement_rows,
966 detector_rows: program.detector_rows()?,
967 observable_rows: program.observable_rows()?,
968 postselection_rows,
969 postselection_expected,
970 })
971}
972
973pub(crate) fn qec_terms_to_pauli(terms: &[QecPauli]) -> Vec<PauliTerm> {
974 terms
975 .iter()
976 .map(|t| PauliTerm::new(t.qubit, t.basis.into()))
977 .collect()
978}
979
980pub(crate) fn validate_measured_qubit_reuse(program: &QecProgram) -> Result<()> {
993 let reuse = |qubit: usize| PrismError::InvalidParameter {
994 message: format!(
995 "qubit {qubit} was measured in a non-Z basis and must be reset before it is used \
996 again: a basis measurement leaves the qubit in the Z frame, not in the basis it \
997 named"
998 ),
999 };
1000 let mut rotated = vec![false; program.num_qubits()];
1001 for op in program.ops() {
1002 match op {
1003 QecOp::Gate { targets, .. } => {
1004 if let Some(&qubit) = targets.iter().find(|&&q| rotated[q]) {
1005 return Err(reuse(qubit));
1006 }
1007 }
1008 QecOp::Measure { basis, qubit } => {
1009 if rotated[*qubit] {
1010 return Err(reuse(*qubit));
1011 }
1012 rotated[*qubit] = *basis != QecBasis::Z;
1013 }
1014 QecOp::MeasurePauliProduct { terms } => {
1015 if let Some(term) = terms.iter().find(|t| rotated[t.qubit]) {
1016 return Err(reuse(term.qubit));
1017 }
1018 }
1019 QecOp::Reset { qubit, .. } => rotated[*qubit] = false,
1020 _ => {}
1021 }
1022 }
1023 Ok(())
1024}
1025
1026pub(crate) fn validate_qec_exp_val_placement(program: &QecProgram) -> Result<()> {
1041 let terminal_violation = |op_name: &str| PrismError::InvalidParameter {
1042 message: format!("`EXP_VAL` must be terminal: `{op_name}` appears after an `EXP_VAL` op"),
1043 };
1044 let mut seen_exp_val = false;
1045 let mut measured_since_reset = vec![false; program.num_qubits()];
1046 for op in program.ops() {
1047 match op {
1048 QecOp::ExpectationValue { terms, .. } => {
1049 seen_exp_val = true;
1050 if let Some(term) = terms.iter().find(|t| measured_since_reset[t.qubit]) {
1051 return Err(PrismError::InvalidParameter {
1052 message: format!(
1053 "`EXP_VAL` term on qubit {}: qubit was measured after its last \
1054 reset; expectation values are defined only on live qubits",
1055 term.qubit
1056 ),
1057 });
1058 }
1059 }
1060 QecOp::Gate { gate, .. } => {
1061 if seen_exp_val {
1062 return Err(terminal_violation(gate.name()));
1063 }
1064 }
1065 QecOp::Measure { qubit, .. } => {
1066 if seen_exp_val {
1067 return Err(terminal_violation("M"));
1068 }
1069 measured_since_reset[*qubit] = true;
1070 }
1071 QecOp::MeasurePauliProduct { .. } => {
1072 if seen_exp_val {
1073 return Err(terminal_violation("MPP"));
1074 }
1075 }
1076 QecOp::Reset { qubit, .. } => {
1077 if seen_exp_val {
1078 return Err(terminal_violation("R"));
1079 }
1080 measured_since_reset[*qubit] = false;
1081 }
1082 QecOp::Noise { channel, .. } if channel.probability() > 0.0 => {
1083 if seen_exp_val {
1084 return Err(terminal_violation(channel.name()));
1085 }
1086 }
1087 QecOp::Feedforward { .. } => {
1088 if seen_exp_val {
1089 return Err(terminal_violation("FEEDFORWARD"));
1090 }
1091 }
1094 QecOp::Detector { .. }
1095 | QecOp::ObservableInclude { .. }
1096 | QecOp::Postselect { .. }
1097 | QecOp::Noise { .. }
1098 | QecOp::Tick => {}
1099 }
1100 }
1101 Ok(())
1102}
1103
1104pub(super) fn append_basis_to_z_rotation(circuit: &mut Circuit, basis: QecBasis, qubit: usize) {
1105 append_axis_to_z_rotation(circuit, basis.into(), qubit);
1106}
1107
1108pub(super) fn append_z_to_basis_rotation(circuit: &mut Circuit, basis: QecBasis, qubit: usize) {
1109 append_z_to_axis_rotation(circuit, basis.into(), qubit);
1110}
1111
1112pub(super) fn append_mpp_parity_rotations(
1115 circuit: &mut Circuit,
1116 terms: &[QecPauli],
1117 scratch: usize,
1118) {
1119 append_parity_rotations(circuit, &qec_terms_to_pauli(terms), scratch);
1120}
1121
1122pub(super) fn qec_non_clifford_error(gate: &Gate) -> PrismError {
1123 PrismError::IncompatibleBackend {
1124 backend: "QEC compiled runner".to_string(),
1125 reason: format!(
1126 "compiled QEC runner requires Clifford gates, got `{}`",
1127 gate.name()
1128 ),
1129 }
1130}
1131
1132pub(super) fn ensure_lowered_record_count(
1133 program: &QecProgram,
1134 produced: usize,
1135 stage: &str,
1136) -> Result<()> {
1137 if produced != program.num_measurements() {
1138 return Err(PrismError::InvalidParameter {
1139 message: format!(
1140 "QEC {stage} lowering produced {produced} records, expected {}",
1141 program.num_measurements()
1142 ),
1143 });
1144 }
1145 Ok(())
1146}
1147
1148fn resolve_records(records: &[QecRecordRef], next_measurement: usize) -> Result<Vec<usize>> {
1149 records
1150 .iter()
1151 .map(|record| record.resolve(next_measurement))
1152 .collect()
1153}
1154
1155fn validate_qubit(qubit: usize, num_qubits: usize) -> Result<()> {
1156 if qubit >= num_qubits {
1157 return Err(PrismError::InvalidQubit {
1158 index: qubit,
1159 register_size: num_qubits,
1160 });
1161 }
1162 Ok(())
1163}
1164
1165fn validate_qubits<I>(qubits: I, num_qubits: usize) -> Result<()>
1166where
1167 I: IntoIterator<Item = usize>,
1168{
1169 for qubit in qubits {
1170 validate_qubit(qubit, num_qubits)?;
1171 }
1172 Ok(())
1173}
1174
1175fn validate_pauli_terms(terms: &[QecPauli], num_qubits: usize) -> Result<()> {
1176 for (idx, term) in terms.iter().enumerate() {
1177 validate_qubit(term.qubit, num_qubits)?;
1178 if terms[..idx].iter().any(|prior| prior.qubit == term.qubit) {
1179 return Err(PrismError::InvalidParameter {
1180 message: format!("Pauli product contains duplicate qubit {}", term.qubit),
1181 });
1182 }
1183 }
1184 Ok(())
1185}
1186
1187fn validate_finite_values(values: &[f64], label: &str) -> Result<()> {
1188 for value in values {
1189 if !value.is_finite() {
1190 return Err(PrismError::InvalidParameter {
1191 message: format!("{label} must be finite"),
1192 });
1193 }
1194 }
1195 Ok(())
1196}
1197
1198fn validate_noise(channel: QecNoise, targets: &[usize], num_qubits: usize) -> Result<()> {
1199 let p = channel.probability();
1200 if !(0.0..=1.0).contains(&p) || !p.is_finite() {
1201 return Err(PrismError::InvalidParameter {
1202 message: format!(
1203 "{} probability must be finite and in [0, 1]",
1204 channel.name()
1205 ),
1206 });
1207 }
1208
1209 if targets.is_empty() {
1210 return Err(PrismError::InvalidParameter {
1211 message: format!("{} requires at least one target", channel.name()),
1212 });
1213 }
1214
1215 if matches!(channel, QecNoise::Depolarize2(_)) && !targets.len().is_multiple_of(2) {
1216 return Err(PrismError::InvalidParameter {
1217 message: "DEPOLARIZE2 requires an even number of targets".to_string(),
1218 });
1219 }
1220
1221 if matches!(channel, QecNoise::Depolarize2(_)) {
1222 for pair in targets.chunks_exact(2) {
1223 if pair[0] == pair[1] {
1224 return Err(PrismError::InvalidParameter {
1225 message: "DEPOLARIZE2 target pairs must use distinct qubits".to_string(),
1226 });
1227 }
1228 }
1229 }
1230
1231 validate_qubits(targets.iter().copied(), num_qubits)
1232}