Skip to main content

bloq_ir/
instantiation.rs

1use bloq_circuit::{
2    BodyId, CircuitBody, ConditionalCorrection, CoordCircuit, FlattenEvent, GateType, MeasRecord,
3    NoiseModel, Op, PauliMap, checked_translate_coordinate,
4};
5use glam::IVec2;
6use thiserror::Error;
7
8use crate::{
9    BloqNode, BloqTemplatePool, CoordinateOverflowError, FxMap, InstanceMeasurement, TemplateId,
10    TemplateInstance, TemplateInstanceId,
11};
12
13/// Why a template-backed node's instances could not be merged into one circuit.
14///
15/// Multi-instance emission and [`Bloq::validate`](crate::Bloq::validate) run
16/// the same merge. Singleton emission stops after the same structural preflight
17/// and translation because merging would change its temporal structure.
18#[derive(Debug, Error, Clone, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum NodeTemplateInstanceMergeError {
21    /// A detector bundle use is malformed.
22    #[error("{0}")]
23    DetectorBundle(#[from] crate::DetectorBundleError),
24    /// Boolean analysis exceeded its configured budget.
25    #[error("resource limit: {0}")]
26    BooleanResource(#[from] bloq_utils::boolean::BooleanResourceError),
27    /// Conditional registrations require selector inputs.
28    #[error("conditional component membership requires selector values")]
29    MembershipSelectionRequired,
30    /// A required selector input is unavailable.
31    #[error("component membership input {0} is not available")]
32    MissingMembershipInput(u32),
33    /// Registration tables violate their structural contract.
34    #[error("invalid component membership: {0}")]
35    InvalidMembership(String),
36    /// An instance names an absent template.
37    #[error("template {0:?} does not exist")]
38    UnknownTemplate(TemplateId),
39    /// A circuit names an absent body.
40    #[error("template circuit references invalid body {0:?}")]
41    InvalidBody(BodyId),
42    /// Circuit bodies recurse.
43    #[error("template circuit contains recursive body {0:?}")]
44    CyclicBody(BodyId),
45    /// MPP products and measurement ids have different lengths.
46    #[error("template circuit MPP has {products} products but {measurements} measurement ids")]
47    MppMeasurementCountMismatch {
48        /// Product count.
49        products: usize,
50        /// Measurement-id count.
51        measurements: usize,
52    },
53    /// Measure targets and outputs have different lengths.
54    #[error("template circuit Measure has {qubits} qubits but {measurements} measurement outputs")]
55    MeasureOutputCountMismatch {
56        /// Qubit count.
57        qubits: usize,
58        /// Measurement-id count.
59        measurements: usize,
60    },
61    /// An MPP operation has no products.
62    #[error("template circuit MPP requires at least one product")]
63    EmptyMpp,
64    /// An MPP product has no Pauli terms.
65    #[error("template circuit contains an empty MPP product")]
66    EmptyPauliProduct,
67    /// An operation produces an unregistered measurement.
68    #[error("template circuit operation produces unregistered measurement id {0}")]
69    UnregisteredMeasurementOutput(u32),
70    /// Several operations produce the same measurement id.
71    #[error("template circuit measurement id {0} is produced by more than one operation")]
72    DuplicateMeasurementOutput(u32),
73    /// A measurement registry coordinate disagrees with its operation.
74    #[error(
75        "template circuit measurement {measurement} is registered at {registered:?} but its operation produces it at {produced:?}"
76    )]
77    MeasurementOutputCoordinateMismatch {
78        /// Measurement id.
79        measurement: u32,
80        /// Registry coordinate.
81        registered: IVec2,
82        /// Operation coordinate.
83        produced: IVec2,
84    },
85    /// A two-qubit gate has an odd target count.
86    #[error("template circuit two-qubit gate {gate} has odd target count {targets}")]
87    OddTwoQubitTargetCount {
88        /// Gate kind.
89        gate: GateType,
90        /// Target count.
91        targets: usize,
92    },
93    /// A two-qubit gate pair repeats one endpoint.
94    #[error("template circuit two-qubit gate {gate} repeats target {qubit:?} within one pair")]
95    RepeatedTwoQubitGateTarget {
96        /// Gate kind.
97        gate: GateType,
98        /// Repeated endpoint.
99        qubit: IVec2,
100    },
101    /// A two-qubit noise operation has an odd target count.
102    #[error("template circuit DEPOLARIZE2 has odd target count {targets}")]
103    OddDepolarize2TargetCount {
104        /// Target count.
105        targets: usize,
106    },
107    /// A two-qubit noise pair repeats one endpoint.
108    #[error("template circuit DEPOLARIZE2 repeats target {0:?} within one pair")]
109    RepeatedDepolarize2Target(IVec2),
110    /// A probability is non-finite or outside `[0, 1]`.
111    #[error("template circuit {instruction} probability must be finite and in [0, 1]")]
112    InvalidProbability {
113        /// Instruction with the invalid probability.
114        instruction: &'static str,
115    },
116    /// Coordinate translation overflowed.
117    #[error("{0}")]
118    CoordinateOverflow(#[from] CoordinateOverflowError),
119    /// Merged measurement ids exhausted `u32`.
120    #[error("template instance measurement id space overflowed")]
121    MeasurementIdOverflow,
122    /// Circuit-body storage or the merged body-id space exhausted capacity.
123    #[error("resource limit: instance merge body-id or storage size overflowed")]
124    BodyCountOverflow,
125    /// Repeat expansion for idle noise failed.
126    #[error("{context}: {0}", context = if .0.is_resource_limited() {
127        "resource limit expanding repeat moments before idle noise"
128    } else {
129        "expanding repeat moments before idle noise"
130    })]
131    NoiseRepeatExpansion(#[from] crate::FlattenError),
132    /// Instance circuits have different tick counts.
133    #[error("template instance circuits have mismatched tick segment counts")]
134    TickSegmentCountMismatch,
135    /// A merged segment mixes repeat and non-repeat shapes.
136    #[error("template instance circuits mix repeat and non-repeat ops in one tick segment")]
137    RepeatShapeMismatch,
138    /// Aligned repeats have different counts.
139    #[error("template instance repeat ops have different repetition counts")]
140    RepeatCountMismatch,
141    /// One body participates in several merge tuples.
142    #[error(
143        "template instance circuit {circuit_index} body {body:?} participates in multiple aligned body tuples"
144    )]
145    BodyMergeConflict {
146        /// Source circuit index.
147        circuit_index: usize,
148        /// Conflicting body.
149        body: BodyId,
150    },
151    /// Instance circuits use one qubit incompatibly in a segment.
152    #[error("template instance circuits overlap qubit {0:?} in one tick segment")]
153    QubitConflict(IVec2),
154    /// Conditional feedforward reads a measurement too early.
155    #[error("ConditionalPauli control references measurement {0} not produced before it")]
156    ControlReferencesUnknownMeasurement(u32),
157}
158
159impl NodeTemplateInstanceMergeError {
160    /// Whether checking or materializing the instances exhausted a resource
161    /// allowance, rather than proving their circuit structure invalid.
162    #[must_use]
163    pub fn is_resource_limited(&self) -> bool {
164        match self {
165            Self::BooleanResource(_) | Self::BodyCountOverflow => true,
166            Self::NoiseRepeatExpansion(error) => error.is_resource_limited(),
167            _ => false,
168        }
169    }
170}
171
172/// A node's merged circuit and instance-space maps.
173///
174/// `measurements` translates each [`InstanceMeasurement`]
175/// to its merged measurement id, and `bodies` translates each
176/// `(instance, template body)` to its merged body id. Side tables (detectors,
177/// observable recipes) named in instance space resolve against these.
178#[derive(Debug, Clone)]
179pub struct NodeEmissionPlan {
180    /// Merged circuit.
181    pub circuit: CoordCircuit,
182    /// Annotated source templates after repeat expansion, when idle noise
183    /// crosses a repeat boundary. Use [`Self::templates`] when resolving this
184    /// plan's detector tables and instance-local measurement ids.
185    pub normalized_templates: Option<BloqTemplatePool>,
186    /// Instance-space measurement to merged-local measurement id.
187    pub measurements: FxMap<InstanceMeasurement, u32>,
188    /// Instance/template body to merged body id.
189    pub bodies: FxMap<(TemplateInstanceId, BodyId), BodyId>,
190}
191
192impl NodeEmissionPlan {
193    /// Source templates matching this plan's circuit and id maps. Most plans
194    /// use `original`; partial repeat moments carry an expanded annotated pool.
195    pub fn templates<'a>(&'a self, original: &'a BloqTemplatePool) -> &'a BloqTemplatePool {
196        self.normalized_templates.as_ref().unwrap_or(original)
197    }
198
199    /// The plan's [`measurements`](Self::measurements) inverted and grouped by
200    /// merged-local id: each `(local, sources)` pair collects every
201    /// [`InstanceMeasurement`] that resolves to the same merged record `local`.
202    ///
203    /// The order is canonical and load-bearing — both backends mint their global
204    /// records in it: groups are yielded ascending by `local`, and within a group
205    /// the sources are sorted by `(instance, measurement)`. `bloq_vm` mints
206    /// one global record per group in this order; `bloq_stim` folds each group to
207    /// its minimum backend id. Both folds are order-independent within a group, so
208    /// the intra-group order is fixed only to make the grouping canonical.
209    #[must_use]
210    pub fn grouped_measurements(&self) -> Vec<(u32, Vec<InstanceMeasurement>)> {
211        let mut flat: Vec<(u32, InstanceMeasurement)> = self
212            .measurements
213            .iter()
214            .map(|(&measurement, &local)| (local, measurement))
215            .collect();
216        flat.sort_unstable_by_key(|&(local, measurement)| {
217            (local, measurement.instance.0, measurement.measurement)
218        });
219        let mut groups: Vec<(u32, Vec<InstanceMeasurement>)> = Vec::new();
220        for (local, measurement) in flat {
221            match groups.last_mut() {
222                Some((last, sources)) if *last == local => sources.push(measurement),
223                _ => groups.push((local, vec![measurement])),
224            }
225        }
226        groups
227    }
228}
229
230/// Options applied after a node's instances have been translated and, when
231/// needed, merged.
232#[derive(Debug, Clone, Copy, Default)]
233pub struct InstantiationOptions<'a> {
234    noise: Option<&'a NoiseModel>,
235    boolean_limits: bloq_utils::boolean::BooleanLimits,
236}
237
238impl<'a> InstantiationOptions<'a> {
239    /// Materialize `noise` on the merged circuit.
240    #[must_use]
241    pub const fn noisy(noise: &'a NoiseModel) -> Self {
242        Self {
243            noise: Some(noise),
244            boolean_limits: bloq_utils::boolean::BooleanLimits::DEFAULT,
245        }
246    }
247
248    /// Bound cumulative validation work and live predicate decision nodes.
249    #[must_use]
250    pub const fn with_boolean_limits(mut self, limits: bloq_utils::boolean::BooleanLimits) -> Self {
251        self.boolean_limits = limits;
252        self
253    }
254
255    /// Configured Boolean-analysis limits.
256    #[must_use]
257    pub const fn boolean_limits(self) -> bloq_utils::boolean::BooleanLimits {
258        self.boolean_limits
259    }
260
261    pub(crate) const fn is_noiseless(self) -> bool {
262        self.noise.is_none()
263    }
264}
265
266impl BloqNode {
267    /// Resolve the component's conditional registrations without executing it.
268    /// Unselected instances produce no gates, records, detectors, or restarts.
269    /// The returned node retains the selected instances' identities.
270    ///
271    /// # Errors
272    ///
273    /// Returns [`NodeTemplateInstanceMergeError`] for missing inputs or an
274    /// invalid, incomplete, or duplicate membership registration.
275    pub fn select_quantum_members(
276        &self,
277        mut input: impl FnMut(u32) -> Option<bool>,
278    ) -> Result<Self, NodeTemplateInstanceMergeError> {
279        if let Some(quantum) = self.try_quantum() {
280            quantum.check_side_table_counts()?;
281        }
282        let mut selected = self.clone();
283        if selected
284            .try_quantum()
285            .is_none_or(|quantum| quantum.guards.is_empty())
286        {
287            return Ok(selected);
288        }
289        let quantum = selected.expect_quantum_mut();
290        let available_instances: crate::FxSet<_> = quantum
291            .instances
292            .iter()
293            .map(|instance| instance.id)
294            .collect();
295        let mut instances = crate::FxMap::default();
296        let mut detectors = crate::FxMap::default();
297        let mut detector_bundles = crate::FxMap::default();
298        let mut restarts = crate::FxMap::default();
299        let mut inputs = crate::FxMap::default();
300        for guard in &quantum.guards {
301            if guard.instances.is_empty()
302                && guard.detectors.is_empty()
303                && guard.detector_bundles.is_empty()
304                && guard.restarts.is_empty()
305                && guard.detector_parities.is_empty()
306                && guard.restart_parities.is_empty()
307            {
308                return Err(NodeTemplateInstanceMergeError::InvalidMembership(
309                    "empty registration".into(),
310                ));
311            }
312            let enabled = *inputs
313                .entry(guard.input)
314                .or_insert_with(|| input(guard.input));
315            let enabled = enabled.ok_or(NodeTemplateInstanceMergeError::MissingMembershipInput(
316                guard.input,
317            ))?;
318            for &id in &guard.instances {
319                if !available_instances.contains(&id) || instances.insert(id, enabled).is_some() {
320                    return Err(NodeTemplateInstanceMergeError::InvalidMembership(format!(
321                        "instance i{} is missing or registered more than once",
322                        id.0,
323                    )));
324                }
325            }
326            for (indices, count, selected) in [
327                (&guard.detectors, quantum.detectors.len(), &mut detectors),
328                (
329                    &guard.detector_bundles,
330                    quantum.detector_bundles.len(),
331                    &mut detector_bundles,
332                ),
333                (&guard.restarts, quantum.restarts.len(), &mut restarts),
334            ] {
335                for &index in indices {
336                    if index as usize >= count || selected.insert(index, enabled).is_some() {
337                        return Err(NodeTemplateInstanceMergeError::InvalidMembership(format!(
338                            "side-table index {index} is missing or registered more than once",
339                        )));
340                    }
341                }
342            }
343            for (index, parity) in &guard.detector_parities {
344                let detector = quantum.detectors.get_mut(*index as usize).ok_or_else(|| {
345                    NodeTemplateInstanceMergeError::InvalidMembership(format!(
346                        "unknown detector d{index}"
347                    ))
348                })?;
349                if enabled {
350                    detector.parity.xor_assign(parity);
351                }
352            }
353            for (index, parity) in &guard.restart_parities {
354                let restart = quantum.restarts.get_mut(*index as usize).ok_or_else(|| {
355                    NodeTemplateInstanceMergeError::InvalidMembership(format!(
356                        "unknown restart r{index}"
357                    ))
358                })?;
359                if enabled {
360                    restart.parity.xor_assign(parity);
361                }
362            }
363        }
364        quantum
365            .instances
366            .retain(|instance| instances.get(&instance.id).copied().unwrap_or(true));
367        let mut index = 0u32;
368        quantum.detectors.retain(|_| {
369            let enabled = detectors.get(&index).copied().unwrap_or(true);
370            index += 1;
371            enabled
372        });
373        let mut index = 0u32;
374        quantum.detector_bundles.retain(|_| {
375            let enabled = detector_bundles.get(&index).copied().unwrap_or(true);
376            index += 1;
377            enabled
378        });
379        let mut index = 0u32;
380        quantum.restarts.retain(|_| {
381            let enabled = restarts.get(&index).copied().unwrap_or(true);
382            index += 1;
383            enabled
384        });
385        quantum.guards.clear();
386        Ok(selected)
387    }
388
389    /// The node's merged circuit — its instances translated by offset and
390    /// merged moment-by-moment.
391    ///
392    /// This is [`Self::emission_plan`] keeping only the circuit; take the plan
393    /// when the instance-space id maps matter, or
394    /// [`Self::emission_plan_with_options`] to apply noise.
395    ///
396    /// # Errors
397    ///
398    /// Returns [`NodeTemplateInstanceMergeError`] when the instances do not
399    /// merge (mismatched tick/repeat structure, incompatible qubit overlap, a
400    /// missing template).
401    pub fn instantiate_circuit(
402        &self,
403        templates: &BloqTemplatePool,
404    ) -> Result<CoordCircuit, NodeTemplateInstanceMergeError> {
405        Ok(self.emission_plan(templates)?.circuit)
406    }
407
408    /// Instantiate and merge the node's template instances into one circuit,
409    /// returning the circuit together with the instance-space id maps
410    /// ([`NodeEmissionPlan`]). A classical or region node has no instances and
411    /// yields an empty plan.
412    ///
413    /// # Errors
414    ///
415    /// Returns [`NodeTemplateInstanceMergeError`] when the instances do not
416    /// merge (mismatched tick/repeat structure, incompatible qubit overlap, a
417    /// missing template).
418    pub fn emission_plan(
419        &self,
420        templates: &BloqTemplatePool,
421    ) -> Result<NodeEmissionPlan, NodeTemplateInstanceMergeError> {
422        self.emission_plan_with_options(templates, InstantiationOptions::default())
423    }
424
425    /// [`Self::emission_plan`] with options applied once after instantiation.
426    ///
427    /// # Errors
428    ///
429    /// Returns [`NodeTemplateInstanceMergeError`] for unresolved membership,
430    /// invalid templates or noise, coordinate overflow, or an invalid merge.
431    ///
432    /// # Panics
433    ///
434    /// Panics only if an internally generated merged circuit body is missing.
435    pub fn emission_plan_with_options(
436        &self,
437        templates: &BloqTemplatePool,
438        options: InstantiationOptions<'_>,
439    ) -> Result<NodeEmissionPlan, NodeTemplateInstanceMergeError> {
440        // Classical / region nodes carry no template instances, so they emit an
441        // empty circuit (like `node_qubits`/`node_measurement_count` skip them).
442        let Some(quantum) = self.try_quantum() else {
443            return Ok(NodeEmissionPlan {
444                circuit: CoordCircuit::new(),
445                normalized_templates: None,
446                measurements: FxMap::default(),
447                bodies: FxMap::default(),
448            });
449        };
450        let instances = &quantum.instances;
451        if !quantum.guards.is_empty() {
452            return Err(NodeTemplateInstanceMergeError::MembershipSelectionRequired);
453        }
454        if let Some(noise) = options.noise {
455            validate_noise_model(noise)?;
456        }
457
458        let mut next_measurement = 0u32;
459        let capacity = estimate_node_instance_capacity(self, templates)?;
460        let mut measurements =
461            FxMap::with_capacity_and_hasher(capacity.measurements, Default::default());
462        let mut bodies = FxMap::with_capacity_and_hasher(capacity.bodies, Default::default());
463        let mut instance_indices =
464            FxMap::with_capacity_and_hasher(instances.len(), Default::default());
465        let mut circuits = Vec::with_capacity(instances.len());
466        // Trigger each template's shared circuit-analysis cache once per node;
467        // nodes may carry many instances of the same template (IR-03).
468        let mut preflighted = crate::FxSet::default();
469        for (index, instance) in instances.iter().enumerate() {
470            instance_indices.insert(instance.id, index);
471            let circuit = instantiate_instance(
472                instance,
473                templates,
474                &mut next_measurement,
475                &mut measurements,
476                &mut bodies,
477                &mut preflighted,
478            )?;
479            circuits.push(circuit);
480        }
481        let ideal_qubits = ideal_instance_qubits(instances, &circuits);
482        let circuit = if circuits.len() == 1 {
483            // A singleton retains source timing; multi-instance SEM-MERGE
484            // introduces its own explicit tick boundaries around repeats.
485            circuits.pop().expect("length checked above")
486        } else {
487            let merged = merge_instantiated_circuits_with_body_aliases(&circuits)?;
488            for ((instance, _), body) in &mut bodies {
489                if let Some(index) = instance_indices.get(instance).copied()
490                    && let Some(&merged) = merged.body_aliases.get(&(index, *body))
491                {
492                    *body = merged;
493                }
494            }
495            for measurement in measurements.values_mut() {
496                *measurement = canonical_measurement(*measurement, &merged.measurement_aliases);
497            }
498            merged.circuit
499        };
500        let mut plan = NodeEmissionPlan {
501            circuit,
502            normalized_templates: None,
503            measurements,
504            bodies,
505        };
506        // Check the original SEM-MERGE before noise can normalize its shape.
507        // The actual merged stream also owns any synthesized tick boundaries.
508        if options
509            .noise
510            .is_some_and(|noise| noise.requires_repeat_flattening(&plan.circuit))
511        {
512            flatten_emission_plan(&mut plan, instances, templates)?;
513        }
514        plan.circuit = apply_instantiation_options(plan.circuit, options, &ideal_qubits)?;
515        Ok(plan)
516    }
517}
518
519/// Expand the merged stream and its source annotations without merging again.
520/// Repeat-isolation ticks and idle members already belong to the merged plan.
521fn flatten_emission_plan(
522    plan: &mut NodeEmissionPlan,
523    instances: &[TemplateInstance],
524    templates: &BloqTemplatePool,
525) -> Result<(), NodeTemplateInstanceMergeError> {
526    let trace = plan.circuit.flatten().map_err(crate::FlattenError::from)?;
527    let merged_occurrences = measurement_occurrences(&trace);
528    let original_measurements = plan.measurements.clone();
529    let mut normalized = templates.clone();
530    let mut source_occurrences = FxMap::default();
531    plan.bodies.clear();
532    for instance in instances {
533        if let std::collections::hash_map::Entry::Vacant(entry) =
534            source_occurrences.entry(instance.template_id)
535        {
536            let (flat, trace) = templates[instance.template_id].flatten_with_trace()?;
537            *normalized
538                .make_mut(instance.template_id)
539                .expect("source template exists") = flat;
540            entry.insert(measurement_occurrences(&trace));
541        }
542        for (&original, emitted) in &source_occurrences[&instance.template_id] {
543            let merged = original_measurements[&InstanceMeasurement {
544                instance: instance.id,
545                measurement: original,
546            }];
547            let merged_emitted = &merged_occurrences[&merged];
548            assert_eq!(
549                emitted.len(),
550                merged_emitted.len(),
551                "aligned instances preserve measurement occurrence counts"
552            );
553            // Each aliased source gets the whole sequence; an occurrence must
554            // not be consumed by a different instance sharing this measurement.
555            for (&measurement, &local) in emitted.iter().zip(merged_emitted) {
556                plan.measurements.insert(
557                    InstanceMeasurement {
558                        instance: instance.id,
559                        measurement,
560                    },
561                    local,
562                );
563            }
564        }
565        plan.bodies.insert(
566            (
567                instance.id,
568                normalized[instance.template_id].circuit.entry_body(),
569            ),
570            plan.circuit.entry_body(),
571        );
572    }
573    plan.normalized_templates = Some(normalized);
574    Ok(())
575}
576
577fn measurement_occurrences(trace: &[FlattenEvent]) -> FxMap<u32, Vec<u32>> {
578    let mut occurrences = FxMap::<_, Vec<_>>::default();
579    for &event in trace {
580        if let FlattenEvent::Measurement { original, emitted } = event {
581            occurrences.entry(original).or_default().push(emitted);
582        }
583    }
584    occurrences
585}
586
587/// Materialize the requested noise on a node's instantiated circuit.
588fn apply_instantiation_options(
589    circuit: CoordCircuit,
590    options: InstantiationOptions<'_>,
591    ideal_qubits: &[IVec2],
592) -> Result<CoordCircuit, NodeTemplateInstanceMergeError> {
593    let Some(noise) = options.noise else {
594        return Ok(circuit);
595    };
596    validate_noise_model(noise)?;
597    noise
598        .noisy_circuit_excluding(&circuit, ideal_qubits)
599        .map_err(crate::FlattenError::from)
600        .map_err(NodeTemplateInstanceMergeError::from)
601}
602
603fn ideal_instance_qubits(instances: &[TemplateInstance], circuits: &[CoordCircuit]) -> Vec<IVec2> {
604    // Spec rule SEM-SPATIAL-PORT: source ownership wins on shared qubits.
605    let mut derived = crate::FxSet::default();
606    let mut source = crate::FxSet::default();
607    for (instance, circuit) in instances.iter().zip(circuits) {
608        let target = if instance.provenance.is_spatial_port_substitution() {
609            &mut derived
610        } else {
611            &mut source
612        };
613        target.extend(circuit.qubits());
614    }
615    derived.difference(&source).copied().collect()
616}
617
618/// Stored circuit work, excluding repeated execution. Saturation keeps this
619/// usable as a conservative preallocation charge at every instantiation gate.
620pub(crate) fn circuit_work(circuit: &CoordCircuit) -> usize {
621    let mut work = circuit
622        .body_count()
623        .saturating_add(circuit.meas_registry().records().len());
624    for index in 0..circuit.body_count() {
625        let ops = circuit
626            .body(BodyId(index as u32))
627            .expect("allocated body")
628            .ops();
629        work = work.saturating_add(ops.len());
630        for op in ops {
631            let targets = match op {
632                Op::Measure {
633                    qubits,
634                    measurements,
635                    ..
636                } => qubits.len().saturating_add(measurements.len()),
637                Op::Gate { qubits, .. }
638                | Op::Depolarize1 { qubits, .. }
639                | Op::Depolarize2 { qubits, .. }
640                | Op::PauliError { qubits, .. } => qubits.len(),
641                Op::MPP {
642                    products,
643                    measurements,
644                } => products
645                    .iter()
646                    .fold(products.len(), |total, product| {
647                        total.saturating_add(product.len())
648                    })
649                    .saturating_add(measurements.len()),
650                Op::ConditionalPauli(corrections) => corrections.len(),
651                Op::Tick | Op::Repeat { .. } => 0,
652            };
653            work = work.saturating_add(targets);
654        }
655    }
656    work
657}
658
659/// Summed `with_capacity` hints for merging a set of circuits.
660#[derive(Default)]
661struct CircuitCounts {
662    measurements: usize,
663    bodies: usize,
664}
665
666impl CircuitCounts {
667    fn add_counts(
668        &mut self,
669        measurements: usize,
670        bodies: usize,
671    ) -> Result<(), NodeTemplateInstanceMergeError> {
672        let measurements = self
673            .measurements
674            .checked_add(measurements)
675            .ok_or(NodeTemplateInstanceMergeError::MeasurementIdOverflow)?;
676        u32::try_from(measurements)
677            .map_err(|_| NodeTemplateInstanceMergeError::MeasurementIdOverflow)?;
678        let bodies = self
679            .bodies
680            .checked_add(bodies)
681            .ok_or(NodeTemplateInstanceMergeError::BodyCountOverflow)?;
682        self.measurements = measurements;
683        self.bodies = bodies;
684        Ok(())
685    }
686}
687
688fn count_circuits<'a>(
689    circuits: impl IntoIterator<Item = &'a CoordCircuit>,
690) -> Result<CircuitCounts, NodeTemplateInstanceMergeError> {
691    let mut counts = CircuitCounts::default();
692    for circuit in circuits {
693        counts.add_counts(
694            circuit.meas_registry().records().len(),
695            circuit.body_count(),
696        )?;
697    }
698    Ok(counts)
699}
700
701fn estimate_node_instance_capacity(
702    node: &BloqNode,
703    templates: &BloqTemplatePool,
704) -> Result<CircuitCounts, NodeTemplateInstanceMergeError> {
705    let mut counts = CircuitCounts::default();
706    for instance in &node.expect_quantum().instances {
707        let circuit = &templates
708            .get(instance.template_id)
709            .ok_or(NodeTemplateInstanceMergeError::UnknownTemplate(
710                instance.template_id,
711            ))?
712            .circuit;
713        counts.add_counts(
714            circuit.meas_registry().records().len(),
715            circuit.body_count(),
716        )?;
717    }
718    Ok(counts)
719}
720
721fn instantiate_instance(
722    instance: &TemplateInstance,
723    templates: &BloqTemplatePool,
724    next_measurement: &mut u32,
725    measurements: &mut FxMap<InstanceMeasurement, u32>,
726    bodies: &mut FxMap<(TemplateInstanceId, BodyId), BodyId>,
727    preflighted: &mut crate::FxSet<TemplateId>,
728) -> Result<CoordCircuit, NodeTemplateInstanceMergeError> {
729    let template = templates.get(instance.template_id).ok_or(
730        NodeTemplateInstanceMergeError::UnknownTemplate(instance.template_id),
731    )?;
732    // Validate operation shape and measurement ownership once per template;
733    // `translate_circuit` relies on this having run and does not re-check ops.
734    if preflighted.insert(instance.template_id) {
735        template.circuit_analysis()?;
736    }
737    let mut local_bodies =
738        FxMap::with_capacity_and_hasher(template.circuit.body_count(), Default::default());
739    let circuit = translate_circuit(
740        &template.circuit,
741        instance.offset,
742        instance.id,
743        next_measurement,
744        measurements,
745        &mut local_bodies,
746    )?;
747    bodies.extend(
748        local_bodies
749            .into_iter()
750            .map(|(template_body, instance_body)| ((instance.id, template_body), instance_body)),
751    );
752    Ok(circuit)
753}
754
755fn translate_circuit(
756    source: &CoordCircuit,
757    offset: IVec2,
758    instance: TemplateInstanceId,
759    next_measurement: &mut u32,
760    measurements: &mut FxMap<InstanceMeasurement, u32>,
761    body_map: &mut FxMap<BodyId, BodyId>,
762) -> Result<CoordCircuit, NodeTemplateInstanceMergeError> {
763    // Op shape and measurement ownership were validated once per template by
764    // `instantiate_instance` (this fn's sole caller); only per-instance coord
765    // translation remains.
766    for coordinate in source.qubits() {
767        checked_translate_instance_coord(coordinate, offset)?;
768    }
769    let mut translator = InstanceTranslator {
770        source,
771        offset,
772        instance,
773        next_measurement,
774        measurements,
775        destination: CoordCircuit::new(),
776        body_map,
777    };
778    translator.translate_body(source.entry_body())?;
779    Ok(translator.destination)
780}
781
782#[derive(Debug)]
783pub(crate) struct RepeatMeasurementEvent {
784    pub(crate) body: BodyId,
785    pub(crate) before: usize,
786    pub(crate) after: usize,
787}
788
789#[derive(Debug, Clone)]
790pub(crate) enum MeasurementSet {
791    Dense(Vec<bool>),
792    Sparse(crate::FxSet<u32>),
793}
794
795impl MeasurementSet {
796    fn for_circuit(circuit: &CoordCircuit) -> Self {
797        let records = circuit.meas_registry().records();
798        // Sorted unique non-negative ids are dense iff their maximum is len - 1.
799        if records
800            .last()
801            .is_none_or(|record| record.id as usize == records.len() - 1)
802        {
803            Self::Dense(vec![false; records.len()])
804        } else {
805            Self::Sparse(crate::FxSet::with_capacity_and_hasher(
806                records.len(),
807                Default::default(),
808            ))
809        }
810    }
811
812    fn empty_like(&self) -> Self {
813        match self {
814            Self::Dense(ids) => Self::Dense(vec![false; ids.len()]),
815            Self::Sparse(ids) => Self::Sparse(crate::FxSet::with_capacity_and_hasher(
816                ids.len(),
817                Default::default(),
818            )),
819        }
820    }
821
822    fn insert(&mut self, id: u32) -> bool {
823        match self {
824            Self::Dense(ids) => {
825                let seen = &mut ids[id as usize];
826                let inserted = !*seen;
827                *seen = true;
828                inserted
829            }
830            Self::Sparse(ids) => ids.insert(id),
831        }
832    }
833
834    pub(crate) fn contains(&self, id: u32) -> bool {
835        match self {
836            Self::Dense(ids) => ids.get(id as usize).copied().unwrap_or(false),
837            Self::Sparse(ids) => ids.contains(&id),
838        }
839    }
840}
841
842#[derive(Debug)]
843pub(crate) struct TemplateCircuitAnalysis {
844    pub(crate) reachable_bodies: crate::FxSet<BodyId>,
845    pub(crate) body_order: Vec<BodyId>,
846    pub(crate) produced_measurements: MeasurementSet,
847    pub(crate) measurement_order: FxMap<u32, usize>,
848    pub(crate) repeat_events: Vec<RepeatMeasurementEvent>,
849}
850
851fn preflight_circuit(
852    source: &CoordCircuit,
853) -> Result<TemplateCircuitAnalysis, NodeTemplateInstanceMergeError> {
854    let mut structural_measurements = MeasurementSet::for_circuit(source);
855    let mut measurement_controls = Vec::new();
856    let mut reachable_bodies = crate::FxSet::default();
857    let mut body_order = Vec::new();
858    let mut active = crate::FxSet::default();
859    let mut pending = vec![(source.entry_body(), 0usize)];
860    active.insert(source.entry_body());
861    while let Some((body, next)) = pending.last_mut() {
862        let ops = source
863            .body(*body)
864            .ok_or(NodeTemplateInstanceMergeError::InvalidBody(*body))?
865            .ops();
866        let Some(op) = ops.get(*next) else {
867            let body = *body;
868            pending.pop();
869            active.remove(&body);
870            reachable_bodies.insert(body);
871            body_order.push(body);
872            continue;
873        };
874        *next += 1;
875        validate_source_op(source, op)?;
876        register_source_measurement_outputs(op, &mut structural_measurements)?;
877        if let Op::ConditionalPauli(corrections) = op {
878            measurement_controls.extend(corrections.iter().map(|correction| correction.control));
879        }
880        if let Op::Repeat { body, .. } = op
881            && !reachable_bodies.contains(body)
882        {
883            if !active.insert(*body) {
884                return Err(NodeTemplateInstanceMergeError::CyclicBody(*body));
885            }
886            pending.push((*body, 0));
887        }
888    }
889    for measurement in measurement_controls {
890        if !structural_measurements.contains(measurement) {
891            return Err(
892                NodeTemplateInstanceMergeError::ControlReferencesUnknownMeasurement(measurement),
893            );
894        }
895    }
896    // Record every body structurally reached: the entry plus
897    // every transitive `REPEAT` target. The entry is never a valid repeat
898    // target, so drop it to leave exactly the bodies a `REPEAT`/detector scope
899    // may name (WF template-body reference checks).
900    reachable_bodies.remove(&source.entry_body());
901
902    let mut produced_measurements = structural_measurements.empty_like();
903    let mut measurement_order = FxMap::default();
904    let mut repeat_events = Vec::new();
905    let mut visited = crate::FxSet::default();
906    visited.insert(source.entry_body());
907    let mut pending = vec![(source.entry_body(), 0usize, None)];
908    while let Some((body, next, before)) = pending.last_mut() {
909        let ops = source
910            .body(*body)
911            .expect("structural preflight checked bodies")
912            .ops();
913        let Some(op) = ops.get(*next) else {
914            if let Some(before) = *before {
915                repeat_events.push(RepeatMeasurementEvent {
916                    body: *body,
917                    before,
918                    after: measurement_order.len(),
919                });
920            }
921            pending.pop();
922            continue;
923        };
924        *next += 1;
925        match op {
926            Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => {
927                for &measurement in measurements {
928                    produced_measurements.insert(measurement);
929                    measurement_order.insert(measurement, measurement_order.len());
930                }
931            }
932            Op::ConditionalPauli(corrections) => {
933                for correction in corrections {
934                    if !produced_measurements.contains(correction.control) {
935                        return Err(
936                            NodeTemplateInstanceMergeError::ControlReferencesUnknownMeasurement(
937                                correction.control,
938                            ),
939                        );
940                    }
941                }
942            }
943            // Measurement availability only grows, and every body emits the
944            // same ids. Its first execution is therefore the strongest check;
945            // later executions add no records and cannot invalidate a reference.
946            Op::Repeat { body, repetitions } if *repetitions > 0 && visited.insert(*body) => {
947                pending.push((*body, 0, Some(measurement_order.len())));
948            }
949            _ => {}
950        }
951    }
952    Ok(TemplateCircuitAnalysis {
953        reachable_bodies,
954        body_order,
955        produced_measurements,
956        measurement_order,
957        repeat_events,
958    })
959}
960
961fn register_source_measurement_outputs(
962    op: &Op,
963    produced: &mut MeasurementSet,
964) -> Result<(), NodeTemplateInstanceMergeError> {
965    let measurements = match op {
966        Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => measurements,
967        Op::Gate { .. }
968        | Op::Depolarize1 { .. }
969        | Op::Depolarize2 { .. }
970        | Op::PauliError { .. }
971        | Op::Tick
972        | Op::Repeat { .. }
973        | Op::ConditionalPauli(_) => return Ok(()),
974    };
975    for &measurement in measurements {
976        if !produced.insert(measurement) {
977            return Err(NodeTemplateInstanceMergeError::DuplicateMeasurementOutput(
978                measurement,
979            ));
980        }
981    }
982    Ok(())
983}
984
985fn validate_source_op(
986    source: &CoordCircuit,
987    op: &Op,
988) -> Result<(), NodeTemplateInstanceMergeError> {
989    validate_circuit_op(op)?;
990    match op {
991        Op::Measure {
992            qubits,
993            measurements,
994            ..
995        } => {
996            for (&qubit, &measurement) in qubits.iter().zip(measurements) {
997                validate_measurement_output(source, measurement, qubit)?;
998            }
999            Ok(())
1000        }
1001        Op::MPP {
1002            products,
1003            measurements,
1004        } => {
1005            for (product, &measurement) in products.iter().zip(measurements) {
1006                validate_measurement_output(source, measurement, mpp_record_coord(product)?)?;
1007            }
1008            Ok(())
1009        }
1010        Op::Gate { .. }
1011        | Op::Depolarize1 { .. }
1012        | Op::Depolarize2 { .. }
1013        | Op::PauliError { .. }
1014        | Op::Tick
1015        | Op::Repeat { .. }
1016        | Op::ConditionalPauli(_) => Ok(()),
1017    }
1018}
1019
1020/// Validate one circuit operation's self-contained shape and probabilities.
1021/// Measurement-registry bindings require a full [`validate_template_circuit`]
1022/// call instead.
1023///
1024/// # Errors
1025///
1026/// [`NodeTemplateInstanceMergeError`] if an operation has invalid arity,
1027/// repeated two-qubit endpoints, an empty MPP or product, or an invalid
1028/// probability.
1029fn validate_circuit_op(op: &Op) -> Result<(), NodeTemplateInstanceMergeError> {
1030    match op {
1031        Op::Gate { gate, qubits } if gate.is_two_qubit_gate() => {
1032            if !qubits.len().is_multiple_of(2) {
1033                return Err(NodeTemplateInstanceMergeError::OddTwoQubitTargetCount {
1034                    gate: *gate,
1035                    targets: qubits.len(),
1036                });
1037            }
1038            if let Some(pair) = qubits
1039                .as_chunks::<2>()
1040                .0
1041                .iter()
1042                .find(|pair| pair[0] == pair[1])
1043            {
1044                return Err(NodeTemplateInstanceMergeError::RepeatedTwoQubitGateTarget {
1045                    gate: *gate,
1046                    qubit: pair[0],
1047                });
1048            }
1049            Ok(())
1050        }
1051        Op::Measure {
1052            qubits,
1053            measurements,
1054            flip_probability,
1055            ..
1056        } => {
1057            validate_probability("Measure", *flip_probability)?;
1058            if qubits.len() != measurements.len() {
1059                return Err(NodeTemplateInstanceMergeError::MeasureOutputCountMismatch {
1060                    qubits: qubits.len(),
1061                    measurements: measurements.len(),
1062                });
1063            }
1064            Ok(())
1065        }
1066        Op::MPP {
1067            products,
1068            measurements,
1069        } => {
1070            validate_mpp_arity(products, measurements)?;
1071            for product in products {
1072                mpp_record_coord(product)?;
1073            }
1074            Ok(())
1075        }
1076        Op::Depolarize1 { probability, .. } => validate_probability("DEPOLARIZE1", *probability),
1077        Op::Depolarize2 {
1078            probability,
1079            qubits,
1080        } => {
1081            validate_probability("DEPOLARIZE2", *probability)?;
1082            if !qubits.len().is_multiple_of(2) {
1083                return Err(NodeTemplateInstanceMergeError::OddDepolarize2TargetCount {
1084                    targets: qubits.len(),
1085                });
1086            }
1087            if let Some(pair) = qubits
1088                .as_chunks::<2>()
1089                .0
1090                .iter()
1091                .find(|pair| pair[0] == pair[1])
1092            {
1093                return Err(NodeTemplateInstanceMergeError::RepeatedDepolarize2Target(
1094                    pair[0],
1095                ));
1096            }
1097            Ok(())
1098        }
1099        Op::PauliError { probability, .. } => validate_probability("Pauli error", *probability),
1100        Op::Gate { .. } | Op::Tick | Op::Repeat { .. } | Op::ConditionalPauli(_) => Ok(()),
1101    }
1102}
1103
1104fn validate_noise_model(noise: &NoiseModel) -> Result<(), NodeTemplateInstanceMergeError> {
1105    for (instruction, probability) in [
1106        ("DEPOLARIZE1", noise.p1),
1107        ("DEPOLARIZE2", noise.p2),
1108        ("Measure", noise.p_meas),
1109        ("Pauli error", noise.p_reset),
1110        ("DEPOLARIZE1", noise.p_idle),
1111    ] {
1112        validate_probability(instruction, probability)?;
1113    }
1114    Ok(())
1115}
1116
1117fn validate_probability(
1118    instruction: &'static str,
1119    probability: f64,
1120) -> Result<(), NodeTemplateInstanceMergeError> {
1121    if !(0.0..=1.0).contains(&probability) {
1122        return Err(NodeTemplateInstanceMergeError::InvalidProbability { instruction });
1123    }
1124    Ok(())
1125}
1126
1127fn validate_measurement_output(
1128    source: &CoordCircuit,
1129    measurement: u32,
1130    produced: IVec2,
1131) -> Result<(), NodeTemplateInstanceMergeError> {
1132    let record = source
1133        .meas_registry()
1134        .record(measurement)
1135        .ok_or(NodeTemplateInstanceMergeError::UnregisteredMeasurementOutput(measurement))?;
1136    if record.qubit != produced {
1137        return Err(
1138            NodeTemplateInstanceMergeError::MeasurementOutputCoordinateMismatch {
1139                measurement,
1140                registered: record.qubit,
1141                produced,
1142            },
1143        );
1144    }
1145    Ok(())
1146}
1147
1148fn checked_translate_pauli_map(
1149    product: &PauliMap,
1150    offset: IVec2,
1151) -> Result<PauliMap, NodeTemplateInstanceMergeError> {
1152    let entries = product
1153        .iter()
1154        .map(|(coordinate, pauli)| Ok((checked_translate_coordinate(*coordinate, offset)?, *pauli)))
1155        .collect::<Result<Vec<_>, NodeTemplateInstanceMergeError>>()?;
1156    Ok(PauliMap::from_unique_entries(entries))
1157}
1158
1159fn checked_translate_instance_coord(
1160    coordinate: IVec2,
1161    offset: IVec2,
1162) -> Result<IVec2, NodeTemplateInstanceMergeError> {
1163    Ok(checked_translate_coordinate(coordinate, offset)?)
1164}
1165
1166fn translate_qubits(
1167    qubits: &[IVec2],
1168    offset: IVec2,
1169) -> Result<Vec<IVec2>, NodeTemplateInstanceMergeError> {
1170    qubits
1171        .iter()
1172        .map(|&qubit| checked_translate_instance_coord(qubit, offset))
1173        .collect()
1174}
1175
1176struct InstanceTranslator<'a, 'b> {
1177    source: &'a CoordCircuit,
1178    offset: IVec2,
1179    instance: TemplateInstanceId,
1180    next_measurement: &'b mut u32,
1181    measurements: &'b mut FxMap<InstanceMeasurement, u32>,
1182    destination: CoordCircuit,
1183    body_map: &'b mut FxMap<BodyId, BodyId>,
1184}
1185
1186impl InstanceTranslator<'_, '_> {
1187    /// Mint this instance's record id for `source_measurement`, binding it to
1188    /// `qubit`. The single place the id-allocation and duplicate-output policy
1189    /// lives — `Measure` and `MPP` differ only in how they derive `qubit`.
1190    fn allocate_record(
1191        &mut self,
1192        source_measurement: u32,
1193        qubit: IVec2,
1194    ) -> Result<u32, NodeTemplateInstanceMergeError> {
1195        let instance_measurement = InstanceMeasurement {
1196            instance: self.instance,
1197            measurement: source_measurement,
1198        };
1199        if self.measurements.contains_key(&instance_measurement) {
1200            return Err(NodeTemplateInstanceMergeError::DuplicateMeasurementOutput(
1201                source_measurement,
1202            ));
1203        }
1204        let measurement = *self.next_measurement;
1205        *self.next_measurement = self
1206            .next_measurement
1207            .checked_add(1)
1208            .ok_or(NodeTemplateInstanceMergeError::MeasurementIdOverflow)?;
1209        self.measurements.insert(instance_measurement, measurement);
1210        self.destination.register_measurement_id(measurement, qubit);
1211        Ok(measurement)
1212    }
1213
1214    fn translate_body(&mut self, body: BodyId) -> Result<BodyId, NodeTemplateInstanceMergeError> {
1215        if let Some(mapped) = self.body_map.get(&body).copied() {
1216            return Ok(mapped);
1217        }
1218        let destination_body = self.allocate_body(body);
1219        let mut pending = vec![(body, destination_body, 0usize, Vec::new())];
1220        while let Some((source, destination, next, translated)) = pending.last_mut() {
1221            let ops = self
1222                .source
1223                .body(*source)
1224                .ok_or(NodeTemplateInstanceMergeError::InvalidBody(*source))?
1225                .ops();
1226            let Some(op) = ops.get(*next) else {
1227                *self
1228                    .destination
1229                    .body_mut(*destination)
1230                    .expect("allocated destination")
1231                    .ops_mut() = std::mem::take(translated);
1232                pending.pop();
1233                continue;
1234            };
1235            if let Op::Repeat { body, .. } = op
1236                && !self.body_map.contains_key(body)
1237            {
1238                let destination = self.allocate_body(*body);
1239                pending.push((*body, destination, 0, Vec::new()));
1240                continue;
1241            }
1242            // Finish children before later ops so measurement controls retain
1243            // the same first-production order as the recursive translation.
1244            translated.push(self.translate_op(op)?);
1245            *next += 1;
1246        }
1247        Ok(destination_body)
1248    }
1249
1250    fn allocate_body(&mut self, body: BodyId) -> BodyId {
1251        let destination = if body == self.source.entry_body() {
1252            self.destination.entry_body()
1253        } else {
1254            self.destination.add_body(CircuitBody::new())
1255        };
1256        self.body_map.insert(body, destination);
1257        destination
1258    }
1259
1260    fn translate_op(&mut self, op: &Op) -> Result<Op, NodeTemplateInstanceMergeError> {
1261        // `preflight_circuit` (run once per template by `instantiate_instance`)
1262        // already validated this op's shape via `validate_source_op`; translation
1263        // only offsets coordinates and allocates instance measurement ids.
1264        match op {
1265            Op::Gate { gate, qubits } => Ok(Op::Gate {
1266                gate: *gate,
1267                qubits: translate_qubits(qubits, self.offset)?,
1268            }),
1269            Op::Measure {
1270                basis,
1271                qubits,
1272                measurements: source_measurements,
1273                flip_probability,
1274            } => {
1275                let qubits = translate_qubits(qubits, self.offset)?;
1276                let mut op_measurements = Vec::with_capacity(qubits.len());
1277                for (&source_measurement, &qubit) in source_measurements.iter().zip(&qubits) {
1278                    op_measurements.push(self.allocate_record(source_measurement, qubit)?);
1279                }
1280                Ok(Op::Measure {
1281                    basis: *basis,
1282                    qubits,
1283                    measurements: op_measurements,
1284                    flip_probability: *flip_probability,
1285                })
1286            }
1287            // Like `Measure`, but the support lives in `products`: offset each
1288            // product and allocate one fresh instance measurement id per product.
1289            Op::MPP {
1290                products,
1291                measurements: source_measurements,
1292            } => {
1293                let products = products
1294                    .iter()
1295                    .map(|product| checked_translate_pauli_map(product, self.offset))
1296                    .collect::<Result<Vec<_>, _>>()?;
1297                let mut op_measurements = Vec::with_capacity(source_measurements.len());
1298                for (&source_measurement, product) in source_measurements.iter().zip(&products) {
1299                    // Derived from the product so the merge stage, which sees only
1300                    // the op, recovers the same coordinate via `mpp_record_coord`.
1301                    let qubit = mpp_record_coord(product)?;
1302                    op_measurements.push(self.allocate_record(source_measurement, qubit)?);
1303                }
1304                Ok(Op::MPP {
1305                    products,
1306                    measurements: op_measurements,
1307                })
1308            }
1309            Op::Tick => Ok(Op::Tick),
1310            Op::Depolarize1 {
1311                probability,
1312                qubits,
1313            } => Ok(Op::Depolarize1 {
1314                probability: *probability,
1315                qubits: translate_qubits(qubits, self.offset)?,
1316            }),
1317            Op::Depolarize2 {
1318                probability,
1319                qubits,
1320            } => Ok(Op::Depolarize2 {
1321                probability: *probability,
1322                qubits: translate_qubits(qubits, self.offset)?,
1323            }),
1324            Op::PauliError {
1325                probability,
1326                pauli,
1327                qubits,
1328            } => Ok(Op::PauliError {
1329                probability: *probability,
1330                pauli: *pauli,
1331                qubits: translate_qubits(qubits, self.offset)?,
1332            }),
1333            Op::Repeat { body, repetitions } => Ok(Op::Repeat {
1334                body: self.body_map[body],
1335                repetitions: *repetitions,
1336            }),
1337            // Offset each target qubit and remap each measurement control id into
1338            // instance space via the same table `Measure` populates. The
1339            // measurement precedes its controlled correction, so the lookup is
1340            // already present. A
1341            // `Value`-slot control is node-scoped (an incoming `Value` edge), not
1342            // measurement-space, so it passes through untouched (U14).
1343            Op::ConditionalPauli(corrections) => Ok(Op::ConditionalPauli(
1344                corrections
1345                    .iter()
1346                    .map(|correction| {
1347                        let control = self
1348                            .measurements
1349                            .get(&InstanceMeasurement {
1350                                instance: self.instance,
1351                                measurement: correction.control,
1352                            })
1353                            .copied()
1354                            .ok_or(
1355                                NodeTemplateInstanceMergeError::ControlReferencesUnknownMeasurement(
1356                                    correction.control,
1357                                ),
1358                            )?;
1359                        Ok(ConditionalCorrection {
1360                            control,
1361                            target: checked_translate_instance_coord(
1362                                correction.target,
1363                                self.offset,
1364                            )?,
1365                            ..*correction
1366                        })
1367                    })
1368                    .collect::<Result<Vec<_>, NodeTemplateInstanceMergeError>>()?,
1369            )),
1370        }
1371    }
1372}
1373
1374struct MergedInstantiatedCircuits {
1375    circuit: CoordCircuit,
1376    body_aliases: FxMap<(usize, BodyId), BodyId>,
1377    measurement_aliases: FxMap<u32, u32>,
1378}
1379
1380fn merge_instantiated_circuits_with_body_aliases(
1381    circuits: &[CoordCircuit],
1382) -> Result<MergedInstantiatedCircuits, NodeTemplateInstanceMergeError> {
1383    if circuits.is_empty() {
1384        return Ok(MergedInstantiatedCircuits {
1385            circuit: CoordCircuit::new(),
1386            body_aliases: FxMap::default(),
1387            measurement_aliases: FxMap::default(),
1388        });
1389    };
1390
1391    let mut destination = CoordCircuit::new();
1392    let capacity = count_circuits(circuits)?;
1393    let body_sources = circuits
1394        .iter()
1395        .enumerate()
1396        .map(|(index, circuit)| BodySource {
1397            index,
1398            circuit,
1399            body: circuit.entry_body(),
1400        })
1401        .collect::<Vec<_>>();
1402    let mut body_aliases = FxMap::with_capacity_and_hasher(
1403        capacity.bodies.saturating_sub(circuits.len()),
1404        Default::default(),
1405    );
1406    let mut measurement_records = Vec::with_capacity(capacity.measurements);
1407    let entry = destination.entry_body();
1408    let aliases = merge_bodies_into(
1409        body_sources,
1410        &mut destination,
1411        entry,
1412        &mut body_aliases,
1413        &mut measurement_records,
1414    )?;
1415    destination.register_measurement_records(&mut measurement_records);
1416    destination.remap_measurement_ids(&aliases);
1417    Ok(MergedInstantiatedCircuits {
1418        circuit: destination,
1419        body_aliases,
1420        measurement_aliases: aliases,
1421    })
1422}
1423
1424#[derive(Clone, Copy)]
1425struct BodySource<'a> {
1426    index: usize,
1427    circuit: &'a CoordCircuit,
1428    body: BodyId,
1429}
1430
1431/// Split a body's ops into tick-delimited segments, each a contiguous slice of
1432/// the source ops. Ticks separate segments and are dropped; a `Repeat` op is
1433/// isolated into its own one-op segment so the merge can pair repeats across
1434/// instances. Returning borrowed slices avoids cloning every op just to split
1435/// it — the merge clones only the ops it actually keeps.
1436fn split_ops_at_ticks(ops: &[Op]) -> Vec<&[Op]> {
1437    let mut segments = Vec::new();
1438    let mut start = 0;
1439    for (index, op) in ops.iter().enumerate() {
1440        match op {
1441            Op::Tick => {
1442                segments.push(&ops[start..index]);
1443                start = index + 1;
1444            }
1445            Op::Repeat { .. } => {
1446                if start < index {
1447                    segments.push(&ops[start..index]);
1448                }
1449                segments.push(&ops[index..index + 1]);
1450                start = index + 1;
1451            }
1452            Op::Gate { .. }
1453            | Op::Measure { .. }
1454            | Op::MPP { .. }
1455            | Op::Depolarize1 { .. }
1456            | Op::Depolarize2 { .. }
1457            | Op::PauliError { .. }
1458            | Op::ConditionalPauli(_) => {}
1459        }
1460    }
1461    segments.push(&ops[start..]);
1462    segments
1463}
1464
1465struct BodyMergeFrame<'a> {
1466    sources: Vec<BodySource<'a>>,
1467    segments: Vec<Vec<&'a [Op]>>,
1468    destination: BodyId,
1469    next_segment: usize,
1470    merged: Vec<Op>,
1471}
1472
1473impl<'a> BodyMergeFrame<'a> {
1474    fn new(
1475        sources: Vec<BodySource<'a>>,
1476        destination: BodyId,
1477        active_bodies: &mut crate::FxSet<(usize, BodyId)>,
1478    ) -> Result<Self, NodeTemplateInstanceMergeError> {
1479        let mut segments = Vec::with_capacity(sources.len());
1480        for source in &sources {
1481            if !active_bodies.insert((source.index, source.body)) {
1482                return Err(NodeTemplateInstanceMergeError::CyclicBody(source.body));
1483            }
1484            let body = source
1485                .circuit
1486                .body(source.body)
1487                .ok_or(NodeTemplateInstanceMergeError::InvalidBody(source.body))?;
1488            segments.push(split_ops_at_ticks(body.ops()));
1489        }
1490        // All members must have the same tick/repeat structure (SEM-MERGE).
1491        let segment_count = segments.first().map_or(0, Vec::len);
1492        if segments
1493            .iter()
1494            .any(|segments| segments.len() != segment_count)
1495        {
1496            return Err(NodeTemplateInstanceMergeError::TickSegmentCountMismatch);
1497        }
1498        let merged = Vec::with_capacity(estimate_merged_ops(&segments));
1499        Ok(Self {
1500            sources,
1501            segments,
1502            destination,
1503            next_segment: 0,
1504            merged,
1505        })
1506    }
1507}
1508
1509fn estimate_merged_ops(segments: &[Vec<&[Op]>]) -> usize {
1510    segments
1511        .iter()
1512        .flat_map(|body_segments| body_segments.iter())
1513        .map(|segment| segment.len())
1514        .sum()
1515}
1516
1517/// An MPP product's measurement-record coordinate.
1518fn mpp_record_coord(product: &PauliMap) -> Result<IVec2, NodeTemplateInstanceMergeError> {
1519    product
1520        .representative_coord()
1521        .ok_or(NodeTemplateInstanceMergeError::EmptyPauliProduct)
1522}
1523
1524fn validate_mpp_arity(
1525    products: &[PauliMap],
1526    measurements: &[u32],
1527) -> Result<(), NodeTemplateInstanceMergeError> {
1528    if products.len() != measurements.len() {
1529        return Err(
1530            NodeTemplateInstanceMergeError::MppMeasurementCountMismatch {
1531                products: products.len(),
1532                measurements: measurements.len(),
1533            },
1534        );
1535    }
1536    if products.is_empty() {
1537        return Err(NodeTemplateInstanceMergeError::EmptyMpp);
1538    }
1539    Ok(())
1540}
1541
1542fn estimate_segment_qubit_refs(segments: &[Vec<&[Op]>], index: usize) -> usize {
1543    segments
1544        .iter()
1545        .filter_map(|body_segments| body_segments.get(index))
1546        .flat_map(|segment| segment.iter())
1547        .map(|op| match op {
1548            Op::Gate { qubits, .. } | Op::Measure { qubits, .. } => qubits.len(),
1549            Op::ConditionalPauli(corrections) => corrections.len(),
1550            Op::MPP { products, .. } => products.iter().map(PauliMap::len).sum(),
1551            Op::Depolarize1 { .. }
1552            | Op::Depolarize2 { .. }
1553            | Op::PauliError { .. }
1554            | Op::Tick
1555            | Op::Repeat { .. } => 0,
1556        })
1557        .sum()
1558}
1559
1560fn collect_compatible_repeat_segments<'a>(
1561    sources: &[BodySource<'a>],
1562    member_segments: &[Vec<&[Op]>],
1563    segment_index: usize,
1564) -> Result<Option<(Vec<BodySource<'a>>, u32)>, NodeTemplateInstanceMergeError> {
1565    let mut repeat_segments = Vec::new();
1566    let mut has_plain_segment = false;
1567    for (source, segments) in sources.iter().copied().zip(member_segments) {
1568        let Some(&segment) = segments.get(segment_index) else {
1569            continue;
1570        };
1571        if segment.is_empty() {
1572            continue;
1573        }
1574        if let [Op::Repeat { body, repetitions }] = segment {
1575            repeat_segments.push((
1576                BodySource {
1577                    body: *body,
1578                    ..source
1579                },
1580                *repetitions,
1581            ));
1582        } else {
1583            has_plain_segment = true;
1584        }
1585    }
1586    if repeat_segments.is_empty() {
1587        return Ok(None);
1588    }
1589    if has_plain_segment {
1590        return Err(NodeTemplateInstanceMergeError::RepeatShapeMismatch);
1591    }
1592    let repetitions = repeat_segments[0].1;
1593    if repeat_segments
1594        .iter()
1595        .any(|(_, repeat_count)| *repeat_count != repetitions)
1596    {
1597        return Err(NodeTemplateInstanceMergeError::RepeatCountMismatch);
1598    }
1599    Ok(Some((
1600        repeat_segments
1601            .into_iter()
1602            .map(|(source, _)| source)
1603            .collect(),
1604        repetitions,
1605    )))
1606}
1607
1608/// Append one instance's op to the merged segment, deduplicating overlaps that
1609/// are legal (a shared boundary qubit measured or reset identically by an
1610/// earlier instance) and rejecting overlaps that are not (a qubit claimed two
1611/// incompatible ways by *different* instances in one tick segment).
1612///
1613/// This is the single place the merge invariant is enforced; emission planning
1614/// validates by running this merge, so the rules cannot drift between the two.
1615///
1616/// `committed` holds the footprint of earlier instances this segment. `current`
1617/// holds only the current instance, so sequential reuse within that instance is
1618/// retained while any overlap with an earlier instance is rejected.
1619fn append_segment_op(
1620    op: &Op,
1621    committed: &SegmentDuplicateState,
1622    current: &mut SegmentDuplicateState,
1623    aliases: &mut FxMap<u32, u32>,
1624    measurement_records: &mut Vec<MeasRecord>,
1625    merged: &mut Vec<Op>,
1626) -> Result<(), NodeTemplateInstanceMergeError> {
1627    match op {
1628        Op::Measure { .. } => append_segment_measurement_op(
1629            op,
1630            committed,
1631            current,
1632            aliases,
1633            measurement_records,
1634            merged,
1635        ),
1636        Op::Gate { gate, qubits } if gate.is_reset() => {
1637            append_segment_reset_op(*gate, qubits, committed, current, merged)
1638        }
1639        Op::Gate { qubits, .. } => {
1640            claim_segment_qubits(qubits.iter().copied(), committed, current)?;
1641            merged.push(op.clone());
1642            Ok(())
1643        }
1644        Op::ConditionalPauli(corrections) => {
1645            claim_segment_qubits(
1646                corrections.iter().map(|correction| correction.target),
1647                committed,
1648                current,
1649            )?;
1650            merged.push(op.clone());
1651            Ok(())
1652        }
1653        // MPPs do not deduplicate across instances, but every product qubit is
1654        // still part of this instance's segment footprint. As with plain
1655        // gates, repeated use within one instance is legal; only an earlier
1656        // instance's committed footprint conflicts.
1657        Op::MPP {
1658            products,
1659            measurements,
1660        } => {
1661            validate_mpp_arity(products, measurements)?;
1662            for (product, &measurement) in products.iter().zip(measurements) {
1663                claim_segment_qubits(product.iter().map(|(qubit, _)| *qubit), committed, current)?;
1664                measurement_records.push(MeasRecord {
1665                    id: measurement,
1666                    qubit: mpp_record_coord(product)?,
1667                });
1668            }
1669            merged.push(op.clone());
1670            Ok(())
1671        }
1672        Op::Depolarize1 { .. }
1673        | Op::Depolarize2 { .. }
1674        | Op::PauliError { .. }
1675        | Op::Repeat { .. }
1676        | Op::Tick => {
1677            merged.push(op.clone());
1678            Ok(())
1679        }
1680    }
1681}
1682
1683#[derive(Default)]
1684struct SegmentDuplicateState {
1685    measurements: FxMap<IVec2, (u32, bloq_circuit::PauliBasis, f64)>,
1686    reset_gates: FxMap<IVec2, bloq_circuit::GateType>,
1687    /// Qubits with non-deduplicable activity, including multiple operations
1688    /// from one instance in this segment.
1689    occupied: crate::FxSet<IVec2>,
1690}
1691
1692impl SegmentDuplicateState {
1693    fn reserve(&mut self, capacity: usize) {
1694        self.measurements.reserve(capacity);
1695        self.reset_gates.reserve(capacity);
1696        self.occupied.reserve(capacity);
1697    }
1698
1699    fn has_any(&self, qubit: IVec2) -> bool {
1700        self.measurements.contains_key(&qubit)
1701            || self.reset_gates.contains_key(&qubit)
1702            || self.occupied.contains(&qubit)
1703    }
1704
1705    fn mark_occupied(&mut self, qubit: IVec2) {
1706        self.measurements.remove(&qubit);
1707        self.reset_gates.remove(&qubit);
1708        self.occupied.insert(qubit);
1709    }
1710
1711    fn clear(&mut self) {
1712        self.measurements.clear();
1713        self.reset_gates.clear();
1714        self.occupied.clear();
1715    }
1716
1717    /// Fold one instance's `current` footprint into the committed state,
1718    /// draining `current` so the caller reuses its allocations for the next
1719    /// instance rather than reallocating a fresh state per segment.
1720    fn commit(&mut self, current: &mut Self) {
1721        self.measurements.extend(current.measurements.drain());
1722        self.reset_gates.extend(current.reset_gates.drain());
1723        self.occupied.extend(current.occupied.drain());
1724    }
1725}
1726
1727fn claim_segment_qubits(
1728    qubits: impl IntoIterator<Item = IVec2>,
1729    committed: &SegmentDuplicateState,
1730    current: &mut SegmentDuplicateState,
1731) -> Result<(), NodeTemplateInstanceMergeError> {
1732    for qubit in qubits {
1733        if committed.has_any(qubit) {
1734            return Err(NodeTemplateInstanceMergeError::QubitConflict(qubit));
1735        }
1736        current.mark_occupied(qubit);
1737    }
1738    Ok(())
1739}
1740
1741fn append_segment_measurement_op(
1742    op: &Op,
1743    committed: &SegmentDuplicateState,
1744    current: &mut SegmentDuplicateState,
1745    aliases: &mut FxMap<u32, u32>,
1746    measurement_records: &mut Vec<MeasRecord>,
1747    merged: &mut Vec<Op>,
1748) -> Result<(), NodeTemplateInstanceMergeError> {
1749    let Op::Measure {
1750        basis,
1751        qubits,
1752        measurements,
1753        flip_probability,
1754    } = op
1755    else {
1756        unreachable!("append_segment_measurement_op only accepts Measure")
1757    };
1758    let mut kept_qubits = Vec::with_capacity(qubits.len());
1759    let mut kept_measurements = Vec::with_capacity(measurements.len());
1760    for (&qubit, &measurement) in qubits.iter().zip(measurements) {
1761        if current.has_any(qubit) {
1762            if committed.has_any(qubit) {
1763                return Err(NodeTemplateInstanceMergeError::QubitConflict(qubit));
1764            }
1765            current.mark_occupied(qubit);
1766            measurement_records.push(MeasRecord {
1767                id: measurement,
1768                qubit,
1769            });
1770            kept_qubits.push(qubit);
1771            kept_measurements.push(measurement);
1772            continue;
1773        }
1774
1775        match committed.measurements.get(&qubit).copied() {
1776            // A shared boundary qubit already measured by an earlier instance:
1777            // dedup, but only if both instances agree on the basis.
1778            Some((canonical, canonical_basis, canonical_flip_probability)) => {
1779                if canonical_basis != *basis || canonical_flip_probability != *flip_probability {
1780                    return Err(NodeTemplateInstanceMergeError::QubitConflict(qubit));
1781                }
1782                aliases.insert(measurement, canonical);
1783                current
1784                    .measurements
1785                    .insert(qubit, (canonical, *basis, *flip_probability));
1786            }
1787            // No prior measurement to dedup against, so a plain gate or reset
1788            // left here by an earlier instance is a genuine overlap.
1789            None => {
1790                if committed.has_any(qubit) {
1791                    return Err(NodeTemplateInstanceMergeError::QubitConflict(qubit));
1792                }
1793                current
1794                    .measurements
1795                    .insert(qubit, (measurement, *basis, *flip_probability));
1796                measurement_records.push(MeasRecord {
1797                    id: measurement,
1798                    qubit,
1799                });
1800                kept_qubits.push(qubit);
1801                kept_measurements.push(measurement);
1802            }
1803        }
1804    }
1805
1806    if !kept_qubits.is_empty() {
1807        merged.push(Op::Measure {
1808            basis: *basis,
1809            qubits: kept_qubits,
1810            measurements: kept_measurements,
1811            flip_probability: *flip_probability,
1812        });
1813    }
1814    Ok(())
1815}
1816
1817fn append_segment_reset_op(
1818    gate: bloq_circuit::GateType,
1819    qubits: &[IVec2],
1820    committed: &SegmentDuplicateState,
1821    current: &mut SegmentDuplicateState,
1822    merged: &mut Vec<Op>,
1823) -> Result<(), NodeTemplateInstanceMergeError> {
1824    let mut kept_qubits = Vec::with_capacity(qubits.len());
1825    for &qubit in qubits {
1826        if current.has_any(qubit) {
1827            if committed.has_any(qubit) {
1828                return Err(NodeTemplateInstanceMergeError::QubitConflict(qubit));
1829            }
1830            current.mark_occupied(qubit);
1831            kept_qubits.push(qubit);
1832            continue;
1833        }
1834
1835        match committed.reset_gates.get(&qubit).copied() {
1836            // Duplicate identical reset from an earlier instance: keep one.
1837            Some(previous) if previous == gate => {
1838                current.reset_gates.insert(qubit, gate);
1839            }
1840            Some(_) => return Err(NodeTemplateInstanceMergeError::QubitConflict(qubit)),
1841            None => {
1842                if committed.has_any(qubit) {
1843                    return Err(NodeTemplateInstanceMergeError::QubitConflict(qubit));
1844                }
1845                current.reset_gates.insert(qubit, gate);
1846                kept_qubits.push(qubit);
1847            }
1848        }
1849    }
1850    if !kept_qubits.is_empty() {
1851        merged.push(Op::Gate {
1852            gate,
1853            qubits: kept_qubits,
1854        });
1855    }
1856    Ok(())
1857}
1858
1859fn merge_bodies_into(
1860    sources: Vec<BodySource<'_>>,
1861    destination: &mut CoordCircuit,
1862    destination_body: BodyId,
1863    body_aliases: &mut FxMap<(usize, BodyId), BodyId>,
1864    measurement_records: &mut Vec<MeasRecord>,
1865) -> Result<FxMap<u32, u32>, NodeTemplateInstanceMergeError> {
1866    let mut active_bodies = crate::FxSet::default();
1867    let mut merged_body_tuples = FxMap::default();
1868    let mut pending = vec![BodyMergeFrame::new(
1869        sources,
1870        destination_body,
1871        &mut active_bodies,
1872    )?];
1873    // Aliases only gain entries during traversal. Keep one map rather than
1874    // copying every descendant's aliases once per ancestor body.
1875    let mut aliases = FxMap::default();
1876    let mut committed = SegmentDuplicateState::default();
1877    let mut current = SegmentDuplicateState::default();
1878    while let Some(frame) = pending.last_mut() {
1879        let segment_count = frame.segments.first().map_or(0, Vec::len);
1880        let index = frame.next_segment;
1881        if index == segment_count {
1882            let frame = pending.pop().expect("current body");
1883            *destination
1884                .body_mut(frame.destination)
1885                .expect("destination body was allocated above")
1886                .ops_mut() = frame.merged;
1887            for source in &frame.sources {
1888                active_bodies.remove(&(source.index, source.body));
1889            }
1890            if !pending.is_empty() {
1891                let body_tuple = frame
1892                    .sources
1893                    .iter()
1894                    .map(|source| (source.index, source.body))
1895                    .collect::<Vec<_>>();
1896                for &source in &body_tuple {
1897                    body_aliases.insert(source, frame.destination);
1898                }
1899                merged_body_tuples.insert(body_tuple, frame.destination);
1900            }
1901            continue;
1902        }
1903        frame.next_segment += 1;
1904        if index > 0 {
1905            frame.merged.push(Op::Tick);
1906        }
1907        if let Some((repeats, repetitions)) =
1908            collect_compatible_repeat_segments(&frame.sources, &frame.segments, index)?
1909        {
1910            let body_tuple = repeats
1911                .iter()
1912                .map(|source| (source.index, source.body))
1913                .collect::<Vec<_>>();
1914            if let Some(&body) = merged_body_tuples.get(&body_tuple) {
1915                frame.merged.push(Op::Repeat { body, repetitions });
1916                continue;
1917            }
1918            if let Some(source) = repeats
1919                .iter()
1920                .find(|source| body_aliases.contains_key(&(source.index, source.body)))
1921            {
1922                return Err(NodeTemplateInstanceMergeError::BodyMergeConflict {
1923                    circuit_index: source.index,
1924                    body: source.body,
1925                });
1926            }
1927            u32::try_from(destination.body_count())
1928                .map_err(|_| NodeTemplateInstanceMergeError::BodyCountOverflow)?;
1929            let body = destination.add_body(CircuitBody::new());
1930            frame.merged.push(Op::Repeat { body, repetitions });
1931            pending.push(BodyMergeFrame::new(repeats, body, &mut active_bodies)?);
1932        } else {
1933            // Reusing a qubit inside one instance is legal. Only committed
1934            // footprints from earlier instances participate in overlap checks.
1935            committed.clear();
1936            let capacity = estimate_segment_qubit_refs(&frame.segments, index);
1937            committed.reserve(capacity);
1938            for segments in &frame.segments {
1939                current.reserve(capacity);
1940                for op in segments[index] {
1941                    append_segment_op(
1942                        op,
1943                        &committed,
1944                        &mut current,
1945                        &mut aliases,
1946                        measurement_records,
1947                        &mut frame.merged,
1948                    )?;
1949                }
1950                committed.commit(&mut current);
1951            }
1952        }
1953    }
1954    Ok(aliases)
1955}
1956
1957/// Preflight one template circuit through the shared analysis used by
1958/// instance emission, returning every body reached as a `REPEAT` target.
1959pub(crate) fn preflight_template_circuit(
1960    circuit: &CoordCircuit,
1961) -> Result<TemplateCircuitAnalysis, NodeTemplateInstanceMergeError> {
1962    // Validate operation arities and measurement-registry ownership through the
1963    // same helpers used by instance translation. This walk deliberately descends
1964    // through zero-count repeats: they skip runtime events, not structural
1965    // validation. `preflight_circuit` records every `REPEAT`-target body
1966    // directly, so the former single-source merge (whose sole role was recovering
1967    // that set, and whose cross-instance error paths cannot fire for one circuit)
1968    // is gone — this no longer merges, hence the name.
1969    preflight_circuit(circuit)
1970}
1971
1972/// Validate one template circuit through the shared preflight used by both
1973/// [`Bloq::validate`](crate::Bloq::validate) and instance emission.
1974///
1975/// # Errors
1976///
1977/// [`NodeTemplateInstanceMergeError`] if bodies, operation arities,
1978/// measurement bindings, or probabilities are invalid.
1979pub fn validate_template_circuit(
1980    circuit: &CoordCircuit,
1981) -> Result<(), NodeTemplateInstanceMergeError> {
1982    preflight_template_circuit(circuit).map(|_| ())
1983}
1984
1985fn canonical_measurement(mut measurement: u32, aliases: &FxMap<u32, u32>) -> u32 {
1986    while let Some(canonical) = aliases.get(&measurement).copied() {
1987        measurement = canonical;
1988    }
1989    measurement
1990}
1991
1992#[cfg(test)]
1993mod tests {
1994    #[test]
1995    fn instance_capacity_checks_before_allocation() {
1996        let mut counts = super::CircuitCounts {
1997            measurements: u32::MAX as usize,
1998            bodies: 0,
1999        };
2000        assert!(matches!(
2001            counts.add_counts(1, 0),
2002            Err(super::NodeTemplateInstanceMergeError::MeasurementIdOverflow)
2003        ));
2004        let mut counts = super::CircuitCounts {
2005            measurements: 0,
2006            bodies: usize::MAX,
2007        };
2008        assert!(matches!(
2009            counts.add_counts(0, 1),
2010            Err(super::NodeTemplateInstanceMergeError::BodyCountOverflow)
2011        ));
2012        assert_eq!(counts.bodies, usize::MAX);
2013    }
2014    use super::{
2015        InstantiationOptions, NodeEmissionPlan, merge_instantiated_circuits_with_body_aliases,
2016    };
2017    use bloq_circuit::{
2018        BodyId, CircuitBody, ConditionalCorrection, CoordCircuit, GateType, NoiseModel, Op, Pauli,
2019        PauliBasis, PauliMap,
2020    };
2021    use glam::{IVec2, ivec2};
2022
2023    use crate::test_fixture::instance_measurement;
2024    use crate::{
2025        BloqNode, BloqTemplate, BloqTemplatePool, CoordinateOverflowError,
2026        NodeTemplateInstanceMergeError, TemplateInstance, TemplateInstanceId,
2027    };
2028
2029    fn malformed_mpp_error(
2030        products: Vec<PauliMap>,
2031        measurements: Vec<u32>,
2032    ) -> NodeTemplateInstanceMergeError {
2033        let qubit = ivec2(0, 0);
2034        let mut circuit = CoordCircuit::new();
2035        for &measurement in &measurements {
2036            circuit.register_measurement_id(measurement, qubit);
2037        }
2038        circuit
2039            .body_mut(circuit.entry_body())
2040            .unwrap()
2041            .ops_mut()
2042            .push(Op::MPP {
2043                products,
2044                measurements,
2045            });
2046        let mut templates = BloqTemplatePool::new();
2047        let template_id = templates.insert(BloqTemplate::new(circuit));
2048        let mut node = BloqNode::from_members(vec![]);
2049        node.expect_quantum_mut()
2050            .instances
2051            .push(TemplateInstance::new(
2052                TemplateInstanceId(0),
2053                template_id,
2054                ivec2(0, 0),
2055            ));
2056        node.emission_plan(&templates).unwrap_err()
2057    }
2058
2059    fn x_product() -> PauliMap {
2060        [(ivec2(0, 0), Pauli::X)].into_iter().collect()
2061    }
2062
2063    #[derive(Debug, Clone, Copy)]
2064    enum SegmentOccupant {
2065        Gate,
2066        Reset,
2067        Measure,
2068    }
2069
2070    fn append_mpp(circuit: &mut CoordCircuit) {
2071        circuit
2072            .measure_pauli_products([x_product()])
2073            .expect("non-empty product");
2074    }
2075
2076    fn append_occupant(circuit: &mut CoordCircuit, occupant: SegmentOccupant) {
2077        match occupant {
2078            SegmentOccupant::Gate => circuit.do_gate(GateType::H, [ivec2(0, 0)]).unwrap(),
2079            SegmentOccupant::Reset => circuit.do_gate(GateType::RZ, [ivec2(0, 0)]).unwrap(),
2080            SegmentOccupant::Measure => {
2081                circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
2082            }
2083        }
2084    }
2085
2086    /// A correction on (0,0) gated by a measurement of an off-target qubit, so
2087    /// the control is live without the control's own qubit joining the segment
2088    /// the correction is being tested against.
2089    fn append_conditional_pauli(circuit: &mut CoordCircuit) {
2090        let control = circuit.measure(PauliBasis::Z, [ivec2(9, 9)])[0];
2091        circuit
2092            .body_mut(circuit.entry_body())
2093            .unwrap()
2094            .ops_mut()
2095            .push(Op::ConditionalPauli(vec![ConditionalCorrection {
2096                pauli: PauliBasis::X,
2097                control,
2098                target: ivec2(0, 0),
2099            }]));
2100    }
2101
2102    fn single_instance_plan(circuit: CoordCircuit) -> NodeEmissionPlan {
2103        single_instance_plan_with_options(circuit, InstantiationOptions::default()).unwrap()
2104    }
2105
2106    fn single_instance_plan_with_options(
2107        circuit: CoordCircuit,
2108        options: InstantiationOptions<'_>,
2109    ) -> Result<NodeEmissionPlan, NodeTemplateInstanceMergeError> {
2110        let mut templates = BloqTemplatePool::new();
2111        let template_id = templates.insert(BloqTemplate::new(circuit));
2112        let mut node = BloqNode::from_members(vec![]);
2113        node.expect_quantum_mut()
2114            .instances
2115            .push(TemplateInstance::new(
2116                TemplateInstanceId(0),
2117                template_id,
2118                ivec2(0, 0),
2119            ));
2120        node.emission_plan_with_options(&templates, options)
2121    }
2122
2123    fn append_repeats(circuit: &mut CoordCircuit, repeats: &[(BodyId, u32)]) {
2124        circuit
2125            .body_mut(circuit.entry_body())
2126            .unwrap()
2127            .ops_mut()
2128            .extend(
2129                repeats
2130                    .iter()
2131                    .map(|&(body, repetitions)| Op::Repeat { body, repetitions }),
2132            );
2133    }
2134
2135    fn two_instance_overlap_error(
2136        first: CoordCircuit,
2137        second: CoordCircuit,
2138    ) -> NodeTemplateInstanceMergeError {
2139        let mut templates = BloqTemplatePool::new();
2140        let first = templates.insert(BloqTemplate::new(first));
2141        let second = templates.insert(BloqTemplate::new(second));
2142        let mut node = BloqNode::from_members(vec![]);
2143        node.expect_quantum_mut().instances = vec![
2144            TemplateInstance::new(TemplateInstanceId(0), first, ivec2(0, 0)),
2145            TemplateInstance::new(TemplateInstanceId(1), second, ivec2(0, 0)),
2146        ];
2147        node.emission_plan(&templates).unwrap_err()
2148    }
2149
2150    fn three_instance_overlap_error(
2151        first: CoordCircuit,
2152        second: CoordCircuit,
2153        third: CoordCircuit,
2154    ) -> NodeTemplateInstanceMergeError {
2155        let mut templates = BloqTemplatePool::new();
2156        let template_ids = [first, second, third]
2157            .into_iter()
2158            .map(|circuit| templates.insert(BloqTemplate::new(circuit)))
2159            .collect::<Vec<_>>();
2160        let mut node = BloqNode::from_members(vec![]);
2161        node.expect_quantum_mut().instances = template_ids
2162            .into_iter()
2163            .enumerate()
2164            .map(|(index, template_id)| {
2165                TemplateInstance::new(TemplateInstanceId(index as u32), template_id, ivec2(0, 0))
2166            })
2167            .collect();
2168        node.emission_plan(&templates).unwrap_err()
2169    }
2170
2171    fn single_instance_error(
2172        circuit: CoordCircuit,
2173        offset: glam::IVec2,
2174    ) -> NodeTemplateInstanceMergeError {
2175        let mut templates = BloqTemplatePool::new();
2176        let template_id = templates.insert(BloqTemplate::new(circuit));
2177        let mut node = BloqNode::from_members(vec![]);
2178        node.expect_quantum_mut()
2179            .instances
2180            .push(TemplateInstance::new(
2181                TemplateInstanceId(0),
2182                template_id,
2183                offset,
2184            ));
2185        node.emission_plan(&templates).unwrap_err()
2186    }
2187
2188    #[test]
2189    fn instantiate_rejects_mpp_product_without_measurement() {
2190        assert_eq!(
2191            malformed_mpp_error(vec![x_product(), PauliMap::empty()], vec![0]),
2192            NodeTemplateInstanceMergeError::MppMeasurementCountMismatch {
2193                products: 2,
2194                measurements: 1,
2195            }
2196        );
2197    }
2198
2199    #[test]
2200    fn instantiate_rejects_mpp_measurement_without_product() {
2201        assert_eq!(
2202            malformed_mpp_error(vec![x_product()], vec![0, 1]),
2203            NodeTemplateInstanceMergeError::MppMeasurementCountMismatch {
2204                products: 1,
2205                measurements: 2,
2206            }
2207        );
2208    }
2209
2210    #[test]
2211    fn mpp_conflicts_with_every_cross_instance_segment_occupant() {
2212        for occupant in [
2213            SegmentOccupant::Gate,
2214            SegmentOccupant::Reset,
2215            SegmentOccupant::Measure,
2216        ] {
2217            for mpp_first in [false, true] {
2218                let mut mpp = CoordCircuit::new();
2219                append_mpp(&mut mpp);
2220                let mut other = CoordCircuit::new();
2221                append_occupant(&mut other, occupant);
2222                let error = if mpp_first {
2223                    two_instance_overlap_error(mpp, other)
2224                } else {
2225                    two_instance_overlap_error(other, mpp)
2226                };
2227                assert_eq!(
2228                    error,
2229                    NodeTemplateInstanceMergeError::QubitConflict(ivec2(0, 0)),
2230                    "{occupant:?}, mpp_first={mpp_first}"
2231                );
2232            }
2233        }
2234    }
2235
2236    #[test]
2237    fn mpp_reuse_within_one_instance_remains_legal() {
2238        for occupant in [
2239            SegmentOccupant::Gate,
2240            SegmentOccupant::Reset,
2241            SegmentOccupant::Measure,
2242        ] {
2243            for mpp_first in [false, true] {
2244                let mut circuit = CoordCircuit::new();
2245                if mpp_first {
2246                    append_mpp(&mut circuit);
2247                    append_occupant(&mut circuit, occupant);
2248                } else {
2249                    append_occupant(&mut circuit, occupant);
2250                    append_mpp(&mut circuit);
2251                }
2252                let mut templates = BloqTemplatePool::new();
2253                let template = templates.insert(BloqTemplate::new(circuit));
2254                let mut node = BloqNode::from_members(vec![]);
2255                node.expect_quantum_mut()
2256                    .instances
2257                    .push(TemplateInstance::new(
2258                        TemplateInstanceId(0),
2259                        template,
2260                        ivec2(0, 0),
2261                    ));
2262
2263                node.emission_plan(&templates)
2264                    .unwrap_or_else(|error| panic!("{occupant:?}, mpp_first={mpp_first}: {error}"));
2265            }
2266        }
2267    }
2268
2269    #[test]
2270    fn gate_and_measurement_or_reset_reuse_within_one_instance_in_both_orders() {
2271        for occupant in [SegmentOccupant::Reset, SegmentOccupant::Measure] {
2272            for gate_first in [false, true] {
2273                let mut circuit = CoordCircuit::new();
2274                if gate_first {
2275                    append_occupant(&mut circuit, SegmentOccupant::Gate);
2276                    append_occupant(&mut circuit, occupant);
2277                } else {
2278                    append_occupant(&mut circuit, occupant);
2279                    append_occupant(&mut circuit, SegmentOccupant::Gate);
2280                }
2281                let plan = single_instance_plan(circuit);
2282                assert_eq!(
2283                    plan.circuit
2284                        .body(plan.circuit.entry_body())
2285                        .unwrap()
2286                        .ops()
2287                        .len(),
2288                    2,
2289                    "{occupant:?}, gate_first={gate_first}"
2290                );
2291            }
2292        }
2293    }
2294
2295    #[test]
2296    fn measurement_and_reset_reuse_within_one_instance_in_both_orders() {
2297        for measurement_first in [false, true] {
2298            let mut circuit = CoordCircuit::new();
2299            if measurement_first {
2300                append_occupant(&mut circuit, SegmentOccupant::Measure);
2301                append_occupant(&mut circuit, SegmentOccupant::Reset);
2302            } else {
2303                append_occupant(&mut circuit, SegmentOccupant::Reset);
2304                append_occupant(&mut circuit, SegmentOccupant::Measure);
2305            }
2306
2307            let plan = single_instance_plan(circuit);
2308            let ops = plan.circuit.body(plan.circuit.entry_body()).unwrap().ops();
2309            assert_eq!(ops.len(), 2, "measurement_first={measurement_first}");
2310            assert_eq!(plan.circuit.meas_registry().records().len(), 1);
2311        }
2312    }
2313
2314    #[test]
2315    fn repeated_measurements_within_one_instance_keep_distinct_outputs() {
2316        let mut circuit = CoordCircuit::new();
2317        append_occupant(&mut circuit, SegmentOccupant::Measure);
2318        append_occupant(&mut circuit, SegmentOccupant::Measure);
2319
2320        let plan = single_instance_plan(circuit);
2321        let ops = plan.circuit.body(plan.circuit.entry_body()).unwrap().ops();
2322        let measurements = ops
2323            .iter()
2324            .filter_map(|op| match op {
2325                Op::Measure { measurements, .. } => Some(measurements[0]),
2326                _ => None,
2327            })
2328            .collect::<Vec<_>>();
2329        assert_eq!(measurements.len(), 2);
2330        assert_ne!(measurements[0], measurements[1]);
2331        assert_eq!(plan.circuit.meas_registry().records().len(), 2);
2332    }
2333
2334    #[test]
2335    fn repeated_resets_within_one_instance_are_both_retained() {
2336        let mut circuit = CoordCircuit::new();
2337        append_occupant(&mut circuit, SegmentOccupant::Reset);
2338        append_occupant(&mut circuit, SegmentOccupant::Reset);
2339
2340        let plan = single_instance_plan(circuit);
2341        let reset_count = plan
2342            .circuit
2343            .body(plan.circuit.entry_body())
2344            .unwrap()
2345            .ops()
2346            .iter()
2347            .filter(|op| {
2348                matches!(
2349                    op,
2350                    Op::Gate {
2351                        gate: GateType::RZ,
2352                        ..
2353                    }
2354                )
2355            })
2356            .count();
2357        assert_eq!(reset_count, 2);
2358    }
2359
2360    #[test]
2361    fn multi_op_instance_cannot_enable_later_measurement_or_reset_dedup() {
2362        for occupant in [SegmentOccupant::Reset, SegmentOccupant::Measure] {
2363            for composite_first in [false, true] {
2364                let mut composite = CoordCircuit::new();
2365                append_occupant(&mut composite, occupant);
2366                append_occupant(&mut composite, occupant);
2367                let mut single = CoordCircuit::new();
2368                append_occupant(&mut single, occupant);
2369
2370                let error = if composite_first {
2371                    two_instance_overlap_error(composite, single)
2372                } else {
2373                    two_instance_overlap_error(single, composite)
2374                };
2375                assert_eq!(
2376                    error,
2377                    NodeTemplateInstanceMergeError::QubitConflict(ivec2(0, 0)),
2378                    "{occupant:?}, composite_first={composite_first}"
2379                );
2380            }
2381        }
2382    }
2383
2384    #[test]
2385    fn conditional_pauli_conflicts_with_every_cross_instance_segment_occupant() {
2386        for occupant in [
2387            SegmentOccupant::Gate,
2388            SegmentOccupant::Reset,
2389            SegmentOccupant::Measure,
2390        ] {
2391            for correction_first in [false, true] {
2392                let mut correction = CoordCircuit::new();
2393                append_conditional_pauli(&mut correction);
2394                let mut other = CoordCircuit::new();
2395                append_occupant(&mut other, occupant);
2396                let error = if correction_first {
2397                    two_instance_overlap_error(correction, other)
2398                } else {
2399                    two_instance_overlap_error(other, correction)
2400                };
2401                assert_eq!(
2402                    error,
2403                    NodeTemplateInstanceMergeError::QubitConflict(ivec2(0, 0)),
2404                    "{occupant:?}, correction_first={correction_first}"
2405                );
2406            }
2407        }
2408
2409        for correction_first in [false, true] {
2410            let mut correction = CoordCircuit::new();
2411            append_conditional_pauli(&mut correction);
2412            let mut mpp = CoordCircuit::new();
2413            append_mpp(&mut mpp);
2414            let error = if correction_first {
2415                two_instance_overlap_error(correction, mpp)
2416            } else {
2417                two_instance_overlap_error(mpp, correction)
2418            };
2419            assert_eq!(
2420                error,
2421                NodeTemplateInstanceMergeError::QubitConflict(ivec2(0, 0)),
2422                "MPP, correction_first={correction_first}"
2423            );
2424        }
2425    }
2426
2427    #[test]
2428    fn conditional_pauli_reuse_within_one_instance_remains_legal() {
2429        for occupant in [
2430            SegmentOccupant::Gate,
2431            SegmentOccupant::Reset,
2432            SegmentOccupant::Measure,
2433        ] {
2434            for correction_first in [false, true] {
2435                let mut circuit = CoordCircuit::new();
2436                if correction_first {
2437                    append_conditional_pauli(&mut circuit);
2438                    append_occupant(&mut circuit, occupant);
2439                } else {
2440                    append_occupant(&mut circuit, occupant);
2441                    append_conditional_pauli(&mut circuit);
2442                }
2443                let plan = single_instance_plan(circuit);
2444                assert_eq!(
2445                    plan.circuit
2446                        .body(plan.circuit.entry_body())
2447                        .unwrap()
2448                        .ops()
2449                        .len(),
2450                    // occupant + the correction and the measurement gating it
2451                    3,
2452                    "{occupant:?}, correction_first={correction_first}"
2453                );
2454            }
2455        }
2456
2457        for correction_first in [false, true] {
2458            let mut circuit = CoordCircuit::new();
2459            if correction_first {
2460                append_conditional_pauli(&mut circuit);
2461                append_mpp(&mut circuit);
2462            } else {
2463                append_mpp(&mut circuit);
2464                append_conditional_pauli(&mut circuit);
2465            }
2466            let plan = single_instance_plan(circuit);
2467            assert_eq!(
2468                plan.circuit
2469                    .body(plan.circuit.entry_body())
2470                    .unwrap()
2471                    .ops()
2472                    .len(),
2473                // MPP + the correction and the measurement gating it
2474                3,
2475                "MPP, correction_first={correction_first}"
2476            );
2477        }
2478    }
2479
2480    #[test]
2481    fn composite_first_instance_cannot_enable_later_measurement_or_reset_dedup() {
2482        for occupant in [SegmentOccupant::Reset, SegmentOccupant::Measure] {
2483            let mut composite = CoordCircuit::new();
2484            append_mpp(&mut composite);
2485            append_occupant(&mut composite, occupant);
2486            let mut second = CoordCircuit::new();
2487            append_occupant(&mut second, occupant);
2488            let mut third = CoordCircuit::new();
2489            append_occupant(&mut third, occupant);
2490
2491            assert_eq!(
2492                three_instance_overlap_error(composite, second, third),
2493                NodeTemplateInstanceMergeError::QubitConflict(ivec2(0, 0)),
2494                "{occupant:?}"
2495            );
2496        }
2497    }
2498
2499    #[test]
2500    fn instance_translation_checks_coordinate_overflow_for_every_operation_shape() {
2501        let coordinate = ivec2(i32::MAX, 0);
2502        let offset = ivec2(1, 0);
2503
2504        let mut gate = CoordCircuit::new();
2505        gate.do_gate(GateType::H, [coordinate]).unwrap();
2506
2507        let mut measure = CoordCircuit::new();
2508        measure.measure(PauliBasis::Z, [coordinate]);
2509
2510        let mut mpp = CoordCircuit::new();
2511        mpp.measure_pauli_products([[((coordinate), Pauli::X)].into_iter().collect()])
2512            .unwrap();
2513
2514        let mut correction = CoordCircuit::new();
2515        let control = correction.measure(PauliBasis::Z, [ivec2(0, 0)])[0];
2516        correction
2517            .body_mut(correction.entry_body())
2518            .unwrap()
2519            .ops_mut()
2520            .push(Op::ConditionalPauli(vec![ConditionalCorrection {
2521                pauli: PauliBasis::X,
2522                control,
2523                target: coordinate,
2524            }]));
2525
2526        for (kind, circuit) in [
2527            ("gate", gate),
2528            ("measure", measure),
2529            ("MPP", mpp),
2530            ("correction", correction),
2531        ] {
2532            assert_eq!(
2533                single_instance_error(circuit, offset),
2534                NodeTemplateInstanceMergeError::CoordinateOverflow(CoordinateOverflowError {
2535                    coordinate,
2536                    offset,
2537                }),
2538                "{kind}"
2539            );
2540        }
2541    }
2542
2543    #[test]
2544    fn instance_translation_checks_unused_registry_coordinates() {
2545        let coordinate = ivec2(i32::MAX, 0);
2546        let offset = ivec2(1, 0);
2547        let mut circuit = CoordCircuit::new();
2548        circuit.register_measurement_id(0, coordinate);
2549
2550        assert_eq!(
2551            single_instance_error(circuit, offset),
2552            NodeTemplateInstanceMergeError::CoordinateOverflow(CoordinateOverflowError {
2553                coordinate,
2554                offset,
2555            })
2556        );
2557    }
2558
2559    #[test]
2560    fn instance_translation_rejects_control_after_zero_repeat() {
2561        let measured = ivec2(0, 0);
2562        let target = ivec2(1, 0);
2563        let mut circuit = CoordCircuit::new();
2564        circuit.register_measurement_id(0, measured);
2565        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
2566            basis: PauliBasis::Z,
2567            qubits: vec![measured],
2568            measurements: vec![0],
2569            flip_probability: 0.0,
2570        }]));
2571        circuit
2572            .body_mut(circuit.entry_body())
2573            .unwrap()
2574            .ops_mut()
2575            .extend([
2576                Op::Repeat {
2577                    body,
2578                    repetitions: 0,
2579                },
2580                Op::ConditionalPauli(vec![ConditionalCorrection {
2581                    pauli: PauliBasis::X,
2582                    control: 0,
2583                    target,
2584                }]),
2585            ]);
2586
2587        assert_eq!(
2588            single_instance_error(circuit, ivec2(0, 0)),
2589            NodeTemplateInstanceMergeError::ControlReferencesUnknownMeasurement(0)
2590        );
2591    }
2592
2593    #[test]
2594    fn instantiate_circuit_merges_translated_template_instances() {
2595        let mut template_circuit = CoordCircuit::new();
2596        template_circuit
2597            .do_gate(GateType::H, [ivec2(0, 0)])
2598            .unwrap();
2599        template_circuit.tick();
2600        template_circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
2601        let mut templates = BloqTemplatePool::new();
2602        let template_id = templates.insert(BloqTemplate::new(template_circuit));
2603        let mut node = BloqNode::from_members(vec![]);
2604        node.expect_quantum_mut().instances = vec![
2605            TemplateInstance::new(TemplateInstanceId(0), template_id, ivec2(0, 0)),
2606            TemplateInstance::new(TemplateInstanceId(1), template_id, ivec2(2, 0)),
2607        ];
2608
2609        let circuit = node.instantiate_circuit(&templates).unwrap();
2610        let measurement_map = node.emission_plan(&templates).unwrap().measurements;
2611        let ops = circuit.body(circuit.entry_body()).unwrap().ops();
2612
2613        assert!(matches!(
2614            &ops[0],
2615            Op::Gate {
2616                gate: GateType::H,
2617                qubits,
2618            } if qubits == &vec![ivec2(0, 0)]
2619        ));
2620        assert!(matches!(
2621            &ops[1],
2622            Op::Gate {
2623                gate: GateType::H,
2624                qubits,
2625            } if qubits == &vec![ivec2(2, 0)]
2626        ));
2627        assert!(matches!(ops[2], Op::Tick));
2628        assert_eq!(circuit.meas_registry().records().len(), 2);
2629        assert_eq!(
2630            circuit
2631                .meas_registry()
2632                .records()
2633                .iter()
2634                .map(|record| record.qubit)
2635                .collect::<Vec<_>>(),
2636            vec![ivec2(0, 0), ivec2(2, 0)]
2637        );
2638        assert_eq!(measurement_map[&instance_measurement(0, 0)], 0);
2639        assert_eq!(measurement_map[&instance_measurement(1, 0)], 1);
2640    }
2641
2642    #[test]
2643    fn noisy_instantiation_annotates_shared_physical_op_once_after_merge() {
2644        let qubit = ivec2(0, 0);
2645        let mut template_circuit = CoordCircuit::new();
2646        template_circuit.do_gate(GateType::RZ, [qubit]).unwrap();
2647        let mut templates = BloqTemplatePool::new();
2648        let template_id = templates.insert(BloqTemplate::new(template_circuit));
2649        let mut node = BloqNode::from_members(vec![]);
2650        node.expect_quantum_mut().instances = vec![
2651            TemplateInstance::new(TemplateInstanceId(0), template_id, IVec2::ZERO),
2652            TemplateInstance::new(TemplateInstanceId(1), template_id, IVec2::ZERO),
2653        ];
2654        let noise = NoiseModel {
2655            p_reset: 0.125,
2656            ..NoiseModel::uniform_depolarizing(0.0)
2657        };
2658
2659        let circuit = node
2660            .emission_plan_with_options(&templates, InstantiationOptions::noisy(&noise))
2661            .unwrap()
2662            .circuit;
2663        let ops = circuit.body(circuit.entry_body()).unwrap().ops();
2664
2665        assert_eq!(ops.len(), 2);
2666        assert!(matches!(
2667            &ops[0],
2668            Op::Gate { gate: GateType::RZ, qubits } if qubits == &[qubit]
2669        ));
2670        assert!(matches!(
2671            &ops[1],
2672            Op::PauliError { probability, pauli: PauliBasis::X, qubits }
2673                if *probability == 0.125 && qubits == &[qubit]
2674        ));
2675    }
2676
2677    #[test]
2678    fn noisy_singleton_preserves_repeat_boundaries() {
2679        let mut template_circuit = CoordCircuit::new();
2680        template_circuit
2681            .do_gate(GateType::H, [IVec2::ZERO])
2682            .unwrap();
2683        template_circuit.tick();
2684        let body = template_circuit.add_body(CircuitBody::from_ops(vec![
2685            Op::Gate {
2686                gate: GateType::H,
2687                qubits: vec![ivec2(1, 0)],
2688            },
2689            Op::Tick,
2690        ]));
2691        append_repeats(&mut template_circuit, &[(body, 2)]);
2692        let noise = NoiseModel {
2693            p_idle: 0.125,
2694            ..NoiseModel::uniform_depolarizing(0.0)
2695        };
2696
2697        let plan = single_instance_plan_with_options(
2698            template_circuit,
2699            InstantiationOptions::noisy(&noise),
2700        )
2701        .unwrap();
2702        assert!(plan.normalized_templates.is_none());
2703        assert_eq!(plan.circuit.entry_top_level_repeats(), [2]);
2704    }
2705
2706    #[test]
2707    fn noise_does_not_make_incompatible_repeat_shapes_valid() {
2708        let program = crate::Bloq::from_text(
2709            "BLOQIR 1
2710template t0 {
2711  circuit {
2712    H (0,0)
2713    REPEAT 2 b1
2714  }
2715  body b1 {
2716    X (0,0)
2717    TICK
2718  }
2719}
2720template t1 {
2721  circuit {
2722    H (1,0)
2723    X (1,0)
2724    TICK
2725    X (1,0)
2726    TICK
2727  }
2728}
2729graph {
2730  n0 quantum {
2731    instance i0 t0 @ (0,0)
2732    instance i1 t1 @ (0,0)
2733  }
2734}",
2735        )
2736        .unwrap();
2737        let noise = NoiseModel {
2738            p_idle: 0.125,
2739            ..NoiseModel::uniform_depolarizing(0.0)
2740        };
2741        let clean = program.validate().unwrap_err();
2742        assert!(matches!(
2743            clean,
2744            crate::BloqValidationError::InvalidInstanceMergeStructure {
2745                source: NodeTemplateInstanceMergeError::RepeatShapeMismatch,
2746                ..
2747            }
2748        ));
2749        assert_eq!(
2750            program
2751                .validate_with_plans(&InstantiationOptions::noisy(&noise))
2752                .unwrap_err(),
2753            clean
2754        );
2755    }
2756
2757    #[test]
2758    fn noisy_multi_instance_plan_keeps_merge_isolation_ticks() {
2759        let mut circuit = CoordCircuit::new();
2760        circuit.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
2761        let body = circuit.add_body(CircuitBody::from_ops(vec![
2762            Op::Gate {
2763                gate: GateType::X,
2764                qubits: vec![IVec2::ZERO],
2765            },
2766            Op::Tick,
2767        ]));
2768        circuit.push_repeat(body, 2);
2769        let mut templates = BloqTemplatePool::new();
2770        let template = templates.insert(BloqTemplate::new(circuit));
2771        let mut node = BloqNode::from_members(Vec::new());
2772        node.expect_quantum_mut().instances = vec![
2773            TemplateInstance::new(TemplateInstanceId(0), template, IVec2::ZERO),
2774            TemplateInstance::new(TemplateInstanceId(1), template, IVec2::X),
2775        ];
2776        let noise = NoiseModel {
2777            p_idle: 0.125,
2778            ..NoiseModel::uniform_depolarizing(0.0)
2779        };
2780        let clean = node.emission_plan(&templates).unwrap();
2781        let noisy = node
2782            .emission_plan_with_options(&templates, InstantiationOptions::noisy(&noise))
2783            .unwrap();
2784        assert!(noisy.normalized_templates.is_none());
2785        let mut actual = noisy.circuit;
2786        let mut expected = noise.noisy_circuit(&clean.circuit).unwrap();
2787        actual.flatten().unwrap();
2788        expected.flatten().unwrap();
2789        assert_eq!(
2790            actual.body(actual.entry_body()).unwrap().ops(),
2791            expected.body(expected.entry_body()).unwrap().ops()
2792        );
2793    }
2794
2795    #[test]
2796    fn noisy_repeat_expansion_preserves_idle_members_and_measurement_aliases() {
2797        let mut circuit = CoordCircuit::new();
2798        circuit.do_gate(GateType::RZ, [IVec2::ZERO]).unwrap();
2799        let measurement = 0;
2800        circuit.register_measurement_id(measurement, IVec2::ZERO);
2801        let body = circuit.add_body(CircuitBody::from_ops(vec![
2802            Op::Measure {
2803                basis: PauliBasis::Z,
2804                qubits: vec![IVec2::ZERO],
2805                measurements: vec![measurement],
2806                flip_probability: 0.0,
2807            },
2808            Op::Tick,
2809            Op::Gate {
2810                gate: GateType::RZ,
2811                qubits: vec![IVec2::ZERO],
2812            },
2813        ]));
2814        circuit.push_repeat(body, 2);
2815        let mut template = BloqTemplate::new(circuit);
2816        template.detectors.push(crate::TemplateDetector {
2817            scope: crate::TemplateDetectorScope::RepeatBody { body },
2818            parity: crate::TemplateDetectorParity::from_measurements([measurement]),
2819            coords: None,
2820        });
2821        let noise = NoiseModel {
2822            p_idle: 0.125,
2823            ..NoiseModel::uniform_depolarizing(0.0)
2824        };
2825        // Shared measurements, disjoint measurements, then an idle member
2826        // whose empty segment aligns with the other instance's repeat.
2827        for measured_offset in [Some(0), Some(2), None] {
2828            let mut templates = BloqTemplatePool::new();
2829            let first = templates.insert(template.clone());
2830            let second = if measured_offset.is_some() {
2831                first
2832            } else {
2833                let mut idle = CoordCircuit::new();
2834                idle.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
2835                idle.tick();
2836                idle.tick();
2837                templates.insert(BloqTemplate::new(idle))
2838            };
2839            let mut node = BloqNode::from_members(Vec::new());
2840            node.expect_quantum_mut().instances = vec![
2841                TemplateInstance::new(TemplateInstanceId(0), first, IVec2::ZERO),
2842                TemplateInstance::new(
2843                    TemplateInstanceId(1),
2844                    second,
2845                    ivec2(measured_offset.unwrap_or(2), 0),
2846                ),
2847            ];
2848            let mut clean = node.emission_plan(&templates).unwrap().circuit;
2849            clean.flatten().unwrap();
2850            let plan = node
2851                .emission_plan_with_options(&templates, InstantiationOptions::noisy(&noise))
2852                .unwrap();
2853            assert!(plan.normalized_templates.is_some());
2854            let expected = noise.noisy_circuit(&clean).unwrap();
2855            assert_eq!(
2856                plan.circuit.body(plan.circuit.entry_body()).unwrap().ops(),
2857                expected.body(expected.entry_body()).unwrap().ops()
2858            );
2859            let normalized = plan.templates(&templates);
2860            assert_eq!(normalized[first].detectors.len(), 2);
2861            for instance in &node.expect_quantum().instances {
2862                for (iteration, detector) in normalized[instance.template_id]
2863                    .detectors
2864                    .iter()
2865                    .enumerate()
2866                {
2867                    let source = detector.parity.measurements().next().unwrap();
2868                    let expected = clean
2869                        .body(clean.entry_body())
2870                        .unwrap()
2871                        .ops()
2872                        .iter()
2873                        .filter_map(|op| match op {
2874                            Op::Measure {
2875                                qubits,
2876                                measurements,
2877                                ..
2878                            } if qubits == &[instance.offset] => Some(measurements[0]),
2879                            _ => None,
2880                        })
2881                        .nth(iteration)
2882                        .unwrap();
2883                    assert_eq!(
2884                        plan.measurements[&instance_measurement(instance.id.0, source)],
2885                        expected
2886                    );
2887                }
2888            }
2889        }
2890    }
2891
2892    #[test]
2893    fn noisy_instantiation_rejects_non_finite_model_probability() {
2894        let mut templates = BloqTemplatePool::new();
2895        let template_id = templates.insert(BloqTemplate::new(CoordCircuit::new()));
2896        let mut node = BloqNode::from_members(vec![]);
2897        node.expect_quantum_mut()
2898            .instances
2899            .push(TemplateInstance::new(
2900                TemplateInstanceId(0),
2901                template_id,
2902                IVec2::ZERO,
2903            ));
2904        let mut noise = NoiseModel::uniform_depolarizing(0.0);
2905        noise.p1 = f64::NAN;
2906
2907        let error = node
2908            .emission_plan_with_options(&templates, InstantiationOptions::noisy(&noise))
2909            .unwrap_err();
2910
2911        assert_eq!(
2912            error,
2913            NodeTemplateInstanceMergeError::InvalidProbability {
2914                instruction: "DEPOLARIZE1"
2915            }
2916        );
2917    }
2918
2919    #[test]
2920    fn instantiate_circuit_reports_repeat_body_map() {
2921        let mut template_circuit = CoordCircuit::new();
2922        let body = template_circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
2923            basis: PauliBasis::Z,
2924            qubits: vec![ivec2(0, 0)],
2925            measurements: vec![0],
2926            flip_probability: 0.0,
2927        }]));
2928        template_circuit.register_measurement_id(0, ivec2(0, 0));
2929        template_circuit
2930            .body_mut(template_circuit.entry_body())
2931            .unwrap()
2932            .ops_mut()
2933            .push(Op::Repeat {
2934                body,
2935                repetitions: 2,
2936            });
2937        let mut templates = BloqTemplatePool::new();
2938        let template_id = templates.insert(BloqTemplate::new(template_circuit));
2939        let mut node = BloqNode::from_members(vec![]);
2940        node.expect_quantum_mut()
2941            .instances
2942            .push(TemplateInstance::new(
2943                TemplateInstanceId(0),
2944                template_id,
2945                ivec2(0, 0),
2946            ));
2947
2948        let instantiated = node.emission_plan(&templates).unwrap();
2949        let Op::Repeat {
2950            body: instantiated_body,
2951            ..
2952        } = instantiated
2953            .circuit
2954            .body(instantiated.circuit.entry_body())
2955            .unwrap()
2956            .ops()[0]
2957        else {
2958            panic!("expected repeat op");
2959        };
2960
2961        assert_eq!(
2962            instantiated.bodies[&(TemplateInstanceId(0), body)],
2963            instantiated_body
2964        );
2965    }
2966
2967    #[test]
2968    fn multi_instance_merge_reuses_aligned_repeat_body_tuple() {
2969        let mut template_circuit = CoordCircuit::new();
2970        let body = template_circuit.add_body(CircuitBody::new());
2971        append_repeats(&mut template_circuit, &[(body, 2), (body, 3)]);
2972        let circuits = [template_circuit.clone(), template_circuit];
2973
2974        let merged = merge_instantiated_circuits_with_body_aliases(&circuits).unwrap();
2975        let repeat_bodies = merged
2976            .circuit
2977            .body(merged.circuit.entry_body())
2978            .unwrap()
2979            .ops()
2980            .iter()
2981            .filter_map(|op| match op {
2982                Op::Repeat { body, .. } => Some(*body),
2983                _ => None,
2984            })
2985            .collect::<Vec<_>>();
2986
2987        assert_eq!(repeat_bodies.len(), 2);
2988        assert_eq!(repeat_bodies[0], repeat_bodies[1]);
2989        assert_eq!(merged.circuit.body_count(), 2);
2990        assert_eq!(merged.body_aliases[&(0, body)], repeat_bodies[0]);
2991        assert_eq!(merged.body_aliases[&(1, body)], repeat_bodies[0]);
2992    }
2993
2994    #[test]
2995    fn deep_multi_instance_merge_preserves_shared_bodies_and_measurement_aliases() {
2996        fn circuit(target: IVec2) -> CoordCircuit {
2997            let mut circuit = CoordCircuit::new();
2998            let control = circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2999            circuit
3000                .body_mut(circuit.entry_body())
3001                .unwrap()
3002                .ops_mut()
3003                .push(Op::ConditionalPauli(vec![ConditionalCorrection {
3004                    pauli: PauliBasis::X,
3005                    control,
3006                    target,
3007                }]));
3008            for _ in 0..4096 {
3009                let child = circuit.entry_body();
3010                let parent = circuit.add_body(CircuitBody::from_ops(vec![
3011                    Op::Repeat {
3012                        body: child,
3013                        repetitions: 1,
3014                    },
3015                    Op::Repeat {
3016                        body: child,
3017                        repetitions: 2,
3018                    },
3019                ]));
3020                circuit.set_entry_body(parent).unwrap();
3021            }
3022            circuit
3023        }
3024
3025        let mut templates = BloqTemplatePool::new();
3026        let mut node = BloqNode::from_members(Vec::new());
3027        for index in 0..2 {
3028            let template = templates.insert(BloqTemplate::new(circuit(ivec2(index + 1, 0))));
3029            node.expect_quantum_mut()
3030                .instances
3031                .push(TemplateInstance::new(
3032                    TemplateInstanceId(index as u32),
3033                    template,
3034                    IVec2::ZERO,
3035                ));
3036        }
3037        let plan = node.emission_plan(&templates).unwrap();
3038        assert_eq!(plan.circuit.body_count(), 4097);
3039        assert_eq!(plan.bodies.len(), 8194);
3040        assert_eq!(plan.circuit.meas_registry().records().len(), 1);
3041        assert_eq!(
3042            plan.measurements[&instance_measurement(0, 0)],
3043            plan.measurements[&instance_measurement(1, 0)]
3044        );
3045        let mut body = plan.circuit.entry_body();
3046        for _ in 0..4096 {
3047            let [
3048                Op::Repeat {
3049                    body: first,
3050                    repetitions: 1,
3051                },
3052                Op::Tick,
3053                Op::Repeat {
3054                    body: second,
3055                    repetitions: 2,
3056                },
3057                Op::Tick,
3058            ] = plan.circuit.body(body).unwrap().ops()
3059            else {
3060                panic!("shared repeats and isolation ticks must retain their order");
3061            };
3062            assert_eq!(first, second);
3063            body = *first;
3064        }
3065        let ops = plan.circuit.body(body).unwrap().ops();
3066        assert_eq!(ops.len(), 3);
3067        for op in &ops[1..] {
3068            let Op::ConditionalPauli(corrections) = op else {
3069                panic!("expected merged corrections");
3070            };
3071            assert_eq!(
3072                corrections[0].control,
3073                plan.measurements[&instance_measurement(0, 0)]
3074            );
3075        }
3076    }
3077
3078    #[test]
3079    fn multi_instance_merge_rejects_one_body_in_different_tuples() {
3080        let mut first = CoordCircuit::new();
3081        let shared = first.add_body(CircuitBody::new());
3082        append_repeats(&mut first, &[(shared, 2), (shared, 2)]);
3083        let mut second = CoordCircuit::new();
3084        let left = second.add_body(CircuitBody::new());
3085        let right = second.add_body(CircuitBody::new());
3086        append_repeats(&mut second, &[(left, 2), (right, 2)]);
3087        let circuits = [first, second];
3088
3089        assert_eq!(
3090            merge_instantiated_circuits_with_body_aliases(&circuits)
3091                .err()
3092                .unwrap(),
3093            NodeTemplateInstanceMergeError::BodyMergeConflict {
3094                circuit_index: 0,
3095                body: shared,
3096            }
3097        );
3098    }
3099
3100    #[test]
3101    fn instantiate_circuit_aliases_duplicate_measurements_in_one_segment() {
3102        let mut template_circuit = CoordCircuit::new();
3103        template_circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
3104        let mut templates = BloqTemplatePool::new();
3105        let template_id = templates.insert(BloqTemplate::new(template_circuit));
3106        let mut node = BloqNode::from_members(vec![]);
3107        node.expect_quantum_mut().instances = vec![
3108            TemplateInstance::new(TemplateInstanceId(0), template_id, ivec2(0, 0)),
3109            TemplateInstance::new(TemplateInstanceId(1), template_id, ivec2(0, 0)),
3110        ];
3111
3112        let instantiated = node.emission_plan(&templates).unwrap();
3113
3114        assert_eq!(instantiated.circuit.meas_registry().records().len(), 1);
3115        assert_eq!(
3116            instantiated.measurements[&instance_measurement(0, 0)],
3117            instantiated.measurements[&instance_measurement(1, 0)]
3118        );
3119    }
3120
3121    #[test]
3122    fn conditional_membership_preserves_common_records_and_selected_aliases() {
3123        use crate::{Bloq, QuantumGuard};
3124
3125        let mut circuit = CoordCircuit::new();
3126        circuit.measure(PauliBasis::Z, [IVec2::ZERO]);
3127        let mut bloq = Bloq::new();
3128        let template = bloq.add_template(BloqTemplate::new(circuit));
3129        let mut node = BloqNode::from_members(Vec::new());
3130        node.expect_quantum_mut().instances = vec![
3131            TemplateInstance::new(TemplateInstanceId(0), template, IVec2::ZERO),
3132            TemplateInstance::new(TemplateInstanceId(1), template, IVec2::ZERO),
3133        ];
3134        node.expect_quantum_mut().guards.push(QuantumGuard {
3135            input: 7,
3136            instances: vec![TemplateInstanceId(0)],
3137            ..Default::default()
3138        });
3139        assert!(matches!(
3140            node.emission_plan(bloq.templates()),
3141            Err(NodeTemplateInstanceMergeError::MembershipSelectionRequired)
3142        ));
3143        assert!(matches!(
3144            node.select_quantum_members(|_| None),
3145            Err(NodeTemplateInstanceMergeError::MissingMembershipInput(7))
3146        ));
3147        let id = bloq.add_node(node);
3148        for restored in [
3149            Bloq::from_text(&bloq.to_text()).unwrap(),
3150            Bloq::from_binary(&bloq.to_binary()).unwrap(),
3151        ] {
3152            for enabled in [false, true, false] {
3153                let selected = restored[id]
3154                    .select_quantum_members(|slot| {
3155                        assert_eq!(slot, 7);
3156                        Some(enabled)
3157                    })
3158                    .unwrap();
3159                let plan = selected.emission_plan(restored.templates()).unwrap();
3160                assert_eq!(plan.circuit.meas_registry().records().len(), 1);
3161                assert!(plan.measurements.contains_key(&instance_measurement(1, 0)));
3162                assert_eq!(
3163                    plan.measurements.contains_key(&instance_measurement(0, 0)),
3164                    enabled
3165                );
3166                if enabled {
3167                    assert_eq!(
3168                        plan.measurements[&instance_measurement(0, 0)],
3169                        plan.measurements[&instance_measurement(1, 0)]
3170                    );
3171                }
3172            }
3173            assert_eq!(
3174                restored[id].expect_quantum().instances.len(),
3175                2,
3176                "selection never mutates the shared registry"
3177            );
3178        }
3179    }
3180
3181    #[test]
3182    fn conditional_membership_rejects_missing_or_duplicate_registrations() {
3183        use crate::QuantumGuard;
3184
3185        let mut node = BloqNode::from_members(Vec::new());
3186        node.expect_quantum_mut().guards.push(QuantumGuard {
3187            input: 0,
3188            instances: vec![TemplateInstanceId(8)],
3189            ..Default::default()
3190        });
3191        assert!(matches!(
3192            node.select_quantum_members(|_| Some(false)),
3193            Err(NodeTemplateInstanceMergeError::InvalidMembership(_))
3194        ));
3195        node.expect_quantum_mut()
3196            .instances
3197            .push(TemplateInstance::new(
3198                TemplateInstanceId(8),
3199                crate::TemplateId(0),
3200                IVec2::ZERO,
3201            ));
3202        node.expect_quantum_mut().guards[0]
3203            .instances
3204            .push(TemplateInstanceId(8));
3205        assert!(matches!(
3206            node.select_quantum_members(|_| Some(true)),
3207            Err(NodeTemplateInstanceMergeError::InvalidMembership(_))
3208        ));
3209    }
3210
3211    #[test]
3212    fn bundle_only_registration_selects_whole_use_and_checks_false_indices() {
3213        use crate::{DetectorBundleId, DetectorBundleUse, QuantumGuard};
3214
3215        let mut node = BloqNode::from_members(Vec::new());
3216        node.expect_quantum_mut()
3217            .detector_bundles
3218            .push(DetectorBundleUse {
3219                bundle: DetectorBundleId(0),
3220                instances: Vec::new(),
3221                offset: IVec2::ZERO,
3222            });
3223        node.expect_quantum_mut().guards.push(QuantumGuard {
3224            input: 3,
3225            detector_bundles: vec![0],
3226            ..Default::default()
3227        });
3228        assert!(
3229            node.select_quantum_members(|_| Some(false))
3230                .unwrap()
3231                .expect_quantum()
3232                .detector_bundles
3233                .is_empty()
3234        );
3235        assert_eq!(
3236            node.select_quantum_members(|_| Some(true))
3237                .unwrap()
3238                .expect_quantum()
3239                .detector_bundles
3240                .len(),
3241            1
3242        );
3243        node.expect_quantum_mut().guards[0].detector_bundles.push(9);
3244        assert!(matches!(
3245            node.select_quantum_members(|_| Some(false)),
3246            Err(NodeTemplateInstanceMergeError::InvalidMembership(_))
3247        ));
3248    }
3249
3250    #[test]
3251    fn measurement_aliases_rewrite_merged_correction_controls() {
3252        fn measured_correction(target: glam::IVec2) -> CoordCircuit {
3253            let measured = ivec2(0, 0);
3254            let mut circuit = CoordCircuit::new();
3255            let measurement = circuit.measure(PauliBasis::Z, [measured])[0];
3256            circuit
3257                .body_mut(circuit.entry_body())
3258                .unwrap()
3259                .ops_mut()
3260                .push(Op::ConditionalPauli(vec![ConditionalCorrection {
3261                    pauli: PauliBasis::X,
3262                    control: measurement,
3263                    target,
3264                }]));
3265            circuit
3266        }
3267
3268        let mut templates = BloqTemplatePool::new();
3269        let first = templates.insert(BloqTemplate::new(measured_correction(ivec2(1, 0))));
3270        let second = templates.insert(BloqTemplate::new(measured_correction(ivec2(2, 0))));
3271        let mut node = BloqNode::from_members(vec![]);
3272        node.expect_quantum_mut().instances = vec![
3273            TemplateInstance::new(TemplateInstanceId(0), first, ivec2(0, 0)),
3274            TemplateInstance::new(TemplateInstanceId(1), second, ivec2(0, 0)),
3275        ];
3276
3277        let plan = node.emission_plan(&templates).unwrap();
3278        let controls = plan
3279            .circuit
3280            .body(plan.circuit.entry_body())
3281            .unwrap()
3282            .ops()
3283            .iter()
3284            .filter_map(|op| match op {
3285                Op::ConditionalPauli(corrections) => Some(corrections[0].control),
3286                _ => None,
3287            })
3288            .collect::<Vec<_>>();
3289
3290        assert_eq!(plan.circuit.meas_registry().records().len(), 1);
3291        assert_eq!(controls, vec![0, 0]);
3292    }
3293}