1mod noise;
28mod parse;
29mod result;
30mod runner;
31
32pub use parse::parse_qec_program;
33pub use result::QecSampleResult;
34#[cfg(feature = "bench-internal")]
35pub use runner::{compile_qec_profiled_sampler, QecProfiledCounts, QecProfiledSampler};
36pub use runner::{run_qec_program, run_qec_program_reference};
37
38use crate::circuit::Circuit;
39use crate::error::{PrismError, Result};
40use crate::gates::Gate;
41use crate::sim::compiled::{get_bit, set_bit, PackedShots, PauliVec};
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum QecBasis {
46 X,
48 Y,
50 Z,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub struct QecPauli {
57 pub basis: QecBasis,
59 pub qubit: usize,
61}
62
63impl QecPauli {
64 pub fn new(basis: QecBasis, qubit: usize) -> Self {
66 Self { basis, qubit }
67 }
68
69 pub fn x(qubit: usize) -> Self {
71 Self::new(QecBasis::X, qubit)
72 }
73
74 pub fn y(qubit: usize) -> Self {
76 Self::new(QecBasis::Y, qubit)
77 }
78
79 pub fn z(qubit: usize) -> Self {
81 Self::new(QecBasis::Z, qubit)
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum QecRecordRef {
93 Absolute(usize),
95 Lookback(usize),
97}
98
99impl QecRecordRef {
100 pub fn absolute(index: usize) -> Self {
102 Self::Absolute(index)
103 }
104
105 pub fn lookback(distance: usize) -> Result<Self> {
107 if distance == 0 {
108 return Err(PrismError::InvalidParameter {
109 message: "measurement lookback distance must be at least 1".to_string(),
110 });
111 }
112 Ok(Self::Lookback(distance))
113 }
114
115 fn resolve(self, next_measurement: usize) -> Result<usize> {
116 match self {
117 Self::Absolute(index) if index < next_measurement => Ok(index),
118 Self::Absolute(index) => Err(PrismError::InvalidParameter {
119 message: format!(
120 "measurement record {index} out of bounds for {next_measurement} existing records"
121 ),
122 }),
123 Self::Lookback(distance) if distance > 0 && distance <= next_measurement => {
124 Ok(next_measurement - distance)
125 }
126 Self::Lookback(distance) => Err(PrismError::InvalidParameter {
127 message: format!(
128 "measurement lookback {distance} out of bounds for {next_measurement} existing records"
129 ),
130 }),
131 }
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq)]
141pub enum QecNoise {
142 XError(f64),
144 ZError(f64),
146 Depolarize1(f64),
150 Depolarize2(f64),
155}
156
157impl QecNoise {
158 pub fn probability(self) -> f64 {
160 match self {
161 Self::XError(p) | Self::ZError(p) | Self::Depolarize1(p) | Self::Depolarize2(p) => p,
162 }
163 }
164
165 pub fn name(self) -> &'static str {
167 match self {
168 Self::XError(_) => "X_ERROR",
169 Self::ZError(_) => "Z_ERROR",
170 Self::Depolarize1(_) => "DEPOLARIZE1",
171 Self::Depolarize2(_) => "DEPOLARIZE2",
172 }
173 }
174}
175
176#[derive(Debug, Clone, PartialEq)]
178pub enum QecOp {
179 Gate { gate: Gate, targets: Vec<usize> },
183 Measure { basis: QecBasis, qubit: usize },
186 MeasurePauliProduct { terms: Vec<QecPauli> },
189 Reset { basis: QecBasis, qubit: usize },
191 Detector {
195 records: Vec<QecRecordRef>,
196 coords: Vec<f64>,
197 },
198 ObservableInclude {
201 observable: usize,
202 records: Vec<QecRecordRef>,
203 },
204 ExpectationValue {
207 terms: Vec<QecPauli>,
208 coefficient: f64,
209 },
210 Postselect {
213 records: Vec<QecRecordRef>,
214 expected: bool,
215 },
216 Noise {
219 channel: QecNoise,
220 targets: Vec<usize>,
221 },
222 Tick,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct QecMeasurementRow {
230 num_qubits: usize,
231 pauli: PauliVec,
232 weight: usize,
233}
234
235impl QecMeasurementRow {
236 pub fn from_terms(num_qubits: usize, terms: &[QecPauli]) -> Result<Self> {
238 if terms.is_empty() {
239 return Err(PrismError::InvalidParameter {
240 message: "QEC measurement row requires at least one Pauli term".to_string(),
241 });
242 }
243 validate_pauli_terms(terms, num_qubits)?;
244
245 let row_words = num_qubits.div_ceil(64);
246 let mut pauli = PauliVec::new(row_words);
247
248 for term in terms {
249 match term.basis {
250 QecBasis::X => set_bit(&mut pauli.x, term.qubit, true),
251 QecBasis::Y => {
252 set_bit(&mut pauli.x, term.qubit, true);
253 set_bit(&mut pauli.z, term.qubit, true);
254 }
255 QecBasis::Z => set_bit(&mut pauli.z, term.qubit, true),
256 }
257 }
258
259 Ok(Self {
260 num_qubits,
261 pauli,
262 weight: terms.len(),
263 })
264 }
265
266 pub fn single(num_qubits: usize, basis: QecBasis, qubit: usize) -> Result<Self> {
268 Self::from_terms(num_qubits, &[QecPauli::new(basis, qubit)])
269 }
270
271 pub fn num_qubits(&self) -> usize {
273 self.num_qubits
274 }
275
276 pub fn weight(&self) -> usize {
278 self.weight
279 }
280
281 pub fn x_mask(&self) -> &[u64] {
283 &self.pauli.x
284 }
285
286 pub fn z_mask(&self) -> &[u64] {
288 &self.pauli.z
289 }
290
291 pub fn pauli_at(&self, qubit: usize) -> Option<QecBasis> {
294 if qubit >= self.num_qubits {
295 return None;
296 }
297 match (get_bit(&self.pauli.x, qubit), get_bit(&self.pauli.z, qubit)) {
298 (true, false) => Some(QecBasis::X),
299 (true, true) => Some(QecBasis::Y),
300 (false, true) => Some(QecBasis::Z),
301 (false, false) => None,
302 }
303 }
304
305 pub fn terms(&self) -> Vec<QecPauli> {
307 let mut terms = Vec::with_capacity(self.weight);
308 for qubit in 0..self.num_qubits {
309 if let Some(basis) = self.pauli_at(qubit) {
310 terms.push(QecPauli::new(basis, qubit));
311 }
312 }
313 terms
314 }
315
316 pub fn packed_bytes(&self) -> usize {
318 (self.pauli.x.len() + self.pauli.z.len()) * std::mem::size_of::<u64>()
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct QecCompiledRows {
325 num_qubits: usize,
326 measurement_rows: Vec<QecMeasurementRow>,
327 detector_rows: Vec<Vec<usize>>,
328 observable_rows: Vec<Vec<usize>>,
329 postselection_rows: Vec<Vec<usize>>,
330 postselection_expected: Vec<bool>,
331}
332
333impl QecCompiledRows {
334 pub fn num_qubits(&self) -> usize {
336 self.num_qubits
337 }
338
339 pub fn measurement_rows(&self) -> &[QecMeasurementRow] {
341 &self.measurement_rows
342 }
343
344 pub fn detector_rows(&self) -> &[Vec<usize>] {
346 &self.detector_rows
347 }
348
349 pub fn observable_rows(&self) -> &[Vec<usize>] {
351 &self.observable_rows
352 }
353
354 pub fn postselection_rows(&self) -> &[Vec<usize>] {
356 &self.postselection_rows
357 }
358
359 pub fn postselection_expected(&self) -> &[bool] {
361 &self.postselection_expected
362 }
363
364 pub fn postselection_predicates(&self) -> impl ExactSizeIterator<Item = (&[usize], bool)> + '_ {
366 self.postselection_rows
367 .iter()
368 .map(Vec::as_slice)
369 .zip(self.postselection_expected.iter().copied())
370 }
371
372 pub fn num_measurements(&self) -> usize {
374 self.measurement_rows.len()
375 }
376
377 pub fn num_detectors(&self) -> usize {
379 self.detector_rows.len()
380 }
381
382 pub fn num_observables(&self) -> usize {
384 self.observable_rows.len()
385 }
386
387 pub fn num_postselections(&self) -> usize {
389 self.postselection_rows.len()
390 }
391
392 pub fn packed_row_words(&self) -> usize {
394 self.num_qubits.div_ceil(64)
395 }
396
397 pub fn measurement_mask_bytes(&self) -> usize {
399 self.measurement_rows
400 .len()
401 .saturating_mul(self.packed_row_words())
402 .saturating_mul(2)
403 .saturating_mul(std::mem::size_of::<u64>())
404 }
405
406 pub fn detector_parities(&self, measurements: &PackedShots) -> Result<PackedShots> {
408 measurements.parity_rows(&self.detector_rows)
409 }
410
411 pub fn observable_parities(&self, measurements: &PackedShots) -> Result<PackedShots> {
413 measurements.parity_rows(&self.observable_rows)
414 }
415
416 pub fn postselection_parities(&self, measurements: &PackedShots) -> Result<PackedShots> {
418 measurements.parity_rows(&self.postselection_rows)
419 }
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
424pub struct QecOptions {
425 pub shots: usize,
427 pub seed: u64,
429 pub chunk_size: Option<usize>,
435 pub keep_measurements: bool,
441}
442
443impl Default for QecOptions {
444 fn default() -> Self {
445 Self {
446 shots: 1024,
447 seed: 42,
448 chunk_size: None,
449 keep_measurements: true,
450 }
451 }
452}
453
454#[derive(Debug, Clone, PartialEq)]
456pub struct QecProgram {
457 num_qubits: usize,
458 ops: Vec<QecOp>,
459 options: QecOptions,
460}
461
462impl QecProgram {
463 pub fn new(num_qubits: usize) -> Self {
465 Self::with_options(num_qubits, QecOptions::default())
466 }
467
468 pub fn with_options(num_qubits: usize, options: QecOptions) -> Self {
470 Self {
471 num_qubits,
472 ops: Vec::new(),
473 options,
474 }
475 }
476
477 pub fn from_ops(num_qubits: usize, options: QecOptions, ops: Vec<QecOp>) -> Result<Self> {
480 let mut program = Self::with_options(num_qubits, options);
481 let mut next_measurement = 0usize;
482 for op in ops {
483 program.validate_op(&op, next_measurement)?;
484 if matches!(
485 op,
486 QecOp::Measure { .. } | QecOp::MeasurePauliProduct { .. }
487 ) {
488 next_measurement += 1;
489 }
490 program.ops.push(op);
491 }
492 Ok(program)
493 }
494
495 pub fn from_text(input: &str) -> Result<Self> {
497 parse_qec_program(input)
498 }
499
500 pub fn num_qubits(&self) -> usize {
502 self.num_qubits
503 }
504
505 pub fn options(&self) -> QecOptions {
507 self.options
508 }
509
510 pub fn set_options(&mut self, options: QecOptions) {
512 self.options = options;
513 }
514
515 pub fn ops(&self) -> &[QecOp] {
517 &self.ops
518 }
519
520 pub fn num_measurements(&self) -> usize {
522 self.ops
523 .iter()
524 .filter(|op| {
525 matches!(
526 op,
527 QecOp::Measure { .. } | QecOp::MeasurePauliProduct { .. }
528 )
529 })
530 .count()
531 }
532
533 pub fn num_detectors(&self) -> usize {
535 self.ops
536 .iter()
537 .filter(|op| matches!(op, QecOp::Detector { .. }))
538 .count()
539 }
540
541 pub fn num_observables(&self) -> usize {
543 self.ops
544 .iter()
545 .filter_map(|op| match op {
546 QecOp::ObservableInclude { observable, .. } => Some(*observable),
547 _ => None,
548 })
549 .max()
550 .map_or(0, |max_idx| max_idx + 1)
551 }
552
553 pub fn push_op(&mut self, op: QecOp) -> Result<()> {
555 self.validate_op(&op, self.num_measurements())?;
556 self.ops.push(op);
557 Ok(())
558 }
559
560 pub fn push_gate(&mut self, gate: Gate, targets: &[usize]) -> Result<()> {
562 self.push_op(QecOp::Gate {
563 gate,
564 targets: targets.to_vec(),
565 })
566 }
567
568 pub fn reset(&mut self, basis: QecBasis, qubit: usize) -> Result<()> {
570 self.push_op(QecOp::Reset { basis, qubit })
571 }
572
573 pub fn measure(&mut self, basis: QecBasis, qubit: usize) -> Result<usize> {
575 let record = self.num_measurements();
576 self.push_op(QecOp::Measure { basis, qubit })?;
577 Ok(record)
578 }
579
580 pub fn measure_z(&mut self, qubit: usize) -> Result<usize> {
582 self.measure(QecBasis::Z, qubit)
583 }
584
585 pub fn measure_x(&mut self, qubit: usize) -> Result<usize> {
587 self.measure(QecBasis::X, qubit)
588 }
589
590 pub fn measure_pauli_product(&mut self, terms: &[QecPauli]) -> Result<usize> {
592 let record = self.num_measurements();
593 self.push_op(QecOp::MeasurePauliProduct {
594 terms: terms.to_vec(),
595 })?;
596 Ok(record)
597 }
598
599 pub fn detector(&mut self, records: &[QecRecordRef]) -> Result<usize> {
601 self.detector_with_coords(records, &[])
602 }
603
604 pub fn detector_with_coords(
606 &mut self,
607 records: &[QecRecordRef],
608 coords: &[f64],
609 ) -> Result<usize> {
610 let detector = self.num_detectors();
611 self.push_op(QecOp::Detector {
612 records: records.to_vec(),
613 coords: coords.to_vec(),
614 })?;
615 Ok(detector)
616 }
617
618 pub fn observable_include(
620 &mut self,
621 observable: usize,
622 records: &[QecRecordRef],
623 ) -> Result<()> {
624 self.push_op(QecOp::ObservableInclude {
625 observable,
626 records: records.to_vec(),
627 })
628 }
629
630 pub fn expectation_value(&mut self, terms: &[QecPauli], coefficient: f64) -> Result<()> {
632 self.push_op(QecOp::ExpectationValue {
633 terms: terms.to_vec(),
634 coefficient,
635 })
636 }
637
638 pub fn postselect(&mut self, records: &[QecRecordRef], expected: bool) -> Result<()> {
640 self.push_op(QecOp::Postselect {
641 records: records.to_vec(),
642 expected,
643 })
644 }
645
646 pub fn noise(&mut self, channel: QecNoise, targets: &[usize]) -> Result<()> {
648 self.push_op(QecOp::Noise {
649 channel,
650 targets: targets.to_vec(),
651 })
652 }
653
654 pub fn detector_rows(&self) -> Result<Vec<Vec<usize>>> {
656 let mut rows = Vec::new();
657 let mut next_measurement = 0;
658 for op in &self.ops {
659 match op {
660 QecOp::Measure { .. } | QecOp::MeasurePauliProduct { .. } => {
661 next_measurement += 1;
662 }
663 QecOp::Detector { records, .. } => {
664 rows.push(resolve_records(records, next_measurement)?);
665 }
666 _ => {}
667 }
668 }
669 Ok(rows)
670 }
671
672 pub fn observable_rows(&self) -> Result<Vec<Vec<usize>>> {
674 let mut rows = Vec::new();
675 let mut next_measurement = 0;
676 for op in &self.ops {
677 match op {
678 QecOp::Measure { .. } | QecOp::MeasurePauliProduct { .. } => {
679 next_measurement += 1;
680 }
681 QecOp::ObservableInclude {
682 observable,
683 records,
684 } => {
685 if rows.len() <= *observable {
686 rows.resize_with(*observable + 1, Vec::new);
687 }
688 rows[*observable].extend(resolve_records(records, next_measurement)?);
689 }
690 _ => {}
691 }
692 }
693 Ok(rows)
694 }
695
696 pub fn postselection_rows(&self) -> Result<Vec<(Vec<usize>, bool)>> {
698 let mut rows = Vec::new();
699 let mut next_measurement = 0;
700 for op in &self.ops {
701 match op {
702 QecOp::Measure { .. } | QecOp::MeasurePauliProduct { .. } => {
703 next_measurement += 1;
704 }
705 QecOp::Postselect { records, expected } => {
706 rows.push((resolve_records(records, next_measurement)?, *expected));
707 }
708 _ => {}
709 }
710 }
711 Ok(rows)
712 }
713
714 pub fn empty_result(&self) -> QecSampleResult {
716 QecSampleResult::empty(
717 self.num_measurements(),
718 self.num_detectors(),
719 self.num_observables(),
720 )
721 }
722
723 fn validate_op(&self, op: &QecOp, next_measurement: usize) -> Result<()> {
724 match op {
725 QecOp::Gate { gate, targets } => {
726 if gate.num_qubits() != targets.len() {
727 return Err(PrismError::GateArity {
728 gate: gate.name().to_string(),
729 expected: gate.num_qubits(),
730 got: targets.len(),
731 });
732 }
733 validate_qubits(targets.iter().copied(), self.num_qubits)?;
734 }
735 QecOp::Measure { qubit, .. } | QecOp::Reset { qubit, .. } => {
736 validate_qubit(*qubit, self.num_qubits)?;
737 }
738 QecOp::MeasurePauliProduct { terms } => {
739 if terms.is_empty() {
740 return Err(PrismError::InvalidParameter {
741 message: "Pauli-product measurement requires at least one term".to_string(),
742 });
743 }
744 validate_pauli_terms(terms, self.num_qubits)?;
745 }
746 QecOp::Detector { records, coords } => {
747 resolve_records(records, next_measurement)?;
748 validate_finite_values(coords, "detector coordinate")?;
749 }
750 QecOp::ObservableInclude { records, .. } | QecOp::Postselect { records, .. } => {
751 resolve_records(records, next_measurement)?;
752 }
753 QecOp::ExpectationValue { terms, coefficient } => {
754 if terms.is_empty() {
755 return Err(PrismError::InvalidParameter {
756 message: "expectation value requires at least one Pauli term".to_string(),
757 });
758 }
759 validate_pauli_terms(terms, self.num_qubits)?;
760 if !coefficient.is_finite() {
761 return Err(PrismError::InvalidParameter {
762 message: "expectation-value coefficient must be finite".to_string(),
763 });
764 }
765 }
766 QecOp::Noise { channel, targets } => {
767 validate_noise(*channel, targets, self.num_qubits)?;
768 }
769 QecOp::Tick => {}
770 }
771 Ok(())
772 }
773}
774
775pub fn compile_qec_program_rows(program: &QecProgram) -> Result<QecCompiledRows> {
788 let mut measurement_rows = Vec::with_capacity(program.num_measurements());
789
790 for op in program.ops() {
791 match op {
792 QecOp::Gate { gate, .. } => {
793 return Err(PrismError::IncompatibleBackend {
794 backend: "QEC row compiler".to_string(),
795 reason: format!(
796 "QEC row compilation does not lower gates yet, got `{}`",
797 gate.name()
798 ),
799 });
800 }
801 QecOp::Measure { basis, qubit } => {
802 measurement_rows.push(QecMeasurementRow::single(
803 program.num_qubits(),
804 *basis,
805 *qubit,
806 )?);
807 }
808 QecOp::MeasurePauliProduct { terms } => {
809 measurement_rows.push(QecMeasurementRow::from_terms(program.num_qubits(), terms)?);
810 }
811 QecOp::Reset { .. } => {
812 return Err(PrismError::IncompatibleBackend {
813 backend: "QEC row compiler".to_string(),
814 reason: "QEC row compilation does not lower resets yet".to_string(),
815 });
816 }
817 QecOp::ExpectationValue { .. } => {
818 return Err(PrismError::IncompatibleBackend {
819 backend: "QEC row compiler".to_string(),
820 reason: "QEC row compilation does not evaluate EXP_VAL yet".to_string(),
821 });
822 }
823 QecOp::Detector { .. }
824 | QecOp::ObservableInclude { .. }
825 | QecOp::Postselect { .. }
826 | QecOp::Tick => {}
827 QecOp::Noise { channel, .. } if channel.probability() == 0.0 => {}
828 QecOp::Noise { .. } => {
829 return Err(PrismError::IncompatibleBackend {
830 backend: "QEC row compiler".to_string(),
831 reason: "QEC row compilation does not support active noise annotations yet"
832 .to_string(),
833 });
834 }
835 }
836 }
837
838 let postselection_predicates = program.postselection_rows()?;
839 let mut postselection_rows = Vec::with_capacity(postselection_predicates.len());
840 let mut postselection_expected = Vec::with_capacity(postselection_predicates.len());
841 for (row, expected) in postselection_predicates {
842 postselection_rows.push(row);
843 postselection_expected.push(expected);
844 }
845
846 Ok(QecCompiledRows {
847 num_qubits: program.num_qubits(),
848 measurement_rows,
849 detector_rows: program.detector_rows()?,
850 observable_rows: program.observable_rows()?,
851 postselection_rows,
852 postselection_expected,
853 })
854}
855
856pub(super) fn append_basis_to_z_rotation(circuit: &mut Circuit, basis: QecBasis, qubit: usize) {
857 match basis {
858 QecBasis::X => circuit.add_gate(Gate::H, &[qubit]),
859 QecBasis::Y => {
860 circuit.add_gate(Gate::Sdg, &[qubit]);
861 circuit.add_gate(Gate::H, &[qubit]);
862 }
863 QecBasis::Z => {}
864 }
865}
866
867pub(super) fn append_z_to_basis_rotation(circuit: &mut Circuit, basis: QecBasis, qubit: usize) {
868 match basis {
869 QecBasis::X => circuit.add_gate(Gate::H, &[qubit]),
870 QecBasis::Y => {
871 circuit.add_gate(Gate::H, &[qubit]);
872 circuit.add_gate(Gate::S, &[qubit]);
873 }
874 QecBasis::Z => {}
875 }
876}
877
878fn resolve_records(records: &[QecRecordRef], next_measurement: usize) -> Result<Vec<usize>> {
879 records
880 .iter()
881 .map(|record| record.resolve(next_measurement))
882 .collect()
883}
884
885fn validate_qubit(qubit: usize, num_qubits: usize) -> Result<()> {
886 if qubit >= num_qubits {
887 return Err(PrismError::InvalidQubit {
888 index: qubit,
889 register_size: num_qubits,
890 });
891 }
892 Ok(())
893}
894
895fn validate_qubits<I>(qubits: I, num_qubits: usize) -> Result<()>
896where
897 I: IntoIterator<Item = usize>,
898{
899 for qubit in qubits {
900 validate_qubit(qubit, num_qubits)?;
901 }
902 Ok(())
903}
904
905fn validate_pauli_terms(terms: &[QecPauli], num_qubits: usize) -> Result<()> {
906 for (idx, term) in terms.iter().enumerate() {
907 validate_qubit(term.qubit, num_qubits)?;
908 if terms[..idx].iter().any(|prior| prior.qubit == term.qubit) {
909 return Err(PrismError::InvalidParameter {
910 message: format!(
911 "Pauli-product measurement contains duplicate qubit {}",
912 term.qubit
913 ),
914 });
915 }
916 }
917 Ok(())
918}
919
920fn validate_finite_values(values: &[f64], label: &str) -> Result<()> {
921 for value in values {
922 if !value.is_finite() {
923 return Err(PrismError::InvalidParameter {
924 message: format!("{label} must be finite"),
925 });
926 }
927 }
928 Ok(())
929}
930
931fn validate_noise(channel: QecNoise, targets: &[usize], num_qubits: usize) -> Result<()> {
932 let p = channel.probability();
933 if !(0.0..=1.0).contains(&p) || !p.is_finite() {
934 return Err(PrismError::InvalidParameter {
935 message: format!(
936 "{} probability must be finite and in [0, 1]",
937 channel.name()
938 ),
939 });
940 }
941
942 if targets.is_empty() {
943 return Err(PrismError::InvalidParameter {
944 message: format!("{} requires at least one target", channel.name()),
945 });
946 }
947
948 if matches!(channel, QecNoise::Depolarize2(_)) && targets.len() % 2 != 0 {
949 return Err(PrismError::InvalidParameter {
950 message: "DEPOLARIZE2 requires an even number of targets".to_string(),
951 });
952 }
953
954 if matches!(channel, QecNoise::Depolarize2(_)) {
955 for pair in targets.chunks_exact(2) {
956 if pair[0] == pair[1] {
957 return Err(PrismError::InvalidParameter {
958 message: "DEPOLARIZE2 target pairs must use distinct qubits".to_string(),
959 });
960 }
961 }
962 }
963
964 validate_qubits(targets.iter().copied(), num_qubits)
965}