Skip to main content

bloq_circuit/
circuit.rs

1use bloq_utils::PauliBasis;
2use glam::IVec2;
3use rustc_hash::{FxHashMap, FxHashSet};
4
5use crate::{
6    BodyId, CircuitBody, CircuitError, MeasRecord, MeasRegistry, MeasurementFrameError, Op,
7    gate::GateType,
8};
9
10/// A replay step produced by [`CoordCircuit::flatten`].
11///
12/// Events appear in emission order. Side-table owners replay them to re-scope
13/// loop-relative annotations onto the unrolled circuit.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum FlattenEvent {
16    /// A measurement occurrence was emitted: the pre-flatten id `original`
17    /// occurs here as `emitted`. Every occurrence except the last gets a fresh
18    /// id; the **final** occurrence keeps `emitted == original`, so a
19    /// pre-flatten id read from outside a loop (a top-level detector, a
20    /// boundary flow, an instance-space parity) still names the same record
21    /// the emission frame's latest-occurrence rule resolved it to.
22    Measurement {
23        /// Measurement id before flattening.
24        original: u32,
25        /// Measurement id assigned to this occurrence.
26        emitted: u32,
27    },
28    /// A `REPEAT` over `body` with at least one repetition began unrolling.
29    /// Loop-carried states resolve their `initial` parity here, before the
30    /// first iteration's measurements shadow anything they reference.
31    LoopEnter {
32        /// Body whose expansion begins.
33        body: BodyId,
34    },
35    /// One unrolled iteration of `body` finished. Body-scoped detectors are
36    /// due here, then loop-carried states advance by their `next` parity.
37    IterationEnd {
38        /// Body whose iteration ended.
39        body: BodyId,
40    },
41    /// A `REPEAT` over `body` finished. Side-table replay uses this to close
42    /// the loop-state frame and expose only this loop's declared final states
43    /// to its parent scope.
44    LoopExit {
45        /// Body whose expansion ended.
46        body: BodyId,
47    },
48}
49
50/// Work allowed when materializing a circuit's repeated operation stream.
51///
52/// Counts body visits, operations, operation targets, and replay events.
53/// The estimate is checked on the shared body DAG before unrolling.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct FlattenLimits {
56    /// Maximum charged expansion work.
57    pub max_work: usize,
58}
59
60impl Default for FlattenLimits {
61    fn default() -> Self {
62        Self {
63            max_work: 16 * 1024 * 1024,
64        }
65    }
66}
67
68/// A quantum circuit whose qubits are addressed by 2-D lattice coordinates
69/// rather than dense integer indices.
70///
71/// Operations live in [`CircuitBody`] blocks (the entry body plus any bodies
72/// referenced by [`Op::Repeat`]), stored flat and referenced by [`BodyId`].
73/// Measurement records are allocated through an internal [`MeasRegistry`], so
74/// every measurement has a stable id independent of qubit index. A
75/// coordinate-to-index layout for the Stim FFI boundary is derived on demand
76/// via [`Self::build_coord_to_index`].
77#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
78pub struct CoordCircuit {
79    meas_registry: MeasRegistry,
80    #[serde(deserialize_with = "deserialize_bodies")]
81    bodies: Vec<CircuitBody>,
82    entry: BodyId,
83}
84
85impl Default for CoordCircuit {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl CoordCircuit {
92    /// Creates an empty circuit with one entry body.
93    pub fn new() -> Self {
94        Self {
95            meas_registry: MeasRegistry::new(),
96            bodies: vec![CircuitBody::new()],
97            entry: BodyId(0),
98        }
99    }
100
101    /// The set of every qubit coordinate touched by any body or measurement
102    /// record.
103    pub fn qubits(&self) -> FxHashSet<IVec2> {
104        let mut occurrences = 0usize;
105        let mut min = IVec2::MAX;
106        let mut max = IVec2::MIN;
107        self.for_each_qubit(|qubit| {
108            occurrences += 1;
109            min = min.min(qubit);
110            max = max.max(qubit);
111        });
112        let mut qubits = FxHashSet::with_capacity_and_hasher(
113            self.meas_registry.records().len(),
114            Default::default(),
115        );
116        if occurrences == 0 {
117            return qubits;
118        }
119        let width = i64::from(max.x) - i64::from(min.x) + 1;
120        let height = i64::from(max.y) - i64::from(min.y) + 1;
121        let dense_area = width
122            .checked_mul(height)
123            .and_then(|area| usize::try_from(area).ok())
124            // Bound dense scratch by the number of coordinate visits.
125            .filter(|&area| area <= occurrences);
126
127        if let Some(area) = dense_area {
128            let mut seen = vec![0u8; area];
129            let width = width as usize;
130            self.for_each_qubit(|qubit| {
131                let x = (i64::from(qubit.x) - i64::from(min.x)) as usize;
132                let y = (i64::from(qubit.y) - i64::from(min.y)) as usize;
133                let index = y * width + x;
134                if seen[index] == 0 {
135                    seen[index] = 1;
136                    qubits.insert(qubit);
137                }
138            });
139        } else {
140            self.for_each_qubit(|qubit| {
141                qubits.insert(qubit);
142            });
143        }
144        qubits
145    }
146
147    /// The id of the top-level body operations are appended to.
148    pub const fn entry_body(&self) -> BodyId {
149        self.entry
150    }
151
152    /// Select an allocated body as the entry without renumbering body ids.
153    ///
154    /// # Errors
155    ///
156    /// Returns [`CircuitError::InvalidCircuitBody`] if `body` is not allocated.
157    pub fn set_entry_body(&mut self, body: BodyId) -> Result<(), CircuitError> {
158        self.body(body)
159            .ok_or(CircuitError::InvalidCircuitBody(body))?;
160        self.entry = body;
161        Ok(())
162    }
163
164    /// The repetition counts of the top-level [`Op::Repeat`]s in the entry
165    /// body, in order. Used to classify memory-padding templates: a one-round
166    /// template has none, a looped template has exactly one.
167    pub fn entry_top_level_repeats(&self) -> Vec<u32> {
168        self.body(self.entry)
169            .map(|body| {
170                body.ops()
171                    .iter()
172                    .filter_map(|op| match op {
173                        Op::Repeat { repetitions, .. } => Some(*repetitions),
174                        _ => None,
175                    })
176                    .collect()
177            })
178            .unwrap_or_default()
179    }
180
181    /// The number of circuit bodies, including repeat bodies.
182    pub fn body_count(&self) -> usize {
183        self.bodies.len()
184    }
185
186    /// Borrows the body with the given id, or `None` if it is not allocated.
187    pub fn body(&self, id: BodyId) -> Option<&CircuitBody> {
188        self.bodies.get(id.0 as usize)
189    }
190
191    /// Mutably borrows the body with the given id, or `None` if it is not
192    /// allocated.
193    pub fn body_mut(&mut self, id: BodyId) -> Option<&mut CircuitBody> {
194        self.bodies.get_mut(id.0 as usize)
195    }
196
197    /// Borrows the measurement registry backing this circuit's ids.
198    pub fn meas_registry(&self) -> &MeasRegistry {
199        &self.meas_registry
200    }
201
202    /// Allocates and returns a fresh measurement id for `qubit`.
203    pub fn reserve_measurement_id(&mut self, qubit: IVec2) -> u32 {
204        self.meas_registry.allocate_one(qubit)
205    }
206
207    #[doc(hidden)]
208    pub fn register_measurement_id(&mut self, id: u32, qubit: IVec2) {
209        self.meas_registry.reserve(id, qubit);
210    }
211
212    #[doc(hidden)]
213    pub fn register_measurement_records(&mut self, records: &mut [MeasRecord]) {
214        self.meas_registry.reserve_many(records);
215    }
216
217    /// Rewrites every measurement id through `remap`, in both the registry and
218    /// every op that references a measurement. Ids absent from `remap` are left
219    /// unchanged.
220    pub fn remap_measurement_ids(&mut self, remap: &rustc_hash::FxHashMap<u32, u32>) {
221        if remap.is_empty() {
222            return;
223        }
224
225        self.meas_registry.remap_ids(remap);
226        for body in &mut self.bodies {
227            for op in body.ops_mut() {
228                remap_op_measurement_ids(op, remap);
229            }
230        }
231    }
232
233    /// Appends `body` and returns its newly assigned [`BodyId`].
234    ///
235    /// # Panics
236    ///
237    /// Panics if the body-id space is exhausted.
238    pub fn add_body(&mut self, body: CircuitBody) -> BodyId {
239        let id = checked_body_id(self.bodies.len()).expect("body-id space is u32");
240        self.bodies.push(body);
241        id
242    }
243
244    fn entry_ops_mut(&mut self) -> &mut Vec<Op> {
245        self.body_mut(self.entry)
246            .expect("entry body is created with the circuit")
247            .ops_mut()
248    }
249
250    /// Build a deterministic coord-to-index layout for the Stim FFI boundary.
251    /// Indices are assigned by sorting coordinates by (x, y).
252    ///
253    /// # Panics
254    ///
255    /// Panics if the qubit-index space is exhausted.
256    pub fn build_coord_to_index(&self) -> FxHashMap<IVec2, u32> {
257        let mut coords = self.qubits().into_iter().collect::<Vec<_>>();
258        coords.sort_unstable_by_key(|coord| (coord.x, coord.y));
259        coords
260            .into_iter()
261            .enumerate()
262            .map(|(idx, coord)| (coord, u32::try_from(idx).expect("qubit-index space is u32")))
263            .collect()
264    }
265
266    /// The number of distinct qubit coordinates in the circuit.
267    ///
268    /// # Panics
269    ///
270    /// Panics if the qubit count exceeds `u32::MAX`.
271    pub fn num_qubits(&self) -> u32 {
272        u32::try_from(self.qubits().len()).expect("qubit count fits u32")
273    }
274
275    /// The number of allocated measurement records.
276    pub fn num_measurements(&self) -> u32 {
277        self.meas_registry.records().len() as u32
278    }
279
280    /// Appends a [`Op::Tick`] moment barrier to the entry body.
281    pub fn tick(&mut self) {
282        self.entry_ops_mut().push(Op::Tick);
283    }
284
285    /// Appends a [`Op::Repeat`] of `body` to the entry body.
286    pub fn push_repeat(&mut self, body: BodyId, repetitions: u32) {
287        self.entry_ops_mut().push(Op::Repeat { body, repetitions });
288    }
289
290    /// Appends a gate acting on `targets` to the entry body.
291    ///
292    /// Empty `targets` is a no-op. Two-qubit gates consume targets pairwise.
293    ///
294    /// # Errors
295    ///
296    /// Returns [`CircuitError::InvalidGateTargetCount`] if a two-qubit gate is
297    /// given an odd number of targets.
298    pub fn do_gate(
299        &mut self,
300        gate: GateType,
301        targets: impl IntoIterator<Item = IVec2>,
302    ) -> Result<(), CircuitError> {
303        let qubits: Vec<IVec2> = targets.into_iter().collect();
304        if qubits.is_empty() {
305            return Ok(());
306        }
307        if gate.is_two_qubit_gate() && !qubits.len().is_multiple_of(2) {
308            return Err(CircuitError::InvalidGateTargetCount {
309                gate,
310                targets: qubits.len(),
311            });
312        }
313
314        self.entry_ops_mut().push(Op::Gate { gate, qubits });
315        Ok(())
316    }
317
318    /// Appends a noiseless single-qubit measurement of each target in `basis`,
319    /// returning one freshly allocated measurement id per target (in target
320    /// order). [`crate::NoiseModel::noisy_circuit`] can add measurement noise.
321    ///
322    /// Empty `targets` is a no-op returning an empty vector; duplicate targets
323    /// each receive a distinct id.
324    pub fn measure(
325        &mut self,
326        basis: PauliBasis,
327        targets: impl IntoIterator<Item = IVec2>,
328    ) -> Vec<u32> {
329        let qubits = targets.into_iter().collect::<Vec<_>>();
330        if qubits.is_empty() {
331            return Vec::new();
332        }
333
334        let measurements = self.meas_registry.allocate(qubits.iter().copied());
335        self.entry_ops_mut().push(Op::Measure {
336            basis,
337            qubits,
338            measurements: measurements.clone(),
339            flip_probability: 0.0,
340        });
341        measurements
342    }
343
344    /// Append a multi-Pauli-product measurement ([`Op::MPP`]): each product is
345    /// measured jointly and produces one record, keyed by the product's
346    /// representative coordinate (see [`PauliMap::representative_coord`]) so
347    /// records stay consistent across cloning. Empty products are rejected.
348    ///
349    /// [`PauliMap::representative_coord`]: crate::PauliMap::representative_coord
350    ///
351    /// # Errors
352    ///
353    /// Returns [`CircuitError::EmptyPauliProduct`] without modifying the circuit
354    /// if any product is empty.
355    pub fn measure_pauli_products(
356        &mut self,
357        products: impl IntoIterator<Item = crate::PauliMap>,
358    ) -> Result<Vec<u32>, CircuitError> {
359        let products = products.into_iter().collect::<Vec<_>>();
360        if products.is_empty() {
361            return Ok(Vec::new());
362        }
363        let representatives = products
364            .iter()
365            .map(|product| {
366                product
367                    .representative_coord()
368                    .ok_or(CircuitError::EmptyPauliProduct)
369            })
370            .collect::<Result<Vec<_>, _>>()?;
371        let measurements = self.meas_registry.allocate(representatives);
372        self.entry_ops_mut().push(Op::MPP {
373            products,
374            measurements: measurements.clone(),
375        });
376        Ok(measurements)
377    }
378
379    /// The output column each measurement id lands in once every
380    /// [`Op::Repeat`] is unrolled, together with the total number of records
381    /// the unrolled circuit emits.
382    ///
383    /// Backends address measurement records by position, but this circuit
384    /// names them by id and hides repetition behind `Repeat`, so the two
385    /// spaces only line up after unrolling. [`Self::flatten`] also unrolls,
386    /// but it rewrites the circuit and mints fresh ids to do so; a caller that
387    /// only wants "which record does this id land in" pays neither cost here.
388    ///
389    /// A measurement inside a loop occurs once per iteration; the column
390    /// reported is its **last** occurrence, matching the latest-occurrence
391    /// rule both [`Self::flatten`] and the Stim emission frame use, so an id
392    /// read from outside a loop resolves to the same record everywhere.
393    ///
394    /// # Errors
395    ///
396    /// Returns [`CircuitError::InvalidCircuitBody`] on a dangling or
397    /// self-recursive body reference, and
398    /// [`MeasurementFrameError::EmittedMeasurementCountOverflow`] when the
399    /// unrolled circuit would emit more than `u32::MAX` records, or
400    /// [`MeasurementFrameError::MeasurementCountOutOfRange`] if one operation
401    /// already contains more than `u32::MAX` records.
402    ///
403    /// [`MeasurementFrameError::EmittedMeasurementCountOverflow`]: crate::MeasurementFrameError
404    ///
405    /// # Examples
406    ///
407    /// ```
408    /// # use bloq_circuit::{CircuitBody, CoordCircuit, Op, PauliBasis};
409    /// # use glam::ivec2;
410    /// let qubit = ivec2(0, 0);
411    /// let mut circuit = CoordCircuit::new();
412    /// let looped = circuit.reserve_measurement_id(qubit);
413    /// let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
414    ///     basis: PauliBasis::Z,
415    ///     qubits: vec![qubit],
416    ///     measurements: vec![looped],
417    ///     flip_probability: 0.0,
418    /// }]));
419    /// circuit.push_repeat(body, 3);
420    ///
421    /// let columns = circuit.expanded_measurement_columns()?;
422    /// assert_eq!(columns.count(), 3);
423    /// assert_eq!(columns.column(looped), Some(2), "the last of three occurrences");
424    /// # Ok::<(), bloq_circuit::CircuitError>(())
425    /// ```
426    pub fn expanded_measurement_columns(&self) -> Result<ExpandedMeasurementColumns, CircuitError> {
427        let mut walk = ExpandedColumnWalk {
428            circuit: self,
429            columns: FxHashMap::default(),
430            count: 0,
431            body_counts: FxHashMap::default(),
432            stack: Vec::new(),
433        };
434        walk.visit(self.entry)?;
435        Ok(ExpandedMeasurementColumns {
436            columns: walk.columns,
437            count: walk.count,
438        })
439    }
440
441    /// Whether any body contains an [`Op::Repeat`].
442    pub fn has_repeats(&self) -> bool {
443        self.bodies
444            .iter()
445            .any(|body| body.ops().iter().any(|op| matches!(op, Op::Repeat { .. })))
446    }
447
448    /// Unroll every [`Op::Repeat`] into a single straight-line entry body,
449    /// returning the replay trace side-table owners need to translate
450    /// loop-relative annotations (see [`FlattenEvent`]).
451    ///
452    /// Measurement-id semantics mirror the emission frame's
453    /// latest-occurrence rule: every unrolled occurrence of a measurement gets
454    /// a fresh id **except the last, which keeps the original id**, so
455    /// references from outside the loop (which always meant "the latest
456    /// occurrence") stay valid without any remap. `ConditionalPauli`
457    /// measurement controls are rewritten to the occurrence emitted most
458    /// recently before them. Zero-repetition repeats are dropped.
459    ///
460    /// A circuit without repeats is returned untouched with an empty trace.
461    ///
462    /// # Errors
463    ///
464    /// [`CircuitError::InvalidCircuitBody`] on a dangling or self-recursive
465    /// body reference, [`CircuitError::FlattenMeasurementIdOverflow`] when the
466    /// unrolled circuit would need more than `u32::MAX` measurement ids, or
467    /// [`CircuitError::FlattenResourceLimit`] when expansion exceeds the default
468    /// [`FlattenLimits`].
469    pub fn flatten(&mut self) -> Result<Vec<FlattenEvent>, CircuitError> {
470        self.flatten_with_limits(FlattenLimits::default())
471    }
472
473    /// [`Self::flatten`] with an explicit allowance for expanded work.
474    ///
475    /// The shared body DAG is counted before the circuit is changed. Rejected
476    /// expansions leave both operations and measurement identities untouched.
477    ///
478    /// # Errors
479    ///
480    /// Returns the same body, id-space, and resource errors as [`Self::flatten`].
481    pub fn flatten_with_limits(
482        &mut self,
483        limits: FlattenLimits,
484    ) -> Result<Vec<FlattenEvent>, CircuitError> {
485        if !self.has_repeats() {
486            return Ok(Vec::new());
487        }
488
489        let (mut counts, _) = self.flatten_counts(limits)?;
490
491        // Pass 2: unroll. The bodies are moved out so fresh-id allocation can
492        // borrow the registry mutably while walking them.
493        let bodies = std::mem::take(&mut self.bodies);
494        let mut out = Vec::new();
495        let mut trace = Vec::new();
496        let mut latest = FxHashMap::default();
497        let result = self.unroll_body(
498            &bodies,
499            self.entry,
500            &mut counts,
501            &mut latest,
502            &mut out,
503            &mut trace,
504        );
505        match result {
506            Ok(()) => {
507                self.bodies = vec![CircuitBody::from_ops(out)];
508                self.entry = BodyId(0);
509                Ok(trace)
510            }
511            Err(error) => {
512                // Leave the circuit structurally intact on failure (freshly
513                // allocated ids may remain in the registry, but no op uses them).
514                self.bodies = bodies;
515                Err(error)
516            }
517        }
518    }
519
520    /// Check expansion without changing the circuit and return its work cost.
521    /// A caller flattening several circuits can charge the returned cost to
522    /// one shared allowance before materializing any of them.
523    ///
524    /// # Errors
525    ///
526    /// Returns the same body, id-space, and resource errors as [`Self::flatten`].
527    pub fn check_flatten_limits(&self, limits: FlattenLimits) -> Result<usize, CircuitError> {
528        if !self.has_repeats() {
529            return Ok(0);
530        }
531        self.flatten_counts(limits).map(|(_, work)| work)
532    }
533
534    fn flatten_counts(
535        &self,
536        limits: FlattenLimits,
537    ) -> Result<(FxHashMap<u32, u64>, usize), CircuitError> {
538        // Visit shared bodies once, without using the native call stack. Zero
539        // repetitions contribute no executed references, matching unrolling.
540        let mut order = Vec::new();
541        let mut active = FxHashSet::default();
542        let mut visited = FxHashSet::default();
543        let mut pending = vec![(self.entry, 0usize)];
544        active.insert(self.entry);
545        while let Some((body, next)) = pending.last_mut() {
546            let ops = self
547                .body(*body)
548                .ok_or(CircuitError::InvalidCircuitBody(*body))?
549                .ops();
550            let Some(op) = ops.get(*next) else {
551                let body = *body;
552                pending.pop();
553                active.remove(&body);
554                visited.insert(body);
555                order.push(body);
556                continue;
557            };
558            *next += 1;
559            if let Op::Repeat { body, repetitions } = op
560                && *repetitions != 0
561                && !visited.contains(body)
562            {
563                if !active.insert(*body) {
564                    return Err(CircuitError::InvalidCircuitBody(*body));
565                }
566                pending.push((*body, 0));
567            }
568        }
569
570        // Propagate invocation multiplicities along DAG edges, rather than
571        // revisiting a body for every path through the DAG.
572        let mut measured_bodies = FxHashSet::default();
573        for &body in &order {
574            if self
575                .body(body)
576                .expect("body order was checked")
577                .ops()
578                .iter()
579                .any(|op| match op {
580                    Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => {
581                        !measurements.is_empty()
582                    }
583                    Op::Repeat { body, repetitions } => {
584                        *repetitions != 0 && measured_bodies.contains(body)
585                    }
586                    _ => false,
587                })
588            {
589                measured_bodies.insert(body);
590            }
591        }
592        let mut invocations = FxHashMap::default();
593        invocations.insert(self.entry, 1u64);
594        let mut counts = FxHashMap::default();
595        for &body in order.iter().rev() {
596            let Some(&multiplier) = invocations.get(&body) else {
597                continue;
598            };
599            for op in self.body(body).expect("body order was checked").ops() {
600                match op {
601                    Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => {
602                        for &measurement in measurements {
603                            let count = counts.entry(measurement).or_insert(0u64);
604                            *count = count
605                                .checked_add(multiplier)
606                                .ok_or(CircuitError::FlattenMeasurementIdOverflow)?;
607                        }
608                    }
609                    Op::Repeat { body, repetitions }
610                        if *repetitions != 0 && measured_bodies.contains(body) =>
611                    {
612                        let extra = multiplier
613                            .checked_mul(u64::from(*repetitions))
614                            .ok_or(CircuitError::FlattenMeasurementIdOverflow)?;
615                        let count = invocations.entry(*body).or_insert(0u64);
616                        *count = count
617                            .checked_add(extra)
618                            .ok_or(CircuitError::FlattenMeasurementIdOverflow)?;
619                    }
620                    _ => {}
621                }
622            }
623        }
624        let fresh_needed = counts
625            .values()
626            .try_fold(0u64, |total, &count| total.checked_add(count - 1))
627            .ok_or(CircuitError::FlattenMeasurementIdOverflow)?;
628        if fresh_needed > u64::from(u32::MAX - self.meas_registry.next_id()) {
629            return Err(CircuitError::FlattenMeasurementIdOverflow);
630        }
631
632        let mut work_by_body = FxHashMap::<BodyId, usize>::default();
633        for body in order {
634            let mut work = 1usize; // Visiting even an empty body costs work.
635            for op in self.body(body).expect("body order was checked").ops() {
636                let extra = match op {
637                    Op::Repeat { body, repetitions } if *repetitions != 0 => {
638                        // LoopEnter/LoopExit plus a body visit and IterationEnd
639                        // for each repetition. Empty repeats are not free.
640                        work_by_body[body]
641                            .saturating_add(1)
642                            .saturating_mul(*repetitions as usize)
643                            .saturating_add(2)
644                    }
645                    Op::Measure {
646                        qubits,
647                        measurements,
648                        ..
649                    } => qubits
650                        .len()
651                        .saturating_add(measurements.len().saturating_mul(2)),
652                    Op::MPP {
653                        products,
654                        measurements,
655                    } => products
656                        .iter()
657                        .fold(products.len(), |total, product| {
658                            total.saturating_add(product.len())
659                        })
660                        .saturating_add(measurements.len().saturating_mul(2)),
661                    Op::Gate { qubits, .. }
662                    | Op::Depolarize1 { qubits, .. }
663                    | Op::Depolarize2 { qubits, .. }
664                    | Op::PauliError { qubits, .. } => qubits.len(),
665                    Op::ConditionalPauli(corrections) => corrections.len(),
666                    Op::Tick | Op::Repeat { .. } => 0,
667                };
668                work = work.saturating_add(1).saturating_add(extra);
669            }
670            work_by_body.insert(body, work);
671        }
672        let observed = work_by_body[&self.entry];
673        if observed == usize::MAX || observed > limits.max_work {
674            return Err(CircuitError::FlattenResourceLimit {
675                observed,
676                limit: limits.max_work,
677            });
678        }
679        Ok((counts, observed))
680    }
681
682    fn unroll_body(
683        &mut self,
684        bodies: &[CircuitBody],
685        body: BodyId,
686        counts: &mut FxHashMap<u32, u64>,
687        latest: &mut FxHashMap<u32, u32>,
688        out: &mut Vec<Op>,
689        trace: &mut Vec<FlattenEvent>,
690    ) -> Result<(), CircuitError> {
691        let mut pending = vec![(body, 0usize, 1u32, false)];
692        while let Some((body, next, remaining, repeated)) = pending.last_mut() {
693            let ops = bodies
694                .get(body.0 as usize)
695                .ok_or(CircuitError::InvalidCircuitBody(*body))?
696                .ops();
697            let Some(op) = ops.get(*next) else {
698                if *repeated {
699                    trace.push(FlattenEvent::IterationEnd { body: *body });
700                }
701                if *remaining > 1 {
702                    *remaining -= 1;
703                    *next = 0;
704                } else {
705                    if *repeated {
706                        trace.push(FlattenEvent::LoopExit { body: *body });
707                    }
708                    pending.pop();
709                }
710                continue;
711            };
712            *next += 1;
713            match op {
714                Op::Repeat { body, repetitions } => {
715                    if *repetitions == 0 {
716                        continue;
717                    }
718                    trace.push(FlattenEvent::LoopEnter { body: *body });
719                    pending.push((*body, 0, *repetitions, true));
720                }
721                Op::Measure {
722                    basis,
723                    qubits,
724                    measurements,
725                    flip_probability,
726                } => {
727                    let measurements = self.remap_unrolled_measurements(
728                        measurements,
729                        qubits.iter().copied(),
730                        counts,
731                        latest,
732                        trace,
733                    );
734                    out.push(Op::Measure {
735                        basis: *basis,
736                        qubits: qubits.clone(),
737                        measurements,
738                        flip_probability: *flip_probability,
739                    });
740                }
741                Op::MPP {
742                    products,
743                    measurements,
744                } => {
745                    let record_qubits = products
746                        .iter()
747                        .map(|product| {
748                            product
749                                .representative_coord()
750                                .ok_or(CircuitError::EmptyPauliProduct)
751                        })
752                        .collect::<Result<Vec<_>, _>>()?;
753                    let measurements = self.remap_unrolled_measurements(
754                        measurements,
755                        record_qubits,
756                        counts,
757                        latest,
758                        trace,
759                    );
760                    out.push(Op::MPP {
761                        products: products.clone(),
762                        measurements,
763                    });
764                }
765                // A measurement control refers to the occurrence emitted most
766                // recently before it (the current iteration's id) — exactly the
767                // `latest` remap, which `remap_op_measurement_ids` applies while
768                // passing `Value` controls through. Absent ids stay unchanged.
769                Op::ConditionalPauli(_) => {
770                    let mut op = op.clone();
771                    remap_op_measurement_ids(&mut op, latest);
772                    out.push(op);
773                }
774                Op::Gate { .. }
775                | Op::Depolarize1 { .. }
776                | Op::Depolarize2 { .. }
777                | Op::PauliError { .. }
778                | Op::Tick => out.push(op.clone()),
779            }
780        }
781        Ok(())
782    }
783
784    /// Emit one unrolled occurrence of each measurement: fresh ids until the
785    /// countdown says this is the final occurrence, which keeps the original.
786    fn remap_unrolled_measurements(
787        &mut self,
788        measurements: &[u32],
789        record_qubits: impl IntoIterator<Item = IVec2>,
790        counts: &mut FxHashMap<u32, u64>,
791        latest: &mut FxHashMap<u32, u32>,
792        trace: &mut Vec<FlattenEvent>,
793    ) -> Vec<u32> {
794        measurements
795            .iter()
796            .zip(record_qubits)
797            .map(|(&original, qubit)| {
798                let remaining = counts
799                    .get_mut(&original)
800                    .expect("pass 1 counted every measurement pass 2 visits");
801                *remaining -= 1;
802                let emitted = if *remaining == 0 {
803                    original
804                } else {
805                    self.meas_registry.allocate_one(qubit)
806                };
807                latest.insert(original, emitted);
808                trace.push(FlattenEvent::Measurement { original, emitted });
809                emitted
810            })
811            .collect()
812    }
813
814    /// Checks if there are any operations that act on the same qubit within
815    /// the same moment. A repeat does not imply a tick, including between its
816    /// iterations.
817    pub fn has_moments_conflict(&self) -> bool {
818        self.body_moment_summary(self.entry, &mut FxHashMap::default(), &mut Vec::new())
819            .is_none_or(|summary| summary.conflict)
820    }
821
822    fn body_moment_summary(
823        &self,
824        body: BodyId,
825        cache: &mut FxHashMap<BodyId, MomentSummary>,
826        stack: &mut Vec<BodyId>,
827    ) -> Option<MomentSummary> {
828        if let Some(summary) = cache.get(&body) {
829            return Some(summary.clone());
830        }
831        if stack.contains(&body) {
832            return None;
833        }
834        let body_ops = self.body(body)?.ops();
835        stack.push(body);
836        let mut summary = MomentSummary::default();
837
838        for op in body_ops {
839            let next = match op {
840                Op::Tick => MomentSummary::tick(),
841                Op::Repeat { body, repetitions } => {
842                    if *repetitions == 0 {
843                        continue;
844                    }
845                    let Some(body) = self.body_moment_summary(*body, cache, stack) else {
846                        stack.pop();
847                        return None;
848                    };
849                    body.repeated(*repetitions)
850                }
851                Op::Gate { qubits, .. } | Op::Measure { qubits, .. } => {
852                    MomentSummary::from_qubits(qubits)
853                }
854                // Noise decorates physical operations in the same moment; it
855                // does not claim the qubit a second time.
856                Op::Depolarize1 { .. } | Op::Depolarize2 { .. } | Op::PauliError { .. } => {
857                    MomentSummary::default()
858                }
859                // MPP and ConditionalPauli may legally share qubits within a
860                // moment, so they don't contribute to conflict detection.
861                Op::ConditionalPauli(_) | Op::MPP { .. } => MomentSummary::default(),
862            };
863            summary.append(next);
864        }
865
866        stack.pop();
867        cache.insert(body, summary.clone());
868        Some(summary)
869    }
870
871    fn for_each_qubit(&self, mut visit: impl FnMut(IVec2)) {
872        for body in &self.bodies {
873            for op in body.ops() {
874                match op {
875                    Op::Gate { qubits, .. }
876                    | Op::Measure { qubits, .. }
877                    | Op::Depolarize1 { qubits, .. }
878                    | Op::Depolarize2 { qubits, .. }
879                    | Op::PauliError { qubits, .. } => {
880                        qubits.iter().copied().for_each(&mut visit);
881                    }
882                    Op::ConditionalPauli(corrections) => {
883                        corrections
884                            .iter()
885                            .map(|correction| correction.target)
886                            .for_each(&mut visit);
887                    }
888                    Op::MPP { products, .. } => {
889                        for product in products {
890                            product.iter().map(|(coord, _)| *coord).for_each(&mut visit);
891                        }
892                    }
893                    Op::Repeat { .. } | Op::Tick => {}
894                }
895            }
896        }
897        self.meas_registry
898            .records()
899            .iter()
900            .map(|record| record.qubit)
901            .for_each(visit);
902    }
903
904    fn fmt_body(
905        &self,
906        f: &mut std::fmt::Formatter<'_>,
907        body: BodyId,
908        indent: usize,
909    ) -> std::fmt::Result {
910        let body = self.body(body).ok_or(std::fmt::Error)?;
911        self.fmt_ops(f, body.ops(), indent)
912    }
913
914    fn fmt_ops(
915        &self,
916        f: &mut std::fmt::Formatter<'_>,
917        ops: &[Op],
918        indent: usize,
919    ) -> std::fmt::Result {
920        for (index, op) in ops.iter().enumerate() {
921            if index > 0 {
922                writeln!(f)?;
923            }
924            self.fmt_op(f, op, indent)?;
925        }
926        Ok(())
927    }
928
929    fn fmt_op(&self, f: &mut std::fmt::Formatter<'_>, op: &Op, indent: usize) -> std::fmt::Result {
930        write_indent(f, indent)?;
931        match op {
932            Op::Gate { gate, qubits } => {
933                write!(f, "{gate}")?;
934                write_coord_targets(f, qubits)
935            }
936            Op::Measure {
937                basis,
938                qubits,
939                flip_probability,
940                ..
941            } => {
942                write!(f, "{}", measurement_gate_name(*basis))?;
943                if *flip_probability != 0.0 {
944                    write!(f, "({flip_probability})")?;
945                }
946                write_coord_targets(f, qubits)
947            }
948            Op::MPP { products, .. } => {
949                write!(f, "MPP")?;
950                for product in products {
951                    write!(f, " ")?;
952                    for (index, (coord, pauli)) in product.iter().enumerate() {
953                        if index > 0 {
954                            write!(f, "*")?;
955                        }
956                        write!(f, "{pauli}({},{})", coord.x, coord.y)?;
957                    }
958                }
959                Ok(())
960            }
961            Op::Depolarize1 {
962                probability,
963                qubits,
964            } => {
965                write!(f, "DEPOLARIZE1({probability})")?;
966                write_coord_targets(f, qubits)
967            }
968            Op::Depolarize2 {
969                probability,
970                qubits,
971            } => {
972                write!(f, "DEPOLARIZE2({probability})")?;
973                write_coord_targets(f, qubits)
974            }
975            Op::PauliError {
976                probability,
977                pauli,
978                qubits,
979            } => {
980                write!(f, "{pauli}_ERROR({probability})")?;
981                write_coord_targets(f, qubits)
982            }
983            Op::Repeat { body, repetitions } => {
984                write!(f, "REPEAT {repetitions} {{")?;
985                let repeat_body = self.body(*body).ok_or(std::fmt::Error)?;
986                if !repeat_body.ops().is_empty() {
987                    writeln!(f)?;
988                    self.fmt_ops(f, repeat_body.ops(), indent + 1)?;
989                    writeln!(f)?;
990                    write_indent(f, indent)?;
991                }
992                write!(f, "}}")
993            }
994            Op::Tick => write!(f, "TICK"),
995            Op::ConditionalPauli(corrections) => {
996                write!(f, "CPAULI")?;
997                for correction in corrections {
998                    write!(
999                        f,
1000                        " {}[{}]({},{})",
1001                        correction.pauli,
1002                        correction.control,
1003                        correction.target.x,
1004                        correction.target.y
1005                    )?;
1006                }
1007                Ok(())
1008            }
1009        }
1010    }
1011}
1012
1013#[derive(Clone, Default)]
1014struct MomentSummary {
1015    first: FxHashSet<IVec2>,
1016    last: FxHashSet<IVec2>,
1017    has_tick: bool,
1018    conflict: bool,
1019}
1020
1021impl MomentSummary {
1022    fn from_qubits(qubits: &[IVec2]) -> Self {
1023        let first: FxHashSet<_> = qubits.iter().copied().collect();
1024        Self {
1025            last: first.clone(),
1026            conflict: first.len() != qubits.len(),
1027            first,
1028            has_tick: false,
1029        }
1030    }
1031
1032    fn tick() -> Self {
1033        Self {
1034            has_tick: true,
1035            ..Self::default()
1036        }
1037    }
1038
1039    fn repeated(mut self, repetitions: u32) -> Self {
1040        if repetitions > 1 && !self.first.is_disjoint(&self.last) {
1041            self.conflict = true;
1042        }
1043        self
1044    }
1045
1046    fn append(&mut self, next: Self) {
1047        self.conflict |= next.conflict || !self.last.is_disjoint(&next.first);
1048        if !self.has_tick {
1049            self.first.extend(next.first.iter().copied());
1050        }
1051        if next.has_tick {
1052            self.last = next.last;
1053        } else {
1054            self.last.extend(next.last);
1055        }
1056        self.has_tick |= next.has_tick;
1057    }
1058}
1059
1060/// Where each measurement id lands once every [`Op::Repeat`] is unrolled — the
1061/// result of [`CoordCircuit::expanded_measurement_columns`].
1062#[derive(Debug, Clone, Default, PartialEq, Eq)]
1063pub struct ExpandedMeasurementColumns {
1064    columns: FxHashMap<u32, u32>,
1065    count: u32,
1066}
1067
1068impl ExpandedMeasurementColumns {
1069    /// The output column of `measurement`'s last occurrence, or `None` if the
1070    /// circuit never emits it.
1071    pub fn column(&self, measurement: u32) -> Option<u32> {
1072        self.columns.get(&measurement).copied()
1073    }
1074
1075    /// How many measurement records the unrolled circuit emits.
1076    pub const fn count(&self) -> u32 {
1077        self.count
1078    }
1079
1080    /// Every `(measurement, column)` pair, in unspecified order.
1081    pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
1082        self.columns
1083            .iter()
1084            .map(|(&measurement, &column)| (measurement, column))
1085    }
1086}
1087
1088/// Accumulator for [`CoordCircuit::expanded_measurement_columns`].
1089struct ExpandedColumnWalk<'a> {
1090    circuit: &'a CoordCircuit,
1091    columns: FxHashMap<u32, u32>,
1092    count: u32,
1093    /// Unrolled record count per body, memoized: loop bodies are visited once
1094    /// to count and once to record, and a body may be shared by several loops.
1095    body_counts: FxHashMap<BodyId, u32>,
1096    stack: Vec<BodyId>,
1097}
1098
1099impl ExpandedColumnWalk<'_> {
1100    fn visit(&mut self, body: BodyId) -> Result<(), CircuitError> {
1101        // A body reachable from itself would unroll forever; validated
1102        // circuits are acyclic, so this guards hand-built ones.
1103        if self.stack.contains(&body) {
1104            return Err(CircuitError::InvalidCircuitBody(body));
1105        }
1106        self.stack.push(body);
1107        let ops = self
1108            .circuit
1109            .body(body)
1110            .ok_or(CircuitError::InvalidCircuitBody(body))?
1111            .ops();
1112        for op in ops {
1113            match op {
1114                Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => {
1115                    for &measurement in measurements {
1116                        self.columns.insert(measurement, self.count);
1117                        self.count = advance(self.count, 1)?;
1118                    }
1119                }
1120                Op::Repeat { body, repetitions } => {
1121                    if *repetitions == 0 {
1122                        continue;
1123                    }
1124                    // Only the final iteration's columns survive the
1125                    // latest-occurrence rule, and the earlier ones do nothing
1126                    // but advance the column counter — which is a
1127                    // multiplication. So skip to the last iteration instead of
1128                    // walking a loop that may repeat billions of times.
1129                    let per_iteration = self.body_count(*body)?;
1130                    let skipped = per_iteration.checked_mul(*repetitions - 1).ok_or(
1131                        MeasurementFrameError::RepeatedMeasurementCountOverflow {
1132                            measurements_per_iteration: per_iteration,
1133                            repetitions: *repetitions,
1134                        },
1135                    )?;
1136                    self.count = advance(self.count, skipped)?;
1137                    self.visit(*body)?;
1138                }
1139                Op::Gate { .. }
1140                | Op::Depolarize1 { .. }
1141                | Op::Depolarize2 { .. }
1142                | Op::PauliError { .. }
1143                | Op::Tick
1144                | Op::ConditionalPauli(_) => {}
1145            }
1146        }
1147        self.stack.pop();
1148        Ok(())
1149    }
1150
1151    /// How many records one unrolled iteration of `body` emits.
1152    fn body_count(&mut self, body: BodyId) -> Result<u32, CircuitError> {
1153        if let Some(&count) = self.body_counts.get(&body) {
1154            return Ok(count);
1155        }
1156        if self.stack.contains(&body) {
1157            return Err(CircuitError::InvalidCircuitBody(body));
1158        }
1159        self.stack.push(body);
1160        let ops = self
1161            .circuit
1162            .body(body)
1163            .ok_or(CircuitError::InvalidCircuitBody(body))?
1164            .ops();
1165        let mut count = 0u32;
1166        for op in ops {
1167            match op {
1168                Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => {
1169                    count = advance(count, measurement_count(measurements.len())?)?;
1170                }
1171                Op::Repeat { body, repetitions } => {
1172                    let per_iteration = self.body_count(*body)?;
1173                    let total = per_iteration.checked_mul(*repetitions).ok_or(
1174                        MeasurementFrameError::RepeatedMeasurementCountOverflow {
1175                            measurements_per_iteration: per_iteration,
1176                            repetitions: *repetitions,
1177                        },
1178                    )?;
1179                    count = advance(count, total)?;
1180                }
1181                Op::Gate { .. }
1182                | Op::Depolarize1 { .. }
1183                | Op::Depolarize2 { .. }
1184                | Op::PauliError { .. }
1185                | Op::Tick
1186                | Op::ConditionalPauli(_) => {}
1187            }
1188        }
1189        self.stack.pop();
1190        self.body_counts.insert(body, count);
1191        Ok(count)
1192    }
1193}
1194
1195fn advance(count: u32, additional: u32) -> Result<u32, MeasurementFrameError> {
1196    count
1197        .checked_add(additional)
1198        .ok_or(MeasurementFrameError::EmittedMeasurementCountOverflow {
1199            emitted_count: count,
1200            additional,
1201        })
1202}
1203
1204fn measurement_count(measurements: usize) -> Result<u32, MeasurementFrameError> {
1205    u32::try_from(measurements)
1206        .map_err(|_| MeasurementFrameError::MeasurementCountOutOfRange { measurements })
1207}
1208
1209pub(crate) fn checked_body_id(index: usize) -> Result<BodyId, CircuitError> {
1210    u32::try_from(index)
1211        .map(BodyId)
1212        .map_err(|_| CircuitError::BodyIdOutOfRange { index })
1213}
1214
1215fn deserialize_bodies<'de, D: serde::Deserializer<'de>>(
1216    deserializer: D,
1217) -> Result<Vec<CircuitBody>, D::Error> {
1218    let bodies = <Vec<CircuitBody> as serde::Deserialize>::deserialize(deserializer)?;
1219    if let Some(last) = bodies.len().checked_sub(1) {
1220        checked_body_id(last).map_err(serde::de::Error::custom)?;
1221    }
1222    Ok(bodies)
1223}
1224
1225fn remap_op_measurement_ids(op: &mut Op, remap: &rustc_hash::FxHashMap<u32, u32>) {
1226    match op {
1227        // An MPP produces measurement records, so its ids follow the same
1228        // remap as ordinary measurements.
1229        Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => {
1230            for measurement in measurements {
1231                if let Some(mapped) = remap.get(measurement).copied() {
1232                    *measurement = mapped;
1233                }
1234            }
1235        }
1236        // A conditional Pauli's control follows the same remap as the
1237        // measurement that produced it.
1238        Op::ConditionalPauli(corrections) => {
1239            for correction in corrections {
1240                if let Some(mapped) = remap.get(&correction.control).copied() {
1241                    correction.control = mapped;
1242                }
1243            }
1244        }
1245        Op::Gate { .. }
1246        | Op::Repeat { .. }
1247        | Op::Depolarize1 { .. }
1248        | Op::Depolarize2 { .. }
1249        | Op::PauliError { .. }
1250        | Op::Tick => {}
1251    }
1252}
1253
1254impl std::fmt::Display for CoordCircuit {
1255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1256        self.fmt_body(f, self.entry, 0)
1257    }
1258}
1259
1260/// The Stim gate name that measures in `basis`.
1261pub fn measurement_gate_name(basis: PauliBasis) -> &'static str {
1262    match basis {
1263        PauliBasis::X => "MX",
1264        PauliBasis::Y => "MY",
1265        PauliBasis::Z => "M",
1266    }
1267}
1268
1269fn write_indent(f: &mut std::fmt::Formatter<'_>, indent: usize) -> std::fmt::Result {
1270    for _ in 0..indent {
1271        write!(f, "    ")?;
1272    }
1273    Ok(())
1274}
1275
1276fn write_coord_targets(f: &mut std::fmt::Formatter<'_>, qubits: &[IVec2]) -> std::fmt::Result {
1277    for &coord in qubits {
1278        write!(f, " ({},{})", coord.x, coord.y)?;
1279    }
1280    Ok(())
1281}
1282
1283#[cfg(test)]
1284mod tests {
1285    use super::*;
1286
1287    #[test]
1288    fn rejected_mpp_keeps_the_circuit_and_next_record_unchanged() {
1289        let mut circuit = CoordCircuit::new();
1290        circuit.measure(PauliBasis::Z, [IVec2::ZERO]);
1291        let before = circuit.clone();
1292        let product = crate::PauliMap::from_iter([(IVec2::X, crate::Pauli::X)]);
1293
1294        assert_eq!(
1295            circuit.measure_pauli_products([product.clone(), crate::PauliMap::empty()]),
1296            Err(CircuitError::EmptyPauliProduct)
1297        );
1298        assert_eq!(circuit, before);
1299        assert_eq!(circuit.measure_pauli_products([product]).unwrap(), [1]);
1300    }
1301
1302    #[test]
1303    fn circuit_roundtrip_retains_body_ids_and_entry() {
1304        let mut circuit = CoordCircuit::new();
1305        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Tick]));
1306        circuit.set_entry_body(body).unwrap();
1307        let encoded = postcard::to_allocvec(&circuit).unwrap();
1308        let decoded: CoordCircuit = postcard::from_bytes(&encoded).unwrap();
1309        assert_eq!(decoded, circuit);
1310    }
1311
1312    #[cfg(target_pointer_width = "64")]
1313    #[test]
1314    fn wide_counts_do_not_alias_u32_identifiers() {
1315        let maximum = u32::MAX as usize;
1316        assert_eq!(measurement_count(maximum), Ok(u32::MAX));
1317        assert_eq!(checked_body_id(maximum), Ok(BodyId(u32::MAX)));
1318        assert_eq!(
1319            measurement_count(maximum + 1),
1320            Err(MeasurementFrameError::MeasurementCountOutOfRange {
1321                measurements: maximum + 1,
1322            })
1323        );
1324        assert_eq!(
1325            checked_body_id(maximum + 1),
1326            Err(CircuitError::BodyIdOutOfRange { index: maximum + 1 })
1327        );
1328    }
1329
1330    #[test]
1331    fn display_uses_coordinate_targets_and_nested_repeat_bodies() {
1332        let qubit = glam::ivec2(2, 3);
1333        let mut circuit = CoordCircuit::new();
1334        circuit.do_gate(GateType::H, [qubit]).unwrap();
1335        circuit.tick();
1336        let body = circuit.add_body(CircuitBody::from_ops(vec![
1337            Op::Measure {
1338                basis: PauliBasis::Z,
1339                qubits: vec![qubit],
1340                measurements: vec![0],
1341                flip_probability: 0.0,
1342            },
1343            Op::Repeat {
1344                body: BodyId(2),
1345                repetitions: 2,
1346            },
1347        ]));
1348        let nested = circuit.add_body(CircuitBody::from_ops(vec![Op::Gate {
1349            gate: GateType::X,
1350            qubits: vec![qubit],
1351        }]));
1352
1353        assert_eq!(nested, BodyId(2));
1354        circuit.push_repeat(body, 3);
1355
1356        assert_eq!(
1357            circuit.to_string(),
1358            "H (2,3)\nTICK\nREPEAT 3 {\n    M (2,3)\n    REPEAT 2 {\n        X (2,3)\n    }\n}"
1359        );
1360    }
1361
1362    #[test]
1363    fn has_moments_conflict_crosses_repeat_boundaries() {
1364        let qubit = glam::ivec2(0, 0);
1365        let mut circuit = CoordCircuit::new();
1366        circuit.do_gate(GateType::H, [qubit]).unwrap();
1367        let repeated = circuit.add_body(CircuitBody::from_ops(vec![Op::Gate {
1368            gate: GateType::X,
1369            qubits: vec![qubit],
1370        }]));
1371        circuit.push_repeat(repeated, 1);
1372
1373        assert!(circuit.has_moments_conflict());
1374
1375        let mut circuit = CoordCircuit::new();
1376        let repeated = circuit.add_body(CircuitBody::from_ops(vec![Op::Gate {
1377            gate: GateType::X,
1378            qubits: vec![qubit],
1379        }]));
1380        circuit.push_repeat(repeated, 2);
1381        assert!(circuit.has_moments_conflict());
1382
1383        circuit
1384            .body_mut(repeated)
1385            .unwrap()
1386            .ops_mut()
1387            .insert(0, Op::Tick);
1388        assert!(!circuit.has_moments_conflict());
1389    }
1390
1391    #[test]
1392    fn measure_allocates_ids_for_each_target_without_sorting() {
1393        let first = glam::ivec2(0, 0);
1394        let second = glam::ivec2(1, 0);
1395        let mut circuit = CoordCircuit::new();
1396
1397        circuit.measure(PauliBasis::Z, [second, first]);
1398
1399        let body = circuit.body(circuit.entry_body()).unwrap();
1400        assert!(matches!(
1401            &body.ops()[0],
1402            Op::Measure { basis: PauliBasis::Z, qubits, measurements, .. }
1403                if qubits == &[second, first]
1404                    && measurements == &[0, 1]
1405        ));
1406        assert_eq!(circuit.meas_registry().records().len(), 2);
1407    }
1408
1409    #[test]
1410    fn duplicate_measurement_targets_keep_distinct_measurement_ids() {
1411        let qubit = glam::ivec2(0, 0);
1412        let mut circuit = CoordCircuit::new();
1413
1414        circuit.measure(PauliBasis::Z, [qubit, qubit]);
1415
1416        let body = circuit.body(circuit.entry_body()).unwrap();
1417        assert!(matches!(
1418            &body.ops()[0],
1419            Op::Measure { measurements, .. }
1420                if measurements == &[0, 1]
1421        ));
1422    }
1423
1424    #[test]
1425    fn qubits_deduplicate_dense_and_extreme_coordinates() {
1426        let dense = [glam::ivec2(0, 0), glam::ivec2(1, 0)];
1427        let mut circuit = CoordCircuit::new();
1428        circuit
1429            .do_gate(GateType::H, [dense[0], dense[1], dense[0]])
1430            .unwrap();
1431        assert_eq!(circuit.qubits(), dense.into_iter().collect());
1432
1433        let extremes = [glam::IVec2::MIN, glam::IVec2::MAX];
1434        let mut circuit = CoordCircuit::new();
1435        circuit.do_gate(GateType::H, extremes).unwrap();
1436        assert_eq!(circuit.qubits(), extremes.into_iter().collect());
1437    }
1438
1439    #[test]
1440    fn qubits_include_products_registered_records_and_unreferenced_bodies() {
1441        let coords = [IVec2::ZERO, IVec2::X, IVec2::Y, IVec2::ONE];
1442        let mut circuit = CoordCircuit::new();
1443        circuit.reserve_measurement_id(coords[0]);
1444        let control = circuit
1445            .measure_pauli_products([crate::PauliMap::from_iter([
1446                (coords[1], crate::Pauli::X),
1447                (coords[2], crate::Pauli::Z),
1448            ])])
1449            .unwrap()[0];
1450        circuit.add_body(CircuitBody::from_ops(vec![Op::ConditionalPauli(vec![
1451            crate::ConditionalCorrection {
1452                pauli: PauliBasis::X,
1453                control,
1454                target: coords[3],
1455            },
1456        ])]));
1457        assert_eq!(circuit.qubits(), coords.into_iter().collect());
1458    }
1459
1460    #[test]
1461    fn flatten_rejects_huge_empty_and_measurement_free_repeats_before_mutation() {
1462        for ops in [
1463            Vec::new(),
1464            vec![Op::Tick],
1465            vec![Op::Gate {
1466                gate: GateType::H,
1467                qubits: vec![IVec2::ZERO],
1468            }],
1469        ] {
1470            let mut circuit = CoordCircuit::new();
1471            let body = circuit.add_body(CircuitBody::from_ops(ops));
1472            circuit.push_repeat(body, u32::MAX);
1473            let original = circuit.clone();
1474            assert!(matches!(
1475                circuit.flatten(),
1476                Err(CircuitError::FlattenResourceLimit { observed, limit }) if observed > limit
1477            ));
1478            assert_eq!(circuit, original);
1479        }
1480    }
1481
1482    #[test]
1483    fn flatten_limits_count_targets_and_can_be_overridden() {
1484        let mut circuit = CoordCircuit::new();
1485        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Gate {
1486            gate: GateType::H,
1487            qubits: vec![IVec2::ZERO; 8],
1488        }]));
1489        circuit.push_repeat(body, 3);
1490        let original = circuit.clone();
1491        let Err(CircuitError::FlattenResourceLimit { observed, .. }) =
1492            circuit.flatten_with_limits(FlattenLimits { max_work: 8 })
1493        else {
1494            panic!("target copies must consume the expansion allowance");
1495        };
1496        assert_eq!(circuit, original);
1497        circuit
1498            .flatten_with_limits(FlattenLimits { max_work: observed })
1499            .unwrap();
1500        assert_eq!(circuit.body(circuit.entry_body()).unwrap().ops().len(), 3);
1501        assert!(!circuit.has_repeats());
1502    }
1503
1504    #[test]
1505    fn flatten_counts_shared_body_paths_and_handles_deep_bodies_iteratively() {
1506        let mut shared = CoordCircuit::new();
1507        let mut body = shared.add_body(CircuitBody::new());
1508        for _ in 0..60 {
1509            body = shared.add_body(CircuitBody::from_ops(vec![
1510                Op::Repeat {
1511                    body,
1512                    repetitions: 1,
1513                },
1514                Op::Repeat {
1515                    body,
1516                    repetitions: 1,
1517                },
1518            ]));
1519        }
1520        shared.push_repeat(body, 1);
1521        assert!(matches!(
1522            shared.flatten(),
1523            Err(CircuitError::FlattenResourceLimit { .. })
1524        ));
1525
1526        let mut deep = CoordCircuit::new();
1527        let mut body = deep.add_body(CircuitBody::from_ops(vec![Op::Tick]));
1528        for _ in 0..4096 {
1529            body = deep.add_body(CircuitBody::from_ops(vec![Op::Repeat {
1530                body,
1531                repetitions: 1,
1532            }]));
1533        }
1534        deep.push_repeat(body, 1);
1535        deep.flatten().unwrap();
1536        assert_eq!(deep.body(deep.entry_body()).unwrap().ops(), &[Op::Tick]);
1537    }
1538
1539    #[test]
1540    fn flatten_unrolls_repeat_and_keeps_final_occurrence_ids() {
1541        let seed_qubit = glam::ivec2(0, 0);
1542        let loop_qubit = glam::ivec2(1, 0);
1543        let mut circuit = CoordCircuit::new();
1544        let seed = circuit.measure(PauliBasis::Z, [seed_qubit])[0];
1545        let repeated = circuit.reserve_measurement_id(loop_qubit);
1546        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1547            basis: PauliBasis::Z,
1548            qubits: vec![loop_qubit],
1549            measurements: vec![repeated],
1550            flip_probability: 0.0,
1551        }]));
1552        circuit.push_repeat(body, 3);
1553
1554        let trace = circuit.flatten().unwrap();
1555
1556        assert!(!circuit.has_repeats());
1557        assert_eq!(circuit.body_count(), 1);
1558        let ops = circuit.body(circuit.entry_body()).unwrap().ops();
1559        let emitted: Vec<u32> = ops
1560            .iter()
1561            .filter_map(|op| match op {
1562                Op::Measure { measurements, .. } => Some(measurements[0]),
1563                _ => None,
1564            })
1565            .collect();
1566        // Iterations 1 and 2 get fresh ids (2, 3); the final iteration keeps
1567        // the original id so outside references still mean "latest occurrence".
1568        assert_eq!(emitted, vec![seed, 2, 3, repeated]);
1569        assert_eq!(
1570            trace,
1571            vec![
1572                FlattenEvent::Measurement {
1573                    original: seed,
1574                    emitted: seed
1575                },
1576                FlattenEvent::LoopEnter { body },
1577                FlattenEvent::Measurement {
1578                    original: repeated,
1579                    emitted: 2
1580                },
1581                FlattenEvent::IterationEnd { body },
1582                FlattenEvent::Measurement {
1583                    original: repeated,
1584                    emitted: 3
1585                },
1586                FlattenEvent::IterationEnd { body },
1587                FlattenEvent::Measurement {
1588                    original: repeated,
1589                    emitted: repeated
1590                },
1591                FlattenEvent::IterationEnd { body },
1592                FlattenEvent::LoopExit { body },
1593            ]
1594        );
1595        // Fresh ids record the measured qubit like the originals.
1596        for id in [2, 3] {
1597            assert_eq!(
1598                circuit.meas_registry().record(id).unwrap().qubit,
1599                loop_qubit
1600            );
1601        }
1602    }
1603
1604    #[test]
1605    fn flatten_unrolls_nested_repeats_multiplicatively() {
1606        let qubit = glam::ivec2(0, 0);
1607        let mut circuit = CoordCircuit::new();
1608        let measurement = circuit.reserve_measurement_id(qubit);
1609        let inner = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1610            basis: PauliBasis::Z,
1611            qubits: vec![qubit],
1612            measurements: vec![measurement],
1613            flip_probability: 0.0,
1614        }]));
1615        let outer = circuit.add_body(CircuitBody::from_ops(vec![Op::Repeat {
1616            body: inner,
1617            repetitions: 2,
1618        }]));
1619        circuit.push_repeat(outer, 3);
1620
1621        circuit.flatten().unwrap();
1622
1623        let ops = circuit.body(circuit.entry_body()).unwrap().ops();
1624        assert_eq!(ops.len(), 6, "3 * 2 unrolled measurements");
1625        let last = match &ops[5] {
1626            Op::Measure { measurements, .. } => measurements[0],
1627            other => panic!("expected measure, got {other:?}"),
1628        };
1629        assert_eq!(last, measurement, "final occurrence keeps the original id");
1630        assert_eq!(circuit.num_measurements(), 6);
1631    }
1632
1633    #[test]
1634    fn flatten_reports_total_fresh_measurement_overflow() {
1635        let qubits = [glam::ivec2(0, 0), glam::ivec2(1, 0)];
1636        let mut circuit = CoordCircuit::new();
1637        let measurements = qubits.map(|qubit| circuit.reserve_measurement_id(qubit));
1638        let inner = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1639            basis: PauliBasis::Z,
1640            qubits: qubits.to_vec(),
1641            measurements: measurements.to_vec(),
1642            flip_probability: 0.0,
1643        }]));
1644        let outer = circuit.add_body(CircuitBody::from_ops(vec![Op::Repeat {
1645            body: inner,
1646            repetitions: u32::MAX,
1647        }]));
1648        circuit.push_repeat(outer, u32::MAX);
1649
1650        assert!(matches!(
1651            circuit.flatten(),
1652            Err(CircuitError::FlattenMeasurementIdOverflow)
1653        ));
1654    }
1655
1656    #[test]
1657    fn flatten_reports_per_measurement_occurrence_overflow() {
1658        let qubit = glam::ivec2(0, 0);
1659        let mut circuit = CoordCircuit::new();
1660        let measurement = circuit.reserve_measurement_id(qubit);
1661        let measured = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1662            basis: PauliBasis::Z,
1663            qubits: vec![qubit],
1664            measurements: vec![measurement],
1665            flip_probability: 0.0,
1666        }]));
1667        let wrapper = circuit.add_body(CircuitBody::from_ops(vec![Op::Repeat {
1668            body: measured,
1669            repetitions: u32::MAX,
1670        }]));
1671        circuit
1672            .body_mut(circuit.entry_body())
1673            .unwrap()
1674            .ops_mut()
1675            .extend([
1676                Op::Repeat {
1677                    body: wrapper,
1678                    repetitions: u32::MAX,
1679                },
1680                Op::Repeat {
1681                    body: wrapper,
1682                    repetitions: u32::MAX,
1683                },
1684            ]);
1685
1686        assert!(matches!(
1687            circuit.flatten(),
1688            Err(CircuitError::FlattenMeasurementIdOverflow)
1689        ));
1690    }
1691
1692    #[test]
1693    fn flatten_drops_zero_repetition_repeats_and_noops_without_repeats() {
1694        let qubit = glam::ivec2(0, 0);
1695        let mut circuit = CoordCircuit::new();
1696        circuit.measure(PauliBasis::Z, [qubit]);
1697        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Gate {
1698            gate: GateType::X,
1699            qubits: vec![qubit],
1700        }]));
1701        circuit.push_repeat(body, 0);
1702
1703        let trace = circuit.flatten().unwrap();
1704        assert!(
1705            trace
1706                .iter()
1707                .all(|event| matches!(event, FlattenEvent::Measurement { .. }))
1708        );
1709        assert_eq!(circuit.body(circuit.entry_body()).unwrap().ops().len(), 1);
1710
1711        // Already flat: untouched, empty trace.
1712        let ops_before = circuit.body(circuit.entry_body()).unwrap().ops().to_vec();
1713        let trace = circuit.flatten().unwrap();
1714        assert!(trace.is_empty());
1715        assert_eq!(
1716            circuit.body(circuit.entry_body()).unwrap().ops(),
1717            &ops_before
1718        );
1719    }
1720
1721    #[test]
1722    fn flatten_remaps_conditional_pauli_controls_to_current_iteration() {
1723        use crate::ConditionalCorrection;
1724        let qubit = glam::ivec2(0, 0);
1725        let mut circuit = CoordCircuit::new();
1726        let measurement = circuit.reserve_measurement_id(qubit);
1727        let body = circuit.add_body(CircuitBody::from_ops(vec![
1728            Op::Measure {
1729                basis: PauliBasis::Z,
1730                qubits: vec![qubit],
1731                measurements: vec![measurement],
1732                flip_probability: 0.0,
1733            },
1734            Op::ConditionalPauli(vec![ConditionalCorrection {
1735                pauli: PauliBasis::X,
1736                control: measurement,
1737                target: qubit,
1738            }]),
1739        ]));
1740        circuit.push_repeat(body, 2);
1741
1742        circuit.flatten().unwrap();
1743
1744        let ops = circuit.body(circuit.entry_body()).unwrap().ops();
1745        let controls: Vec<u32> = ops
1746            .iter()
1747            .filter_map(|op| match op {
1748                Op::ConditionalPauli(corrections) => Some(corrections[0].control),
1749                _ => None,
1750            })
1751            .collect();
1752        let measurements: Vec<u32> = ops
1753            .iter()
1754            .filter_map(|op| match op {
1755                Op::Measure { measurements, .. } => Some(measurements[0]),
1756                _ => None,
1757            })
1758            .collect();
1759        assert_eq!(
1760            controls, measurements,
1761            "each control follows its own iteration"
1762        );
1763    }
1764
1765    #[test]
1766    fn flatten_rejects_self_recursive_body() {
1767        let mut circuit = CoordCircuit::new();
1768        let body = circuit.add_body(CircuitBody::new());
1769        circuit.body_mut(body).unwrap().ops_mut().push(Op::Repeat {
1770            body,
1771            repetitions: 2,
1772        });
1773        circuit.push_repeat(body, 2);
1774
1775        assert!(matches!(
1776            circuit.flatten(),
1777            Err(CircuitError::InvalidCircuitBody(recursive)) if recursive == body
1778        ));
1779    }
1780
1781    /// A measurement in a `REPEAT` body, plus one after the loop.
1782    fn looped_and_trailing_circuit(repetitions: u32) -> (CoordCircuit, u32, u32) {
1783        let looped_qubit = glam::ivec2(0, 0);
1784        let trailing_qubit = glam::ivec2(1, 0);
1785        let mut circuit = CoordCircuit::new();
1786        let looped = circuit.reserve_measurement_id(looped_qubit);
1787        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1788            basis: PauliBasis::Z,
1789            qubits: vec![looped_qubit],
1790            measurements: vec![looped],
1791            flip_probability: 0.0,
1792        }]));
1793        circuit.push_repeat(body, repetitions);
1794        let trailing = circuit.measure(PauliBasis::Z, [trailing_qubit])[0];
1795        (circuit, looped, trailing)
1796    }
1797
1798    #[test]
1799    fn expanded_columns_report_the_last_occurrence_of_a_looped_measurement() {
1800        let (circuit, looped, trailing) = looped_and_trailing_circuit(4);
1801
1802        let columns = circuit.expanded_measurement_columns().unwrap();
1803
1804        assert_eq!(columns.count(), 5);
1805        assert_eq!(columns.column(looped), Some(3));
1806        assert_eq!(columns.column(trailing), Some(4));
1807        assert_eq!(columns.column(99), None);
1808    }
1809
1810    /// The unrolled column of the final occurrence is a multiplication, not a
1811    /// walk, so a loop far too large to unroll still resolves instantly.
1812    #[test]
1813    fn expanded_columns_resolve_a_loop_too_large_to_unroll() {
1814        let (circuit, looped, trailing) = looped_and_trailing_circuit(1_000_000_000);
1815
1816        let columns = circuit.expanded_measurement_columns().unwrap();
1817
1818        assert_eq!(columns.column(looped), Some(999_999_999));
1819        assert_eq!(columns.column(trailing), Some(1_000_000_000));
1820    }
1821
1822    #[test]
1823    fn expanded_columns_agree_with_flatten() {
1824        let (mut circuit, looped, trailing) = looped_and_trailing_circuit(3);
1825        let columns = circuit.expanded_measurement_columns().unwrap();
1826
1827        circuit.flatten().unwrap();
1828
1829        // Flatten keeps the final occurrence's original id, so the emitted
1830        // order of the flat circuit indexes the same columns.
1831        let emitted: Vec<u32> = circuit
1832            .body(circuit.entry_body())
1833            .unwrap()
1834            .ops()
1835            .iter()
1836            .filter_map(|op| match op {
1837                Op::Measure { measurements, .. } => Some(measurements[0]),
1838                _ => None,
1839            })
1840            .collect();
1841        assert_eq!(emitted.len() as u32, columns.count());
1842        for measurement in [looped, trailing] {
1843            let position = emitted.iter().rposition(|&id| id == measurement).unwrap();
1844            assert_eq!(columns.column(measurement), Some(position as u32));
1845        }
1846    }
1847
1848    /// The contract spelled out literally: re-walk a `Repeat` body once per
1849    /// repetition and let later iterations overwrite earlier ones.
1850    /// [`CoordCircuit::expanded_measurement_columns`] skips straight to the
1851    /// final iteration instead, so this is the oracle that keeps the
1852    /// optimization honest.
1853    fn naive_expanded_columns(circuit: &CoordCircuit) -> (FxHashMap<u32, u32>, u32) {
1854        fn walk(
1855            circuit: &CoordCircuit,
1856            body: BodyId,
1857            columns: &mut FxHashMap<u32, u32>,
1858            count: &mut u32,
1859        ) {
1860            for op in circuit.body(body).expect("body exists").ops() {
1861                match op {
1862                    Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => {
1863                        for &measurement in measurements {
1864                            columns.insert(measurement, *count);
1865                            *count += 1;
1866                        }
1867                    }
1868                    Op::Repeat { body, repetitions } => {
1869                        for _ in 0..*repetitions {
1870                            walk(circuit, *body, columns, count);
1871                        }
1872                    }
1873                    _ => {}
1874                }
1875            }
1876        }
1877
1878        let mut columns = FxHashMap::default();
1879        let mut count = 0;
1880        walk(circuit, circuit.entry_body(), &mut columns, &mut count);
1881        (columns, count)
1882    }
1883
1884    /// Build a circuit from a nesting spec: `repeats[i]` is the repetition
1885    /// count of loop level `i`, innermost last, each level measuring one qubit.
1886    fn nested_repeat_circuit(repeats: &[u32], trailing: bool) -> CoordCircuit {
1887        let mut circuit = CoordCircuit::new();
1888        let mut inner: Option<BodyId> = None;
1889        for level in (0..repeats.len()).rev() {
1890            let qubit = glam::ivec2(level as i32, 0);
1891            let measurement = circuit.reserve_measurement_id(qubit);
1892            let mut ops = vec![Op::Measure {
1893                basis: PauliBasis::Z,
1894                qubits: vec![qubit],
1895                measurements: vec![measurement],
1896                flip_probability: 0.0,
1897            }];
1898            if let Some(inner) = inner {
1899                ops.push(Op::Repeat {
1900                    body: inner,
1901                    repetitions: repeats[level + 1],
1902                });
1903            }
1904            inner = Some(circuit.add_body(CircuitBody::from_ops(ops)));
1905        }
1906        if let Some(outermost) = inner {
1907            circuit.push_repeat(outermost, repeats[0]);
1908        }
1909        if trailing {
1910            circuit.measure(PauliBasis::Z, [glam::ivec2(-1, 0)]);
1911        }
1912        circuit
1913    }
1914
1915    /// The loop-skipping optimization must agree with the literal re-walk on
1916    /// every shape, not just the flat one: nested loop counts multiply.
1917    #[test]
1918    fn expanded_columns_agree_with_a_literal_repeat_rewalk() {
1919        let shapes: &[&[u32]] = &[
1920            &[],
1921            &[1],
1922            &[7],
1923            &[3, 4],
1924            &[2, 3, 5],
1925            &[1, 1, 1],
1926            &[6, 1, 2, 3],
1927        ];
1928        for shape in shapes {
1929            for trailing in [false, true] {
1930                let circuit = nested_repeat_circuit(shape, trailing);
1931                let expected = naive_expanded_columns(&circuit);
1932                assert_eq!(
1933                    expected.1,
1934                    shape
1935                        .iter()
1936                        .rev()
1937                        .fold(0, |inner, count| count * (1 + inner))
1938                        + u32::from(trailing)
1939                );
1940
1941                let actual = circuit.expanded_measurement_columns().unwrap();
1942
1943                assert_eq!(actual.count(), expected.1, "shape {shape:?} {trailing}");
1944                assert_eq!(
1945                    actual.iter().collect::<std::collections::BTreeMap<_, _>>(),
1946                    expected.0.into_iter().collect(),
1947                    "shape {shape:?} trailing={trailing}"
1948                );
1949            }
1950        }
1951    }
1952
1953    /// A zero-repetition loop contributes nothing in either walk, and must not
1954    /// leave the column counter shifted for what follows it.
1955    #[test]
1956    fn expanded_columns_skip_zero_repetition_loops_like_the_rewalk() {
1957        let qubit = glam::ivec2(0, 0);
1958        let mut circuit = CoordCircuit::new();
1959        let skipped = circuit.reserve_measurement_id(qubit);
1960        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1961            basis: PauliBasis::Z,
1962            qubits: vec![qubit],
1963            measurements: vec![skipped],
1964            flip_probability: 0.0,
1965        }]));
1966        circuit.push_repeat(body, 0);
1967        let after = circuit.measure(PauliBasis::Z, [qubit])[0];
1968
1969        let columns = circuit.expanded_measurement_columns().unwrap();
1970
1971        assert_eq!(
1972            (
1973                columns.iter().collect::<std::collections::BTreeMap<_, _>>(),
1974                columns.count()
1975            ),
1976            {
1977                let (expected, count) = naive_expanded_columns(&circuit);
1978                (expected.into_iter().collect(), count)
1979            }
1980        );
1981        assert_eq!(columns.column(skipped), None, "never emitted");
1982        assert_eq!(columns.column(after), Some(0), "counter not shifted");
1983    }
1984
1985    #[test]
1986    fn expanded_columns_reject_a_self_recursive_body() {
1987        let mut circuit = CoordCircuit::new();
1988        let body = circuit.add_body(CircuitBody::new());
1989        circuit.body_mut(body).unwrap().ops_mut().push(Op::Repeat {
1990            body,
1991            repetitions: 2,
1992        });
1993        circuit.push_repeat(body, 2);
1994
1995        assert!(matches!(
1996            circuit.expanded_measurement_columns(),
1997            Err(CircuitError::InvalidCircuitBody(recursive)) if recursive == body
1998        ));
1999    }
2000
2001    #[test]
2002    fn do_gate_rejects_odd_two_qubit_targets() {
2003        let qubits = [IVec2::new(0, 0), IVec2::new(1, 0), IVec2::new(2, 0)];
2004        let mut circuit = CoordCircuit::new();
2005
2006        let err = circuit
2007            .do_gate(GateType::CX, qubits)
2008            .expect_err("odd two-qubit targets should fail");
2009
2010        assert_eq!(
2011            err,
2012            CircuitError::InvalidGateTargetCount {
2013                gate: GateType::CX,
2014                targets: 3,
2015            }
2016        );
2017        assert!(circuit.body(circuit.entry_body()).unwrap().ops().is_empty());
2018    }
2019}