1use std::collections::BTreeSet;
4use std::sync::Arc;
5
6use bloq_ir::circuit::{
7 DetectorParity, DetectorTerm, GateType, NoiseModel, Op, PauliBasis, PauliMap,
8};
9use bloq_ir::lowering::{
10 InstanceMeasurement, InstantiationOptions, NodeEmissionPlan, NodeTemplateInstanceMergeError,
11 TemplateDetectorScope, TemplateInstanceId,
12};
13use bloq_ir::{
14 Bloq, BloqEdge, BloqNode, BloqNodeId, BloqNodeKind, BodySelector, BoundaryFace, ClassicalExpr,
15 ClassicalNode, CoordinateOverflowError, CycleDetected, FlattenError, LevelPath,
16 MemoryRoundTarget, NodeDetectorParity, NodeProvenance, ObservableOutput, PipePadding,
17 RegionNode, SubGraph, TemporalPipeRef, ValueRef,
18};
19use glam::{IVec2, IVec3};
20use rustc_hash::FxHashMap;
21
22use crate::instruction::*;
23
24pub const DEFAULT_MAX_QUANTUM_VARIANTS: usize = 4_096;
26
27type ScopedNode = (LevelPath, BloqNodeId);
28#[derive(Clone)]
29struct DynamicWait {
30 siblings: Vec<ScopedNode>,
31 hoist_after: Option<ScopedNode>,
32 origin: TaskOrigin,
33}
34type DynamicWaits = FxHashMap<ScopedNode, DynamicWait>;
35
36fn task_origin(function: TaskFunction, members: impl IntoIterator<Item = IVec3>) -> TaskOrigin {
37 let members = members
38 .into_iter()
39 .map(|member| member.to_array())
40 .collect::<BTreeSet<_>>();
41 let sites = members
42 .iter()
43 .map(|member| [member[0], member[1]])
44 .collect::<BTreeSet<_>>();
45 TaskOrigin {
46 function,
47 sites: sites.into_iter().collect(),
48 members: members.into_iter().collect(),
49 }
50}
51
52fn pipe_origin(pipes: impl IntoIterator<Item = TemporalPipeRef>) -> TaskOrigin {
53 task_origin(
54 TaskFunction::Memory,
55 pipes.into_iter().flat_map(|pipe| [pipe.src, pipe.dst]),
56 )
57}
58
59fn merge_origins<'a>(
60 function: TaskFunction,
61 origins: impl IntoIterator<Item = &'a TaskOrigin>,
62) -> TaskOrigin {
63 task_origin(
64 function,
65 origins
66 .into_iter()
67 .flat_map(|origin| origin.members.iter().copied().map(IVec3::from_array)),
68 )
69}
70
71#[derive(Clone)]
72struct TerminalHold {
73 owner: ScopedNode,
74 wait: ScopedNode,
75 bindings: Vec<ScopedNode>,
76}
77
78#[derive(Debug, Clone, Copy)]
80pub struct LoweringConfig<'a> {
81 pub gate_duration: f64,
84 pub decoder_latency_rounds: u32,
86 pub noise: Option<&'a NoiseModel>,
88 pub source_timing: SourceTiming,
91 pub max_quantum_variants: usize,
93}
94
95#[derive(Debug, Clone, Copy, Default, PartialEq)]
97pub struct SourceTiming {
98 pub factory: f64,
100 pub input: f64,
102 pub clifford: f64,
104}
105
106impl Default for LoweringConfig<'_> {
107 fn default() -> Self {
108 Self {
109 gate_duration: 1.0,
110 decoder_latency_rounds: DEFAULT_DECODER_LATENCY_ROUNDS,
111 noise: None,
112 source_timing: SourceTiming::default(),
113 max_quantum_variants: DEFAULT_MAX_QUANTUM_VARIANTS,
114 }
115 }
116}
117
118#[derive(Debug, thiserror::Error)]
120#[non_exhaustive]
121pub enum LowerError {
122 #[error("Bloq IR materialization failed: {0}")]
124 InvalidProgram(#[from] bloq_ir::BloqValidationError),
125 #[error("Bloq IR flattening failed: {0}")]
127 Flatten(#[from] FlattenError),
128 #[error("dynamic join memory insertion failed: {0}")]
130 Edit(#[from] bloq_ir::EditError),
131 #[error("dynamic join region structure is unsupported: {0}")]
133 Structure(#[from] bloq_ir::StructureError),
134 #[error("execution closure analysis failed: {0}")]
136 Analysis(#[from] crate::ExecError),
137 #[error("moment alignment failed: {0}")]
139 Alignment(#[from] bloq_ir::MomentAlignmentError),
140 #[error("quantum stage instantiation failed: {0}")]
142 Instantiation(#[from] NodeTemplateInstanceMergeError),
143 #[error("{0}")]
145 DetectorBundle(#[from] bloq_ir::DetectorBundleError),
146 #[error("Bloq IR graph is cyclic: {0}")]
148 Cycle(#[from] CycleDetected),
149 #[error("{0}")]
151 CoordinateOverflow(#[from] CoordinateOverflowError),
152 #[error(
154 "gate duration must be finite and exceed the runtime clock tolerance, and derived durations must stay finite"
155 )]
156 InvalidDuration,
157 #[error("source release times must be finite and non-negative")]
159 InvalidSourceTiming,
160 #[error("{0} identifier space exhausted")]
162 IdOverflow(&'static str),
163 #[error(
165 "quantum node {node} has {guards} guard inputs: {variants} reachable choices exceed limit {limit}"
166 )]
167 QuantumVariantLimit {
168 node: u32,
170 guards: usize,
172 variants: usize,
174 limit: usize,
176 },
177 #[error("cannot allocate {variants} quantum alternatives: {source}")]
179 QuantumVariantAllocation {
180 variants: usize,
182 #[source]
184 source: std::collections::TryReserveError,
185 },
186 #[error("measurement record {instance}:{measurement} has no VM identity")]
188 MissingRecord {
189 instance: u32,
191 measurement: u32,
193 },
194 #[error("node {node} value slot {slot} is not backed by a VM bit")]
196 MissingBit {
197 node: u32,
199 slot: u32,
201 },
202 #[error("logical output {0:?} has no complete lowered frame")]
204 MissingOutputFrame([i32; 3]),
205 #[error("logical input instance {0} has no lowered VM task")]
207 MissingInputInstance(u32),
208 #[error("logical output instance {0} has no physical owner")]
210 MissingOutputInstance(u32),
211 #[error("dynamic VM output boundary is consumed before terminal reporting")]
213 ReusedOutputBoundary,
214 #[error("RUS node {0} has an incoming quantum seam and is not source-isolated")]
216 NonSourceRus(u32),
217 #[error("RUS readiness seam {from}->{to} has no complete one-round memory template")]
219 MissingMemoryTemplate {
220 from: u32,
222 to: u32,
224 },
225 #[error("RUS node {0} has an outgoing padded seam but no decoder hold cycle")]
227 MissingDecoderHold(u32),
228 #[error("flattened quantum stream retains unsupported {0}")]
230 Unflattened(&'static str),
231 #[error("dynamic memory frontier detector has {0} padding records; expected one")]
233 InvalidFrontier(usize),
234}
235
236fn materialize_dynamic_waits(bloq: &mut Bloq) -> Result<DynamicWaits, LowerError> {
242 let mut targets = Vec::new();
243 let mut seen = BTreeSet::new();
244 for (path, level) in bloq.levels() {
245 for to in level.node_ids() {
246 let incoming = level.incoming(to).collect::<Vec<_>>();
247 let sources = incoming
248 .iter()
249 .map(|edge| edge.source)
250 .collect::<BTreeSet<_>>();
251 for edge in incoming {
252 let BloqEdge::Quantum(quantum) = edge.edge else {
253 continue;
254 };
255 let source_rus = matches!(
256 level.node(edge.source).and_then(BloqNode::try_region),
257 Some(RegionNode::RepeatUntilSuccess { .. })
258 );
259 if sources.len() < 2 && !source_rus {
260 continue;
261 }
262 if quantum.pipes.is_empty()
263 || quantum.pipes.iter().any(|pipe| pipe.padding.is_none())
264 {
265 return Err(LowerError::MissingMemoryTemplate {
266 from: edge.source.0,
267 to: edge.target.0,
268 });
269 }
270 if seen.insert((path.clone(), edge.source, edge.target)) {
271 targets.push((
272 path.clone(),
273 edge.source,
274 edge.target,
275 sources.clone(),
276 pipe_origin(quantum.pipes.iter().map(|pipe| pipe.pipe)),
277 ));
278 }
279 }
280 }
281 }
282
283 let mut waits = DynamicWaits::default();
284 for (path, from, to, sources, origin) in targets {
285 let siblings = sources
286 .iter()
287 .copied()
288 .filter(|source| *source != from)
289 .map(|node| (path.clone(), node))
290 .collect();
291 let source_region = bloq
292 .level_at(&path)
293 .and_then(|level| level.node(from))
294 .and_then(BloqNode::try_region)
295 .is_some();
296 let (wait_path, wait) = if source_region {
297 let body_path = path.child(from, BodySelector::Body);
298 let escape = bloq
299 .level_at(&body_path)
300 .expect("region body path exists")
301 .quantum_tail()?;
302 let wait = bloq.insert_memory_rounds(
303 MemoryRoundTarget::After {
304 path: body_path.clone(),
305 node: escape,
306 },
307 1,
308 )?;
309 (body_path, wait)
310 } else {
311 let wait = bloq.insert_memory_rounds(
312 MemoryRoundTarget::Edge {
313 path: path.clone(),
314 from,
315 to,
316 },
317 1,
318 )?;
319 (path.clone(), wait)
320 };
321 waits.insert(
322 (wait_path, wait),
323 DynamicWait {
324 siblings,
325 hoist_after: source_region.then_some((path, from)),
326 origin,
327 },
328 );
329 }
330 Ok(waits)
331}
332
333fn materialize_terminal_holds(
334 bloq: &mut Bloq,
335 waits: &mut DynamicWaits,
336) -> Result<Vec<TerminalHold>, LowerError> {
337 let top = LevelPath::default();
338 let frames = bloq
339 .output_frames()
340 .into_iter()
341 .map(|frame| (frame.port, [frame.x, frame.z]))
342 .collect::<FxHashMap<_, _>>();
343 let mut targets = Vec::new();
344 for output in bloq.logical_outputs() {
345 let owner = bloq.levels().find_map(|(path, level)| {
346 level.quantum_nodes().find_map(|(node, quantum)| {
347 quantum
348 .instances
349 .iter()
350 .any(|instance| instance.id == output.instance)
351 .then(|| (path.clone(), node))
352 })
353 });
354 let Some((path, owner)) = owner else {
355 return Err(LowerError::MissingOutputInstance(output.instance.0));
356 };
357 if targets.iter().any(|(existing_path, existing, _, _, _, _)| {
358 *existing_path == path && *existing == owner
359 }) {
360 continue;
361 }
362 let level = bloq
363 .level_at(&path)
364 .expect("logical output owner path remains valid");
365 let (from, edge) = level
366 .incoming(owner)
367 .find_map(|edge| match edge.edge {
368 BloqEdge::Quantum(quantum) => Some((edge.source, quantum)),
369 BloqEdge::Value { .. } | BloqEdge::Compose { .. } | BloqEdge::Order => None,
370 })
371 .ok_or(LowerError::MissingMemoryTemplate {
372 from: owner.0,
373 to: owner.0,
374 })?;
375 if edge.pipes.is_empty() || edge.pipes.iter().any(|pipe| pipe.padding.is_none()) {
376 return Err(LowerError::MissingMemoryTemplate {
377 from: from.0,
378 to: owner.0,
379 });
380 }
381 let frame_nodes = frames
382 .get(&output.port)
383 .copied()
384 .ok_or(LowerError::MissingOutputFrame(output.port.to_array()))?;
385 let bindings = bloq
386 .levels()
387 .flat_map(|(binding_path, level)| {
388 level
389 .nodes()
390 .filter_map(move |(node, data)| match data.try_classical() {
391 Some(classical)
392 if classical.operators().iter().any(|operator| {
393 operator.face == BoundaryFace::Output
394 && operator.instance == output.instance
395 }) =>
396 {
397 Some((binding_path.clone(), node))
398 }
399 _ => None,
400 })
401 })
402 .collect::<Vec<_>>();
403 targets.push((
404 path,
405 owner,
406 from,
407 frame_nodes,
408 bindings,
409 pipe_origin(edge.pipes.iter().map(|pipe| pipe.pipe)),
410 ));
411 }
412
413 let mut holds = Vec::new();
414 for (path, owner, from, frame_nodes, bindings, origin) in targets {
415 let wait = bloq.insert_memory_rounds(
416 MemoryRoundTarget::Edge {
417 path: path.clone(),
418 from,
419 to: owner,
420 },
421 1,
422 )?;
423 let wait = (path.clone(), wait);
424 waits.insert(
425 wait.clone(),
426 DynamicWait {
427 siblings: frame_nodes
428 .into_iter()
429 .map(|frame| (top.clone(), frame))
430 .collect(),
431 hoist_after: None,
432 origin,
433 },
434 );
435 holds.push(TerminalHold {
436 owner: (path, owner),
437 wait,
438 bindings,
439 });
440 }
441 Ok(holds)
442}
443
444pub fn lower(bloq: &Bloq, config: &LoweringConfig<'_>) -> Result<Program, LowerError> {
456 if !(config.gate_duration.is_finite() && config.gate_duration > crate::runtime::TIME_EPSILON) {
457 return Err(LowerError::InvalidDuration);
458 }
459 if [
460 config.source_timing.factory,
461 config.source_timing.input,
462 config.source_timing.clifford,
463 ]
464 .into_iter()
465 .any(|release| !release.is_finite() || release < 0.0)
466 {
467 return Err(LowerError::InvalidSourceTiming);
468 }
469 let mut bloq = bloq.clone();
470 let mut dynamic_waits = materialize_dynamic_waits(&mut bloq)?;
471 let terminal_holds = materialize_terminal_holds(&mut bloq, &mut dynamic_waits)?;
472 bloq.flatten()?;
473 if !crate::closure::plan_early_output_reads(&bloq)?.is_empty()
474 || crate::closure::logical_output_cuts(&bloq)?
475 .iter()
476 .any(|cuts| !cuts.is_empty())
477 {
478 return Err(LowerError::ReusedOutputBoundary);
479 }
480 crate::closure::guard_decode_observable_closure(&bloq)?;
481 let options = config
482 .noise
483 .map_or_else(InstantiationOptions::default, InstantiationOptions::noisy);
484 let plans = bloq.emission_plans(&options)?;
485 let instance_templates = bloq
486 .levels()
487 .flat_map(|(_, level)| {
488 level.quantum_nodes().flat_map(|(_, quantum)| {
489 quantum
490 .instances
491 .iter()
492 .map(|instance| (instance.id, instance.template_id))
493 })
494 })
495 .collect();
496 let layout = bloq.sorted_layout_coords()?;
497 let qubit_count = u32::try_from(layout.len()).map_err(|_| LowerError::IdOverflow("qubit"))?;
498 let coords = layout
499 .iter()
500 .enumerate()
501 .map(|(index, &coord)| (coord, index as QubitId))
502 .collect();
503 let mut cx = LowerCx {
504 bloq: &bloq,
505 config,
506 options,
507 plans: &plans,
508 coords,
509 tasks: Vec::new(),
510 task_origins: Vec::new(),
511 next_bit: 0,
512 next_record: 0,
513 next_resource: 0,
514 records: FxHashMap::default(),
515 readout_parities: FxHashMap::default(),
516 boolean_functions: BooleanFunctions::default(),
517 instance_templates,
518 dynamic_waits,
519 node_tasks: FxHashMap::default(),
520 instance_tasks: FxHashMap::default(),
521 observable_parts: FxHashMap::default(),
522 terminal_holds,
523 pending_waits: Vec::new(),
524 hoisted_waits: FxHashMap::default(),
525 };
526 let top_round_duration = cx
527 .bloq
528 .pipe_padding()
529 .map(|padding| cx.one_round_duration(*padding))
530 .collect::<Result<Vec<_>, _>>()?
531 .into_iter()
532 .reduce(f64::max)
533 .unwrap_or(config.gate_duration);
534 let entry = cx
535 .lower_level(
536 bloq.top(),
537 &LevelPath::default(),
538 None,
539 top_round_duration,
540 None,
541 )?
542 .stream;
543 cx.rewire_terminal_holds()?;
544 cx.finish_waits()?;
545 cx.assign_source_releases()?;
546 let inputs = cx.lower_inputs()?;
547 let outputs = cx.lower_outputs()?;
548 Ok(Program {
549 qubit_count,
550 bit_count: cx.next_bit,
551 record_count: cx.next_record,
552 tasks: cx.tasks,
553 task_origins: cx.task_origins,
554 entry,
555 inputs,
556 outputs,
557 decoder_latency_rounds: config.decoder_latency_rounds,
558 })
559}
560
561#[derive(Clone, Copy)]
562struct LoweredNode {
563 entry: TaskId,
564 ready: TaskId,
565 exit: TaskId,
566 bit: Option<BitId>,
567 flip: Option<BitId>,
568 raw: Option<BitId>,
569}
570
571impl LoweredNode {
572 fn output(&self, output: ObservableOutput) -> Option<BitId> {
573 match output {
574 ObservableOutput::Corrected => self.bit,
575 ObservableOutput::Flip => self.flip,
576 }
577 }
578}
579
580struct LoweredLevel {
581 stream: Stream,
582 nodes: FxHashMap<BloqNodeId, LoweredNode>,
583}
584
585struct LowerCx<'a, 'c> {
586 bloq: &'a Bloq,
587 config: &'c LoweringConfig<'c>,
588 options: InstantiationOptions<'c>,
589 plans: &'a bloq_ir::ValidatedPlans,
590 coords: FxHashMap<IVec2, QubitId>,
591 tasks: Vec<Task>,
592 task_origins: Vec<TaskOrigin>,
593 next_bit: BitId,
594 next_record: RecordId,
595 next_resource: ResourceId,
596 records: FxHashMap<InstanceMeasurement, RecordId>,
597 readout_parities: FxHashMap<Vec<InstanceMeasurement>, RecordParity>,
598 boolean_functions: BooleanFunctions,
599 instance_templates: FxHashMap<TemplateInstanceId, bloq_ir::TemplateId>,
600 dynamic_waits: DynamicWaits,
601 node_tasks: FxHashMap<ScopedNode, LoweredNode>,
602 instance_tasks: FxHashMap<TemplateInstanceId, TaskId>,
603 observable_parts: FxHashMap<TaskId, Vec<TaskId>>,
604 terminal_holds: Vec<TerminalHold>,
605 pending_waits: Vec<(TaskId, Vec<ScopedNode>)>,
606 hoisted_waits: FxHashMap<ScopedNode, (TaskId, Vec<ScopedNode>)>,
607}
608
609impl LowerCx<'_, '_> {
610 fn quantum_origin(&self, node: &BloqNode, source: Option<SourceRole>) -> TaskOrigin {
611 let quantum = node.expect_quantum();
612 let boundary_instances = self
613 .bloq
614 .logical_inputs()
615 .iter()
616 .map(|input| input.instance)
617 .chain(
618 self.bloq
619 .logical_outputs()
620 .iter()
621 .map(|output| output.instance),
622 )
623 .collect::<BTreeSet<_>>();
624 let ideal_boundary = !quantum.instances.is_empty()
625 && quantum
626 .instances
627 .iter()
628 .all(|instance| boundary_instances.contains(&instance.id));
629 match &node.provenance {
630 NodeProvenance::BlockComponent { members } => task_origin(
631 if ideal_boundary {
632 TaskFunction::IdealBoundary
633 } else if source == Some(SourceRole::PreparedY) {
634 TaskFunction::PreparedY
635 } else if !quantum.guards.is_empty() {
636 TaskFunction::AdaptiveMeasurement
637 } else {
638 TaskFunction::Cube
639 },
640 members.iter().map(|member| member.pos),
641 ),
642 NodeProvenance::TemporalPipe { pipe } => task_origin(
643 if pipe.hadamard {
644 TaskFunction::TemporalHadamard
645 } else {
646 TaskFunction::Synthetic
647 },
648 [pipe.src, pipe.dst],
649 ),
650 NodeProvenance::SpatialPortSubstitution { source, .. } => {
651 task_origin(TaskFunction::IdealBoundary, [*source])
652 }
653 NodeProvenance::MemoryPadding { pipe, .. } => pipe_origin(std::iter::once(*pipe)),
654 NodeProvenance::Generator { .. }
655 | NodeProvenance::Action { .. }
656 | NodeProvenance::BranchSelector { .. }
657 | NodeProvenance::OutputFrame { .. }
658 | NodeProvenance::None => TaskOrigin::default(),
659 }
660 }
661
662 fn align_level(
663 &mut self,
664 level: &SubGraph,
665 nodes: &FxHashMap<BloqNodeId, LoweredNode>,
666 ) -> Result<(), LowerError> {
667 let mut lanes = Vec::new();
668 for (node, quantum) in level.quantum_nodes() {
669 let Some(lowered) = nodes.get(&node) else {
670 continue;
671 };
672 let Some(stream) = instruction_stream(&self.tasks[lowered.entry as usize].instruction)
673 else {
674 continue;
675 };
676 let moments = stream
677 .moments
678 .iter()
679 .map(|moment| moment.kind.map(ir_moment_kind))
680 .collect();
681 let mut qubits = self
682 .bloq
683 .node_qubits(&level[node])?
684 .into_iter()
685 .collect::<Vec<_>>();
686 qubits.sort_by_key(|qubit| (qubit.x, qubit.y));
687 let _ = quantum;
690 lanes.push(bloq_ir::MomentLane {
691 node,
692 moments,
693 qubits,
694 });
695 }
696 if lanes.len() < 2 {
697 return Ok(());
698 }
699 let aligned = bloq_ir::align_moment_lanes(level, lanes)?;
700 let mut positions = FxHashMap::<BloqNodeId, FxHashMap<usize, (i64, usize)>>::default();
701 for layer in aligned {
702 for (slot, aligned) in layer.slots.into_iter().enumerate() {
703 for entry in aligned.entries {
704 positions
705 .entry(entry.node)
706 .or_default()
707 .insert(entry.moment, (layer.layer, slot));
708 }
709 }
710 }
711 let lane_tasks = nodes
712 .iter()
713 .filter(|(node, _)| level[**node].try_quantum().is_some())
714 .map(|(&node, lowered)| (node, lowered.entry))
715 .collect::<FxHashMap<_, _>>();
716 for (node, mut positions) in positions {
717 let task = nodes[&node].entry as usize;
718 let incoming_quantum = level
719 .incoming(node)
720 .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)));
721 let causally_started = lane_tasks.iter().any(|(&predecessor, &predecessor_task)| {
722 predecessor != node
723 && (level.has_path(predecessor, node)
724 || self.tasks[task].dependencies.contains(&predecessor_task))
725 });
726 if causally_started || !incoming_quantum {
727 let mut first = FxHashMap::<i64, usize>::default();
728 for &(layer, slot) in positions.values() {
729 first
730 .entry(layer)
731 .and_modify(|current| *current = (*current).min(slot))
732 .or_insert(slot);
733 }
734 for (layer, slot) in positions.values_mut() {
735 *slot -= first[layer];
736 }
737 }
738 let ideal = level[node]
739 .expect_quantum()
740 .instances
741 .iter()
742 .filter(|instance| instance.provenance.is_spatial_port_substitution())
743 .flat_map(|instance| {
744 self.bloq.templates()[instance.template_id]
745 .qubits()
746 .iter()
747 .filter_map(|&qubit| self.qubit(qubit, instance.offset).ok())
748 })
749 .collect::<BTreeSet<_>>();
750 let idle_qubits = self.tasks[task]
751 .qubits
752 .iter()
753 .copied()
754 .filter(|qubit| !ideal.contains(qubit))
755 .collect::<Box<[_]>>();
756 rewrite_instruction_streams(
757 &mut self.tasks[task].instruction,
758 &positions,
759 self.config.gate_duration,
760 &idle_qubits,
761 self.config.noise.map_or(0.0, |noise| noise.p_idle),
762 )?;
763 self.tasks[task].duration = instruction_duration(&self.tasks[task].instruction)?;
764 }
765 Ok(())
766 }
767
768 fn lower_outputs(&self) -> Result<Vec<LogicalOutput>, LowerError> {
769 let top = LevelPath::default();
770 let frames = self
771 .bloq
772 .output_frames()
773 .into_iter()
774 .map(|frame| (frame.port.to_array(), frame))
775 .collect::<FxHashMap<_, _>>();
776 self.bloq
777 .logical_outputs()
778 .iter()
779 .map(|output| {
780 let port = output.port.to_array();
781 let frame = frames
782 .get(&port)
783 .ok_or(LowerError::MissingOutputFrame(port))?;
784 let frame_x = self
785 .node_tasks
786 .get(&(top.clone(), frame.x))
787 .and_then(|node| node.bit)
788 .ok_or(LowerError::MissingOutputFrame(port))?;
789 let frame_z = self
790 .node_tasks
791 .get(&(top.clone(), frame.z))
792 .and_then(|node| node.bit)
793 .ok_or(LowerError::MissingOutputFrame(port))?;
794 Ok(LogicalOutput {
795 port,
796 x: self.pauli_product(&output.x, IVec2::ZERO)?,
797 z: self.pauli_product(&output.z, IVec2::ZERO)?,
798 frame_x,
799 frame_z,
800 })
801 })
802 .collect()
803 }
804
805 fn finish_waits(&mut self) -> Result<(), LowerError> {
806 for (task, siblings) in &self.pending_waits {
807 let until = siblings
808 .iter()
809 .map(|sibling| {
810 self.node_tasks
811 .get(sibling)
812 .map(|node| node.ready)
813 .ok_or(LowerError::Unflattened("dynamic join sibling"))
814 })
815 .collect::<Result<Vec<_>, _>>()?
816 .into_boxed_slice();
817 let Instruction::WaitFor {
818 until: task_until, ..
819 } = &mut self.tasks[*task as usize].instruction
820 else {
821 unreachable!("dynamic wait task retains its instruction")
822 };
823 *task_until = until;
824 }
825 Ok(())
826 }
827
828 fn rewire_terminal_holds(&mut self) -> Result<(), LowerError> {
829 for hold in &self.terminal_holds {
830 let owner = self
831 .node_tasks
832 .get(&hold.owner)
833 .ok_or(LowerError::Unflattened("terminal output owner"))?
834 .exit;
835 let wait = self
836 .node_tasks
837 .get(&hold.wait)
838 .ok_or(LowerError::Unflattened("terminal output wait"))?
839 .entry;
840 let predecessors = self.tasks[wait as usize].dependencies.clone();
841 for binding in &hold.bindings {
842 let binding = self
843 .node_tasks
844 .get(binding)
845 .ok_or(LowerError::Unflattened("terminal output binding"))?
846 .entry;
847 for task in std::iter::once(binding).chain(
848 self.observable_parts
849 .get(&binding)
850 .into_iter()
851 .flatten()
852 .copied(),
853 ) {
854 let dependencies = self.tasks[task as usize]
855 .dependencies
856 .iter()
857 .copied()
858 .flat_map(|dependency| {
859 if dependency == owner {
860 predecessors.to_vec()
861 } else {
862 vec![dependency]
863 }
864 })
865 .collect::<BTreeSet<_>>()
866 .into_iter()
867 .collect();
868 self.tasks[task as usize].dependencies = dependencies;
869 }
870 }
871 }
872 Ok(())
873 }
874
875 fn assign_source_releases(&mut self) -> Result<(), LowerError> {
876 let reference_duration = self
877 .tasks
878 .iter()
879 .filter(|task| task.source == Some(SourceRole::Clifford))
880 .map(|task| task.duration)
881 .fold(0.0, f64::max);
882 for task in &mut self.tasks {
883 task.release = match task.source {
884 Some(SourceRole::Factory) => self.config.source_timing.factory,
885 Some(SourceRole::LogicalInput) => self.config.source_timing.input,
886 Some(SourceRole::Clifford) => self.config.source_timing.clifford,
887 Some(SourceRole::PreparedY) => {
888 self.config.source_timing.clifford
889 + (reference_duration - task.duration).max(0.0)
890 }
891 None => 0.0,
892 };
893 if !task.release.is_finite() {
894 return Err(LowerError::InvalidSourceTiming);
895 }
896 }
897 Ok(())
898 }
899
900 fn lower_inputs(&self) -> Result<Vec<LogicalInput>, LowerError> {
901 self.bloq
902 .logical_inputs()
903 .iter()
904 .map(|input| {
905 let (template_id, offset) = self
906 .bloq
907 .levels()
908 .flat_map(|(_, level)| level.quantum_nodes())
909 .flat_map(|(_, quantum)| quantum.instances.iter())
910 .find(|instance| instance.id == input.instance)
911 .map(|instance| (instance.template_id, instance.offset))
912 .ok_or(LowerError::MissingInputInstance(input.instance.0))?;
913 let task = *self
914 .instance_tasks
915 .get(&input.instance)
916 .ok_or(LowerError::MissingInputInstance(input.instance.0))?;
917 let stabilizers = self.bloq.templates()[template_id]
918 .boundary_flows
919 .iter()
920 .filter(|flow| flow.start.is_empty() && !flow.end.is_empty())
921 .map(|flow| {
922 let mut product = self.pauli_product(&flow.end, offset)?;
923 product.negative = flow.sign;
924 Ok(product)
925 })
926 .collect::<Result<Vec<_>, LowerError>>()?;
927 let data_qubits = stabilizers
928 .iter()
929 .flat_map(|stabilizer| stabilizer.terms.iter().map(|(qubit, _)| *qubit))
930 .collect::<BTreeSet<_>>()
931 .into_iter()
932 .collect::<Vec<_>>()
933 .into_boxed_slice();
934 Ok(LogicalInput {
935 port: input.port.to_array(),
936 task,
937 data_qubits,
938 stabilizers: stabilizers.into_boxed_slice(),
939 x: self.pauli_product(&input.x, IVec2::ZERO)?,
940 z: self.pauli_product(&input.z, IVec2::ZERO)?,
941 })
942 })
943 .collect()
944 }
945
946 fn lower_level(
947 &mut self,
948 level: &SubGraph,
949 path: &LevelPath,
950 enclosing_rus: Option<ResourceId>,
951 decoder_round_duration: f64,
952 restart_source: Option<ValueRef>,
953 ) -> Result<LoweredLevel, LowerError> {
954 let implicit_demands = crate::closure::implicit_correction_demands(level, restart_source);
955 let mut nodes = FxHashMap::<BloqNodeId, LoweredNode>::default();
956 let mut stream = Vec::with_capacity(level.node_count());
957 let mut last_qubit = FxHashMap::<QubitId, TaskId>::default();
958 let mut discard_barrier = None;
959 let mut hoisted_here = Vec::new();
960
961 for id in level.deterministic_emit_order()? {
962 let node = &level[id];
963 let input_bits = self.input_bits(level, id, &nodes)?;
964 let mut activation = node
965 .activation
966 .map(|slot| self.required_slot(id, slot, &input_bits))
967 .transpose()?;
968 let mut dependencies = level
969 .incoming(id)
970 .filter_map(|edge| {
971 nodes.get(&edge.source).map(|producer| match edge.edge {
972 BloqEdge::Compose { .. } => producer.entry,
973 _ => producer.exit,
974 })
975 })
976 .collect::<BTreeSet<_>>();
977 if let Some(barrier) = discard_barrier {
978 dependencies.insert(barrier);
979 }
980 if self.dynamic_waits.contains_key(&(path.clone(), id))
981 && let Some(guard) = level.incoming(id).find_map(|edge| match edge.edge {
982 BloqEdge::Quantum(quantum) => quantum.guard,
983 BloqEdge::Value { .. } | BloqEdge::Compose { .. } | BloqEdge::Order => None,
984 })
985 {
986 let guard_output = guard.output;
987 let guard = nodes[&guard.node];
988 activation = Some(guard.output(guard_output).ok_or(LowerError::MissingBit {
989 node: id.0,
990 slot: guard.entry,
991 })?);
992 dependencies.insert(guard.exit);
993 }
994
995 let lowered = match &node.kind {
996 BloqNodeKind::Quantum(_) => {
997 let wait = self.dynamic_waits.get(&(path.clone(), id)).cloned();
998 let lowered = self.lower_quantum_node(
999 level,
1000 id,
1001 path,
1002 node,
1003 dependencies,
1004 activation,
1005 &input_bits,
1006 wait.is_some(),
1007 &mut stream,
1008 &mut last_qubit,
1009 )?;
1010 if let Some(wait) = wait {
1011 self.set_task_origin(lowered.entry, wait.origin.clone());
1012 if let Some(owner) = wait.hoist_after {
1013 hoisted_here.push(lowered.entry);
1014 self.hoisted_waits
1015 .insert(owner, (lowered.entry, wait.siblings));
1016 } else {
1017 self.pending_waits.push((lowered.entry, wait.siblings));
1018 }
1019 }
1020 lowered
1021 }
1022 BloqNodeKind::Classical(classical) => {
1023 let classical = classical.as_ref();
1024 if matches!(classical, ClassicalNode::Discard { .. }) {
1025 dependencies.extend(stream.iter().copied());
1026 }
1027 let observable = matches!(classical, ClassicalNode::Observable { .. });
1028 let decoded =
1029 matches!(classical, ClassicalNode::Observable { index: Some(_), .. })
1030 && crate::closure::needs_correction(level, id, &implicit_demands);
1031 let output = produces_bit(classical)
1032 .then(|| self.alloc_bit())
1033 .transpose()?;
1034 let flip = decoded.then(|| self.alloc_bit()).transpose()?;
1035 let raw_output = if decoded {
1036 Some(self.alloc_bit()?)
1037 } else {
1038 output
1039 };
1040 let mut local_tasks = Vec::new();
1041 let instruction = match classical {
1042 ClassicalNode::Compute { expr } => Instruction::Eval(
1043 self.boolean_functions.lower(expr, &input_bits, id)?,
1044 ),
1045 ClassicalNode::Observable {
1046 index,
1047 measurements,
1048 operators,
1049 } => {
1050 let mut bits = Vec::new();
1051 let mut bindings = Vec::new();
1052 for input in level.data_inputs(id) {
1053 let producer = nodes[&input.producer];
1054 if let Some(bit) = match input.output {
1055 None => producer.raw,
1056 Some(output) => producer.output(output),
1057 } {
1058 bits.push(bit);
1059 }
1060 if input.output.is_none()
1061 && task_produces_bindings(
1062 &self.tasks[producer.entry as usize].instruction,
1063 )
1064 {
1065 bindings.push(producer.entry);
1066 }
1067 }
1068 if !measurements.is_empty() {
1069 let bit = self.alloc_bit()?;
1070 let parity = self.readout_parity(measurements)?;
1071 let task = self.push_task(Task {
1072 label: task_label(path, id, "observable_measurements"),
1073 release: 0.0,
1074 source: None,
1075 dependencies: dependencies.iter().copied().collect(),
1076 activation,
1077 output: Some(bit),
1078 qubits: Box::new([]),
1079 duration: 0.0,
1080 instruction: Instruction::Accumulate(parity),
1081 })?;
1082 self.set_task_origin(
1083 task,
1084 task_origin(TaskFunction::Classical, std::iter::empty()),
1085 );
1086 stream.push(task);
1087 local_tasks.push(task);
1088 dependencies.insert(task);
1089 bits.push(bit);
1090 }
1091 if !operators.is_empty() {
1092 let boundaries = operators
1093 .iter()
1094 .map(|operator| {
1095 Ok(BoundaryBinding {
1096 input: operator.face == BoundaryFace::Input,
1097 operator: self
1098 .pauli_product(&operator.operator, IVec2::ZERO)?,
1099 })
1100 })
1101 .collect::<Result<Vec<_>, LowerError>>()?;
1102 let task = self.push_task(Task {
1103 label: task_label(path, id, "observable_boundaries"),
1104 release: 0.0,
1105 source: None,
1106 dependencies: dependencies.iter().copied().collect(),
1107 activation,
1108 output: None,
1109 qubits: Box::new([]),
1110 duration: 0.0,
1111 instruction: Instruction::Bind(boundaries.into_boxed_slice()),
1112 })?;
1113 self.set_task_origin(
1114 task,
1115 task_origin(TaskFunction::Classical, std::iter::empty()),
1116 );
1117 stream.push(task);
1118 local_tasks.push(task);
1119 dependencies.insert(task);
1120 bindings.push(task);
1121 }
1122 match index {
1123 Some(index) => Instruction::Observable {
1124 index: *index,
1125 bits: bits.into_boxed_slice(),
1126 bindings: bindings.into_boxed_slice(),
1127 },
1128 None => Instruction::ReadoutRecipe {
1129 bits: bits.into_boxed_slice(),
1130 bindings: bindings.into_boxed_slice(),
1131 },
1132 }
1133 }
1134 ClassicalNode::Discard { condition } => Instruction::Discard(
1135 self.boolean_functions.lower(condition, &input_bits, id)?,
1136 ),
1137 };
1138 let task = self.push_task(Task {
1139 label: task_label(path, id, "classical"),
1140 release: 0.0,
1141 source: None,
1142 dependencies: dependencies.into_iter().collect(),
1143 activation,
1144 output: raw_output,
1145 qubits: Box::new([]),
1146 duration: 0.0,
1147 instruction,
1148 })?;
1149 self.set_task_origin(
1150 task,
1151 task_origin(TaskFunction::Classical, std::iter::empty()),
1152 );
1153 stream.push(task);
1154 let completed = if let ClassicalNode::Observable { index, .. } = classical {
1155 let raw = raw_output.expect("observable allocates a raw bit");
1156 self.observable_parts.insert(task, local_tasks);
1157 if let (Some(index), Some(output), Some(flip)) = (index, output, flip) {
1158 let request = DecodeRequest {
1159 output: ObservableOutput::Corrected,
1160 observable: task,
1161 index: *index,
1162 raw,
1163 round_duration: decoder_round_duration,
1164 timing: if enclosing_rus.is_some() {
1165 DecodeTiming::FactoryCompletion
1166 } else {
1167 DecodeTiming::Measurement
1168 },
1169 };
1170 let mut completed = task;
1171 for (output, bit, role) in [
1172 (ObservableOutput::Corrected, output, "decode"),
1173 (ObservableOutput::Flip, flip, "flip"),
1174 ] {
1175 completed = self.push_task(Task {
1176 label: task_label(path, id, role),
1177 release: 0.0,
1178 source: None,
1179 dependencies: Box::new([task]),
1180 activation,
1181 output: Some(bit),
1182 qubits: Box::new([]),
1183 duration: 0.0,
1184 instruction: Instruction::Decode(DecodeRequest {
1185 output,
1186 ..request
1187 }),
1188 })?;
1189 self.set_task_origin(
1190 completed,
1191 task_origin(TaskFunction::Classical, std::iter::empty()),
1192 );
1193 stream.push(completed);
1194 }
1195 completed
1196 } else {
1197 task
1198 }
1199 } else {
1200 task
1201 };
1202 if matches!(classical, ClassicalNode::Discard { .. }) {
1203 discard_barrier = Some(task);
1204 }
1205 LoweredNode {
1206 entry: task,
1207 ready: completed,
1208 exit: completed,
1209 bit: output,
1210 flip,
1211 raw: if observable { raw_output } else { output },
1212 }
1213 }
1214 BloqNodeKind::Region(region) => {
1215 if level
1216 .incoming(id)
1217 .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
1218 {
1219 return Err(LowerError::NonSourceRus(id.0));
1220 }
1221 let before_bit = self.next_bit;
1222 let before_record = self.next_record;
1223 let RegionNode::RepeatUntilSuccess {
1224 body: body_graph,
1225 restart_condition: condition,
1226 restart_source,
1227 } = region;
1228 let restart_source = *restart_source;
1229 let resource = self.alloc_resource()?;
1230 let child_path = path.child(id, BodySelector::Body);
1231 let child_round_duration = self
1232 .rus_round_duration(level, id)?
1233 .unwrap_or(decoder_round_duration);
1234 let lowered_body = self.lower_level(
1235 body_graph,
1236 &child_path,
1237 Some(resource),
1238 child_round_duration,
1239 restart_source,
1240 )?;
1241 let cultivation_exits = body_graph
1242 .quantum_nodes()
1243 .filter_map(|(node, _)| {
1244 let task = lowered_body.nodes.get(&node)?.exit;
1245 matches!(
1246 self.tasks[task as usize].instruction,
1247 Instruction::Quantum(_)
1248 | Instruction::MemoryRounds { .. }
1249 | Instruction::Idle { .. }
1250 )
1251 .then_some(task)
1252 })
1253 .filter(|task| {
1254 !body_graph.quantum_nodes().any(|(node, _)| {
1255 let Some(candidate) = lowered_body.nodes.get(&node) else {
1256 return false;
1257 };
1258 matches!(
1259 self.tasks[candidate.entry as usize].instruction,
1260 Instruction::Quantum(_)
1261 | Instruction::MemoryRounds { .. }
1262 | Instruction::Idle { .. }
1263 ) && candidate.entry != *task
1264 && self.tasks[candidate.entry as usize]
1265 .dependencies
1266 .contains(task)
1267 })
1268 })
1269 .collect::<Vec<_>>()
1270 .into_boxed_slice();
1271 let physical_rus = !cultivation_exits.is_empty();
1272 if physical_rus {
1273 for &task in &lowered_body.stream.tasks {
1274 if matches!(
1275 self.tasks[task as usize].instruction,
1276 Instruction::Quantum(_)
1277 | Instruction::MemoryRounds { .. }
1278 | Instruction::Idle { .. }
1279 ) {
1280 self.task_origins[task as usize].function =
1281 TaskFunction::FactoryBody;
1282 }
1283 }
1284 }
1285 let region_origin = merge_origins(
1286 if physical_rus {
1287 TaskFunction::Factory
1288 } else {
1289 TaskFunction::Control
1290 },
1291 lowered_body
1292 .stream
1293 .tasks
1294 .iter()
1295 .map(|&task| &self.task_origins[task as usize]),
1296 );
1297 let body = lowered_body.stream;
1298 let output = Some(self.alloc_bit()?);
1299 let decoder_hold = self
1300 .hoisted_waits
1301 .get(&(path.clone(), id))
1302 .and_then(|(task, _)| match &self.tasks[*task as usize].instruction {
1303 Instruction::WaitFor { memory, .. } => Some(memory.clone()),
1304 _ => None,
1305 })
1306 .unwrap_or_default();
1307 if physical_rus && decoder_hold.is_empty() {
1308 return Err(LowerError::MissingDecoderHold(id.0));
1309 }
1310 let source_bit = restart_source
1311 .map(|source| {
1312 lowered_body
1313 .nodes
1314 .get(&source.node)
1315 .and_then(|node| node.output(source.output))
1316 .ok_or(LowerError::MissingBit {
1317 node: id.0,
1318 slot: source.node.0,
1319 })
1320 })
1321 .transpose()?;
1322 let mut restart_inputs = input_bits.clone();
1323 let mut slots = Vec::new();
1324 condition.for_each_input(&mut |slot| slots.push(slot));
1325 if let Some(source_bit) = source_bit {
1326 for slot in slots {
1327 restart_inputs.entry(slot).or_insert(source_bit);
1328 }
1329 }
1330 let owned_qubits: Box<[QubitId]> = body
1331 .tasks
1332 .iter()
1333 .flat_map(|&task| self.tasks[task as usize].qubits.iter().copied())
1334 .collect::<BTreeSet<_>>()
1335 .into_iter()
1336 .collect::<Vec<_>>()
1337 .into_boxed_slice();
1338 let qubits = owned_qubits.clone();
1339 let instruction = Instruction::Rus {
1340 body,
1341 restart: self
1342 .boolean_functions
1343 .lower(condition, &restart_inputs, id)?,
1344 owned_qubits,
1345 attempt_bits: (before_bit..self.next_bit).collect(),
1346 attempt_records: (before_record..self.next_record).collect(),
1347 resource,
1348 retry_prepare: QuantumStream::default(),
1349 decoder_hold,
1350 cultivation_exits,
1351 };
1352 if qubits.iter().any(|qubit| last_qubit.contains_key(qubit)) {
1357 return Err(LowerError::NonSourceRus(id.0));
1358 }
1359 let task = self.push_task(Task {
1360 label: task_label(path, id, region.kind_name()),
1361 release: 0.0,
1362 source: physical_rus.then_some(SourceRole::Factory),
1363 dependencies: dependencies.into_iter().collect(),
1364 activation,
1365 output,
1366 qubits: qubits.clone(),
1367 duration: 0.0,
1368 instruction,
1369 })?;
1370 self.set_task_origin(task, region_origin);
1371 stream.push(task);
1372 let mut exit = {
1373 let signal = self.push_task(Task {
1374 label: task_label(path, id, "ready"),
1375 release: 0.0,
1376 source: None,
1377 dependencies: Box::new([task]),
1378 activation: None,
1379 output: None,
1380 qubits: Box::new([]),
1381 duration: 0.0,
1382 instruction: Instruction::SignalReady { resource },
1383 })?;
1384 self.set_task_origin(
1385 signal,
1386 task_origin(TaskFunction::Control, std::iter::empty()),
1387 );
1388 stream.push(signal);
1389 signal
1390 };
1391 let ready = exit;
1392 if let Some((template, siblings)) =
1393 self.hoisted_waits.remove(&(path.clone(), id))
1394 {
1395 let mut hoisted = self.tasks[template as usize].clone();
1396 let origin = self.task_origins[template as usize].clone();
1397 hoisted.label = task_label(path, id, "accepted-join-wait");
1398 hoisted.dependencies = Box::new([exit]);
1399 let hoisted = self.push_task(hoisted)?;
1400 self.set_task_origin(hoisted, origin);
1401 stream.push(hoisted);
1402 self.pending_waits.push((hoisted, siblings));
1403 exit = hoisted;
1404 }
1405 for &qubit in qubits.iter() {
1406 last_qubit.insert(qubit, exit);
1407 }
1408 LoweredNode {
1409 entry: task,
1410 ready,
1411 exit,
1412 bit: output,
1413 flip: None,
1414 raw: output,
1415 }
1416 }
1417 };
1418 nodes.insert(id, lowered);
1419 self.node_tasks.insert((path.clone(), id), lowered);
1420 }
1421
1422 for removed in &hoisted_here {
1423 let replacements = self.tasks[*removed as usize].dependencies.clone();
1424 for &task in &stream {
1425 if task == *removed {
1426 continue;
1427 }
1428 let dependencies = &mut self.tasks[task as usize].dependencies;
1429 if dependencies.contains(removed) {
1430 let mut rewritten = dependencies
1431 .iter()
1432 .copied()
1433 .filter(|dependency| dependency != removed)
1434 .chain(replacements.iter().copied())
1435 .collect::<BTreeSet<_>>();
1436 *dependencies = std::mem::take(&mut rewritten).into_iter().collect();
1437 }
1438 }
1439 }
1440 stream.retain(|task| !hoisted_here.contains(task));
1441 self.align_level(level, &nodes)?;
1442
1443 Ok(LoweredLevel {
1444 stream: Stream {
1445 tasks: stream.into_boxed_slice(),
1446 value: level
1447 .value_output()
1448 .and_then(|value| nodes[&value.node].output(value.output)),
1449 bindings: level
1450 .boundary_outputs()
1451 .iter()
1452 .map(|node| nodes[node].entry)
1453 .collect(),
1454 },
1455 nodes,
1456 })
1457 }
1458
1459 #[expect(
1460 clippy::too_many_arguments,
1461 reason = "one node lowering carries its graph context"
1462 )]
1463 fn lower_quantum_node(
1464 &mut self,
1465 level: &SubGraph,
1466 id: BloqNodeId,
1467 path: &LevelPath,
1468 node: &BloqNode,
1469 mut dependencies: BTreeSet<TaskId>,
1470 activation: Option<BitId>,
1471 input_bits: &FxHashMap<u32, BitId>,
1472 dynamic_wait: bool,
1473 stream: &mut Vec<TaskId>,
1474 last_qubit: &mut FxHashMap<QubitId, TaskId>,
1475 ) -> Result<LoweredNode, LowerError> {
1476 self.bloq
1477 .check_detector_bundle_bindings(node.expect_quantum(), |instance| {
1478 self.instance_templates.get(&instance).copied()
1479 })?;
1480 let mut alternatives = self.quantum_alternatives(level, path, id, node, input_bits)?;
1481 let top_level_source = *path == LevelPath::default()
1482 && !level
1483 .incoming(id)
1484 .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)));
1485 let input_instances = self
1486 .bloq
1487 .logical_inputs()
1488 .iter()
1489 .map(|input| input.instance)
1490 .collect::<BTreeSet<_>>();
1491 let output_instances = self
1492 .bloq
1493 .logical_outputs()
1494 .iter()
1495 .map(|output| output.instance)
1496 .collect::<BTreeSet<_>>();
1497 let quantum = node.expect_quantum();
1498 let owns_input = quantum
1499 .instances
1500 .iter()
1501 .any(|instance| input_instances.contains(&instance.id));
1502 let prepares_y = alternatives.iter().any(|alternative| {
1503 alternative.stream.moments.iter().any(|moment| {
1504 moment.operations.iter().any(|operation| {
1505 matches!(
1506 operation,
1507 QuantumOp::Reset {
1508 basis: Pauli::Y,
1509 ..
1510 }
1511 )
1512 })
1513 })
1514 });
1515 let source = top_level_source.then_some(if owns_input {
1516 SourceRole::LogicalInput
1517 } else if prepares_y {
1518 SourceRole::PreparedY
1519 } else {
1520 SourceRole::Clifford
1521 });
1522 let mut origin = self.quantum_origin(node, source);
1523 if matches!(node.provenance, NodeProvenance::MemoryPadding { .. }) {
1524 let pipes = level
1525 .incoming(id)
1526 .chain(level.outgoing(id))
1527 .flat_map(|edge| edge.edge.pipes())
1528 .map(|pipe| pipe.pipe)
1529 .collect::<Vec<_>>();
1530 if !pipes.is_empty() {
1531 origin = pipe_origin(pipes);
1532 }
1533 }
1534 if !quantum.instances.is_empty()
1535 && quantum.instances.iter().all(|instance| {
1536 input_instances.contains(&instance.id) || output_instances.contains(&instance.id)
1537 })
1538 {
1539 for alternative in &mut alternatives {
1540 for moment in &mut alternative.stream.moments {
1541 moment.duration = 0.0;
1542 }
1543 }
1544 }
1545 let qubits = alternatives
1546 .iter()
1547 .flat_map(|alternative| alternative.stream.moments.iter())
1548 .flat_map(|moment| moment.operations.iter())
1549 .flat_map(op_qubits)
1550 .collect::<BTreeSet<_>>();
1551 for qubit in &qubits {
1552 if let Some(previous) = last_qubit.get(qubit) {
1553 dependencies.insert(*previous);
1554 }
1555 }
1556 let duration = alternatives
1557 .iter()
1558 .try_fold(0.0_f64, |longest, alternative| {
1559 stream_duration(&alternative.stream).map(|duration| longest.max(duration))
1560 })?;
1561 let rounds = match node.provenance {
1562 NodeProvenance::MemoryPadding { rounds, .. } => Some(rounds),
1563 _ => None,
1564 };
1565 let main_instruction = match (dynamic_wait, &node.provenance, alternatives.as_slice()) {
1566 (true, _, [alternative]) => Instruction::WaitFor {
1567 until: Box::new([]),
1568 memory: Box::new([self.lower_inserted_memory_cycle(node, alternative)?]),
1569 },
1570 (false, NodeProvenance::MemoryPadding { .. }, [alternative]) => {
1571 Instruction::MemoryRounds {
1572 rounds: rounds.expect("matched padding provenance"),
1573 stream: alternative.stream.clone(),
1574 detectors: alternative.detectors.clone(),
1575 }
1576 }
1577 (false, _, _) => Instruction::Quantum(QuantumTask {
1578 alternatives: alternatives.into_boxed_slice(),
1579 }),
1580 (true, _, _) => return Err(LowerError::Unflattened("guarded dynamic memory")),
1581 };
1582 let main = self.push_task(Task {
1583 label: task_label(path, id, "quantum"),
1584 release: 0.0,
1585 source,
1586 dependencies: dependencies.into_iter().collect(),
1587 activation,
1588 output: None,
1589 qubits: qubits.iter().copied().collect(),
1590 duration: if dynamic_wait { 0.0 } else { duration },
1591 instruction: main_instruction,
1592 })?;
1593 self.set_task_origin(main, origin);
1594 for instance in &quantum.instances {
1595 self.instance_tasks.insert(instance.id, main);
1596 }
1597 stream.push(main);
1598 for qubit in &qubits {
1599 last_qubit.insert(*qubit, main);
1600 }
1601 Ok(LoweredNode {
1602 entry: main,
1603 ready: main,
1604 exit: main,
1605 bit: None,
1606 flip: None,
1607 raw: None,
1608 })
1609 }
1610
1611 fn quantum_alternatives(
1612 &mut self,
1613 level: &SubGraph,
1614 path: &LevelPath,
1615 id: BloqNodeId,
1616 node: &BloqNode,
1617 inputs: &FxHashMap<u32, BitId>,
1618 ) -> Result<Vec<QuantumAlternative>, LowerError> {
1619 let quantum = node.expect_quantum();
1620 if quantum.guards.is_empty() {
1621 let plan = match self.plans.get(path, id) {
1622 Some(plan) => plan.clone(),
1623 None => node.emission_plan_with_options(self.bloq.templates(), self.options)?,
1624 };
1625 return Ok(vec![self.lower_alternative(node, &plan, Box::new([]))?]);
1626 }
1627 let slots = quantum
1628 .guards
1629 .iter()
1630 .map(|guard| guard.input)
1631 .collect::<BTreeSet<_>>()
1632 .into_iter()
1633 .collect::<Vec<_>>();
1634 let producers = level
1635 .value_inputs(id)
1636 .map(|input| (input.slot, input.producer))
1637 .collect::<FxHashMap<_, _>>();
1638 let predicates = slots
1639 .iter()
1640 .map(|slot| {
1641 producers.get(slot).copied().ok_or(
1642 NodeTemplateInstanceMergeError::MissingMembershipInput(*slot),
1643 )
1644 })
1645 .collect::<Result<Vec<_>, _>>()?;
1646 let mut assignments = bloq_ir::lowering::PredicateAnalysis::new(level).values_up_to(
1647 &predicates,
1648 self.config.max_quantum_variants.saturating_add(1),
1649 )?;
1650 let variants = assignments.len();
1651 if variants > self.config.max_quantum_variants {
1652 return Err(LowerError::QuantumVariantLimit {
1653 node: id.0,
1654 guards: slots.len(),
1655 variants,
1656 limit: self.config.max_quantum_variants,
1657 });
1658 }
1659 assignments.sort_unstable_by(|left, right| left.iter().rev().cmp(right.iter().rev()));
1661 let mut out = Vec::new();
1662 out.try_reserve_exact(variants)
1663 .map_err(|source| LowerError::QuantumVariantAllocation { variants, source })?;
1664 for assignment in assignments {
1665 let values = slots
1666 .iter()
1667 .copied()
1668 .zip(assignment)
1669 .collect::<FxHashMap<_, _>>();
1670 let selected = node.select_quantum_members(|slot| values.get(&slot).copied())?;
1671 let plan = selected.emission_plan_with_options(self.bloq.templates(), self.options)?;
1672 let when = slots
1673 .iter()
1674 .map(|slot| Ok((self.required_slot(id, *slot, inputs)?, values[slot])))
1675 .collect::<Result<Vec<_>, LowerError>>()?
1676 .into_boxed_slice();
1677 out.push(self.lower_alternative(&selected, &plan, when)?);
1678 }
1679 let all_records = out
1680 .iter()
1681 .flat_map(|alternative| alternative.selected_records.iter().copied())
1682 .collect::<BTreeSet<_>>();
1683 for alternative in &mut out {
1684 let selected = alternative
1685 .selected_records
1686 .iter()
1687 .copied()
1688 .collect::<BTreeSet<_>>();
1689 alternative.excluded_records = all_records.difference(&selected).copied().collect();
1690 }
1691 Ok(out)
1692 }
1693
1694 fn lower_alternative(
1695 &mut self,
1696 node: &BloqNode,
1697 plan: &NodeEmissionPlan,
1698 when: Box<[(BitId, bool)]>,
1699 ) -> Result<QuantumAlternative, LowerError> {
1700 let grouped = plan.grouped_measurements();
1701 let mut local_records = FxHashMap::<u32, Box<[RecordId]>>::default();
1702 for (local, sources) in grouped {
1703 let records = sources
1704 .into_iter()
1705 .map(|source| self.record(source))
1706 .collect::<Result<Vec<_>, _>>()?;
1707 local_records.insert(local, records.into_boxed_slice());
1708 }
1709 let stream = self.lower_ops(
1710 plan.circuit
1711 .body(plan.circuit.entry_body())
1712 .expect("emission plan entry body")
1713 .ops(),
1714 IVec2::ZERO,
1715 &local_records,
1716 )?;
1717 let quantum = node.expect_quantum();
1718 let templates = plan.templates(self.bloq.templates());
1719 let mut detectors = Vec::new();
1720 let mut restarts = Vec::new();
1721 for instance in &quantum.instances {
1722 let template = &templates[instance.template_id];
1723 for detector in &template.detectors {
1724 if detector.scope != TemplateDetectorScope::TopLevel {
1725 return Err(LowerError::Unflattened("repeat detector"));
1726 }
1727 detectors.push(self.template_parity(instance.id, &detector.parity)?);
1728 }
1729 restarts.extend(
1730 template
1731 .restarts
1732 .iter()
1733 .map(|restart| self.template_parity(instance.id, &restart.parity))
1734 .collect::<Result<Vec<_>, _>>()?,
1735 );
1736 }
1737 detectors.extend(
1738 self.bloq
1739 .node_detectors(quantum)?
1740 .map(|detector| self.node_parity(&detector.parity()))
1741 .collect::<Result<Vec<_>, _>>()?,
1742 );
1743 restarts.extend(
1744 quantum
1745 .restarts
1746 .iter()
1747 .map(|restart| self.node_parity(&restart.parity))
1748 .collect::<Result<Vec<_>, _>>()?,
1749 );
1750 Ok(QuantumAlternative {
1751 when,
1752 stream,
1753 selected_records: local_records
1754 .values()
1755 .flat_map(|records| records.iter().copied())
1756 .collect::<BTreeSet<_>>()
1757 .into_iter()
1758 .collect(),
1759 excluded_records: Box::new([]),
1760 detectors: detectors.into_boxed_slice(),
1761 restarts: restarts.into_boxed_slice(),
1762 })
1763 }
1764
1765 fn lower_ops(
1766 &self,
1767 ops: &[Op],
1768 offset: IVec2,
1769 records: &FxHashMap<u32, Box<[RecordId]>>,
1770 ) -> Result<QuantumStream, LowerError> {
1771 let mut moments = Vec::new();
1772 let mut current = Vec::new();
1773 let mut current_kind = None;
1774 let mut timed = false;
1775 let flush = |current: &mut Vec<QuantumOp>,
1776 current_kind: &mut Option<MomentKind>,
1777 timed: &mut bool,
1778 moments: &mut Vec<Moment>| {
1779 if !current.is_empty() {
1780 moments.push(Moment {
1781 kind: current_kind.take(),
1782 duration: if *timed {
1783 self.config.gate_duration
1784 } else {
1785 0.0
1786 },
1787 operations: std::mem::take(current).into_boxed_slice(),
1788 });
1789 } else if *timed {
1790 moments.push(Moment {
1791 kind: current_kind.take(),
1792 duration: self.config.gate_duration,
1793 operations: Box::new([]),
1794 });
1795 }
1796 *current_kind = None;
1797 *timed = false;
1798 };
1799 for op in ops {
1800 if let Some(kind) = vm_moment_kind(op) {
1801 if current_kind.is_some_and(|current| current != kind) {
1802 flush(&mut current, &mut current_kind, &mut timed, &mut moments);
1803 }
1804 current_kind = Some(kind);
1805 }
1806 match op {
1807 Op::Tick => flush(&mut current, &mut current_kind, &mut timed, &mut moments),
1808 Op::Repeat { .. } => return Err(LowerError::Unflattened("circuit repeat")),
1809 Op::Gate { gate, qubits } if gate.is_two_qubit_gate() => {
1810 timed = true;
1811 let (control_basis, target_basis) = gate
1812 .two_qubit_bases()
1813 .expect("two-qubit gate has Pauli bases");
1814 for pair in qubits.as_chunks::<2>().0 {
1815 current.push(QuantumOp::Gate2 {
1816 control_basis: pauli(control_basis),
1817 target_basis: pauli(target_basis),
1818 control: self.qubit(pair[0], offset)?,
1819 target: self.qubit(pair[1], offset)?,
1820 });
1821 }
1822 }
1823 Op::Gate { gate, qubits } => {
1824 timed = true;
1825 for &qubit in qubits {
1826 let qubit = self.qubit(qubit, offset)?;
1827 if let Some(op) = gate1(*gate, qubit) {
1828 current.push(op);
1829 }
1830 }
1831 }
1832 Op::Measure {
1833 basis,
1834 qubits,
1835 measurements,
1836 flip_probability,
1837 } => {
1838 timed = true;
1839 for (&qubit, measurement) in qubits.iter().zip(measurements) {
1840 current.push(QuantumOp::Measure {
1841 observable: PauliProduct {
1842 negative: false,
1843 terms: Box::new([(self.qubit(qubit, offset)?, pauli(*basis))]),
1844 },
1845 records: records[measurement].clone(),
1846 flip_probability: *flip_probability,
1847 });
1848 }
1849 }
1850 Op::MPP {
1851 products,
1852 measurements,
1853 } => {
1854 timed = true;
1855 for (product, measurement) in products.iter().zip(measurements) {
1856 current.push(QuantumOp::Measure {
1857 observable: self.pauli_product(product, offset)?,
1858 records: records[measurement].clone(),
1859 flip_probability: 0.0,
1860 });
1861 }
1862 }
1863 Op::ConditionalPauli(corrections) => {
1864 timed |= !corrections.is_empty();
1865 for correction in corrections {
1866 current.push(QuantumOp::ConditionalPauli {
1867 basis: pauli(correction.pauli),
1868 qubit: self.qubit(correction.target, offset)?,
1869 control: records[&correction.control][0],
1870 });
1871 }
1872 }
1873 Op::Depolarize1 {
1874 probability,
1875 qubits,
1876 } => current.push(QuantumOp::Depolarize1 {
1877 probability: *probability,
1878 qubits: qubits
1879 .iter()
1880 .map(|&qubit| self.qubit(qubit, offset))
1881 .collect::<Result<Vec<_>, _>>()?
1882 .into_boxed_slice(),
1883 }),
1884 Op::Depolarize2 {
1885 probability,
1886 qubits,
1887 } => current.push(QuantumOp::Depolarize2 {
1888 probability: *probability,
1889 pairs: qubits
1890 .as_chunks::<2>()
1891 .0
1892 .iter()
1893 .map(|pair| {
1894 Ok((self.qubit(pair[0], offset)?, self.qubit(pair[1], offset)?))
1895 })
1896 .collect::<Result<Vec<_>, LowerError>>()?
1897 .into_boxed_slice(),
1898 }),
1899 Op::PauliError {
1900 probability,
1901 pauli: basis,
1902 qubits,
1903 } => current.push(QuantumOp::PauliError {
1904 probability: *probability,
1905 basis: pauli(*basis),
1906 qubits: qubits
1907 .iter()
1908 .map(|&qubit| self.qubit(qubit, offset))
1909 .collect::<Result<Vec<_>, _>>()?
1910 .into_boxed_slice(),
1911 }),
1912 }
1913 }
1914 flush(&mut current, &mut current_kind, &mut timed, &mut moments);
1915 let mut compact: Vec<Moment> = Vec::with_capacity(moments.len());
1916 let mut leading_noise = Vec::new();
1917 for mut moment in moments {
1918 if moment.kind.is_none() && moment.duration == 0.0 {
1919 if let Some(previous) = compact.last_mut() {
1920 previous.operations = std::mem::take(&mut previous.operations)
1921 .into_vec()
1922 .into_iter()
1923 .chain(moment.operations.into_vec())
1924 .collect();
1925 } else {
1926 leading_noise.extend(moment.operations.into_vec());
1927 }
1928 continue;
1929 }
1930 if !leading_noise.is_empty() {
1931 moment.operations = std::mem::take(&mut leading_noise)
1932 .into_iter()
1933 .chain(moment.operations.into_vec())
1934 .collect();
1935 }
1936 compact.push(moment);
1937 }
1938 if !leading_noise.is_empty() {
1939 compact.push(Moment {
1940 kind: None,
1941 duration: 0.0,
1942 operations: leading_noise.into_boxed_slice(),
1943 });
1944 }
1945 moments = compact;
1946 if self.config.noise.is_none()
1947 && !ops.iter().any(|op| {
1948 matches!(
1949 op,
1950 Op::Depolarize1 { .. } | Op::Depolarize2 { .. } | Op::PauliError { .. }
1951 )
1952 })
1953 {
1954 let authored = bloq_ir::aligned_moment_segments(ops)?
1955 .into_iter()
1956 .filter_map(|segment| segment.map(|segment| segment.kind))
1957 .collect::<Vec<_>>();
1958 let lowered = moments
1959 .iter()
1960 .filter_map(|moment| moment.kind.map(ir_moment_kind))
1961 .collect::<Vec<_>>();
1962 if authored != lowered {
1963 return Err(LowerError::Unflattened("moment segmentation drift"));
1964 }
1965 }
1966 Ok(QuantumStream {
1967 moments: moments.into_boxed_slice(),
1968 })
1969 }
1970
1971 fn lower_inserted_memory_cycle(
1972 &self,
1973 node: &BloqNode,
1974 alternative: &QuantumAlternative,
1975 ) -> Result<MemoryCycle, LowerError> {
1976 let selected = alternative
1977 .selected_records
1978 .iter()
1979 .copied()
1980 .collect::<BTreeSet<_>>();
1981 let mut initializers = Vec::new();
1982 for detector in alternative.detectors.iter() {
1983 let owned = detector
1984 .records
1985 .iter()
1986 .copied()
1987 .filter(|record| selected.contains(record))
1988 .collect::<Vec<_>>();
1989 match owned.as_slice() {
1990 [] => {}
1991 [record] => initializers.push(FrontierInitializer {
1992 record: *record,
1993 parity: RecordParity {
1994 records: detector
1995 .records
1996 .iter()
1997 .copied()
1998 .filter(|other| other != record)
1999 .collect(),
2000 constant: detector.constant,
2001 },
2002 }),
2003 _ => return Err(LowerError::InvalidFrontier(owned.len())),
2004 }
2005 }
2006
2007 let mut boundary_flows = Vec::new();
2008 for instance in &node.expect_quantum().instances {
2009 let template = &self.bloq.templates()[instance.template_id];
2010 for flow in &template.boundary_flows {
2011 let parity = self.instance_parity(
2012 flow.measurements
2013 .iter()
2014 .map(|&measurement| InstanceMeasurement {
2015 instance: instance.id,
2016 measurement,
2017 }),
2018 flow.sign,
2019 )?;
2020 boundary_flows.push(BoundaryFlow {
2021 start: self.pauli_product(&flow.start, instance.offset)?,
2022 end: self.pauli_product(&flow.end, instance.offset)?,
2023 frontier_record: (parity.records.len() == 1).then_some(parity.records[0]),
2024 parity,
2025 });
2026 }
2027 }
2028 Ok(MemoryCycle {
2029 round_duration: stream_duration(&alternative.stream)?,
2030 stream: alternative.stream.clone(),
2031 detectors: alternative.detectors.clone(),
2032 boundary_flows: boundary_flows.into_boxed_slice(),
2033 initializers: initializers.into_boxed_slice(),
2034 })
2035 }
2036
2037 fn rus_round_duration(
2038 &self,
2039 level: &SubGraph,
2040 region: BloqNodeId,
2041 ) -> Result<Option<f64>, LowerError> {
2042 let mut duration = None::<f64>;
2043 for padding in level
2044 .outgoing(region)
2045 .flat_map(|edge| edge.edge.pipes())
2046 .filter_map(|seam| seam.padding)
2047 {
2048 let total = self.one_round_duration(padding)?;
2049 duration = Some(duration.map_or(total, |old| old.max(total)));
2050 }
2051 Ok(duration)
2052 }
2053
2054 fn one_round_duration(&self, padding: PipePadding) -> Result<f64, LowerError> {
2055 let template = &self.bloq.templates()[padding.one_round];
2056 let ops = template
2057 .circuit
2058 .body(template.circuit.entry_body())
2059 .expect("one-round memory template has an entry body")
2060 .ops();
2061 let mut moment_timed = false;
2062 let mut total = 0.0;
2063 for op in ops {
2064 match op {
2065 Op::Tick => {
2066 if moment_timed {
2067 total = add_duration(total, self.config.gate_duration)?;
2068 }
2069 moment_timed = false;
2070 }
2071 Op::Repeat { .. } => {
2072 return Err(LowerError::Unflattened("one-round memory template"));
2073 }
2074 Op::Gate { .. } | Op::Measure { .. } | Op::MPP { .. } => moment_timed = true,
2075 Op::ConditionalPauli(corrections) => {
2076 moment_timed |= !corrections.is_empty();
2077 }
2078 Op::Depolarize1 { .. } | Op::Depolarize2 { .. } | Op::PauliError { .. } => {}
2079 }
2080 }
2081 if moment_timed {
2082 total = add_duration(total, self.config.gate_duration)?;
2083 }
2084 Ok(total)
2085 }
2086
2087 fn input_bits(
2088 &self,
2089 level: &SubGraph,
2090 id: BloqNodeId,
2091 nodes: &FxHashMap<BloqNodeId, LoweredNode>,
2092 ) -> Result<FxHashMap<u32, BitId>, LowerError> {
2093 level
2094 .value_inputs(id)
2095 .map(|input| {
2096 nodes
2097 .get(&input.producer)
2098 .and_then(|node| node.output(input.output.expect("value port")))
2099 .map(|bit| (input.slot, bit))
2100 .ok_or(LowerError::MissingBit {
2101 node: id.0,
2102 slot: input.slot,
2103 })
2104 })
2105 .collect()
2106 }
2107
2108 fn required_slot(
2109 &self,
2110 node: BloqNodeId,
2111 slot: u32,
2112 inputs: &FxHashMap<u32, BitId>,
2113 ) -> Result<BitId, LowerError> {
2114 inputs
2115 .get(&slot)
2116 .copied()
2117 .ok_or(LowerError::MissingBit { node: node.0, slot })
2118 }
2119
2120 fn push_task(&mut self, task: Task) -> Result<TaskId, LowerError> {
2121 let id = u32::try_from(self.tasks.len()).map_err(|_| LowerError::IdOverflow("task"))?;
2122 self.tasks.push(task);
2123 self.task_origins.push(TaskOrigin::default());
2124 Ok(id)
2125 }
2126
2127 fn set_task_origin(&mut self, task: TaskId, origin: TaskOrigin) {
2128 self.task_origins[task as usize] = origin;
2129 }
2130
2131 fn alloc_bit(&mut self) -> Result<BitId, LowerError> {
2132 let id = self.next_bit;
2133 self.next_bit = id.checked_add(1).ok_or(LowerError::IdOverflow("bit"))?;
2134 Ok(id)
2135 }
2136
2137 fn alloc_resource(&mut self) -> Result<ResourceId, LowerError> {
2138 let id = self.next_resource;
2139 self.next_resource = id
2140 .checked_add(1)
2141 .ok_or(LowerError::IdOverflow("resource"))?;
2142 Ok(id)
2143 }
2144
2145 fn record(&mut self, source: InstanceMeasurement) -> Result<RecordId, LowerError> {
2146 if let Some(&record) = self.records.get(&source) {
2147 return Ok(record);
2148 }
2149 let record = self.next_record;
2150 self.next_record = record
2151 .checked_add(1)
2152 .ok_or(LowerError::IdOverflow("record"))?;
2153 self.records.insert(source, record);
2154 Ok(record)
2155 }
2156
2157 fn lookup_record(&self, source: InstanceMeasurement) -> Result<RecordId, LowerError> {
2158 self.records
2159 .get(&source)
2160 .copied()
2161 .ok_or(LowerError::MissingRecord {
2162 instance: source.instance.0,
2163 measurement: source.measurement,
2164 })
2165 }
2166
2167 fn instance_parity(
2168 &self,
2169 measurements: impl IntoIterator<Item = InstanceMeasurement>,
2170 constant: bool,
2171 ) -> Result<RecordParity, LowerError> {
2172 Ok(RecordParity {
2173 records: measurements
2174 .into_iter()
2175 .map(|measurement| self.lookup_record(measurement))
2176 .collect::<Result<Vec<_>, _>>()?
2177 .into(),
2178 constant,
2179 })
2180 }
2181
2182 fn readout_parity(
2183 &mut self,
2184 records: &[InstanceMeasurement],
2185 ) -> Result<RecordParity, LowerError> {
2186 if let Some(parity) = self.readout_parities.get(records) {
2187 return Ok(parity.clone());
2188 }
2189 let parity = self.instance_parity(records.iter().copied(), false)?;
2190 self.readout_parities
2191 .insert(records.to_vec(), parity.clone());
2192 Ok(parity)
2193 }
2194
2195 fn template_parity(
2196 &self,
2197 instance: TemplateInstanceId,
2198 parity: &DetectorParity,
2199 ) -> Result<RecordParity, LowerError> {
2200 let mut measurements = Vec::new();
2201 for term in parity.terms() {
2202 match *term {
2203 DetectorTerm::Measurement(measurement) => measurements.push(InstanceMeasurement {
2204 instance,
2205 measurement,
2206 }),
2207 DetectorTerm::LoopState(_) => {
2208 return Err(LowerError::Unflattened("loop-carried detector state"));
2209 }
2210 }
2211 }
2212 self.instance_parity(measurements, parity.sign())
2213 }
2214
2215 fn node_parity(&self, parity: &NodeDetectorParity) -> Result<RecordParity, LowerError> {
2216 let mut measurements = Vec::new();
2217 for term in parity.terms() {
2218 match *term {
2219 DetectorTerm::Measurement(measurement) => measurements.push(measurement),
2220 DetectorTerm::LoopState(_) => {
2221 return Err(LowerError::Unflattened("loop-carried detector state"));
2222 }
2223 }
2224 }
2225 self.instance_parity(measurements, parity.sign())
2226 }
2227
2228 fn qubit(&self, coordinate: IVec2, offset: IVec2) -> Result<QubitId, LowerError> {
2229 let coordinate = bloq_ir::circuit::checked_translate_coordinate(coordinate, offset)?;
2230 self.coords
2231 .get(&coordinate)
2232 .copied()
2233 .ok_or(CoordinateOverflowError { coordinate, offset }.into())
2234 }
2235
2236 fn pauli_product(&self, product: &PauliMap, offset: IVec2) -> Result<PauliProduct, LowerError> {
2237 Ok(PauliProduct {
2238 negative: false,
2239 terms: product
2240 .iter()
2241 .filter(|(_, basis)| **basis != bloq_ir::circuit::Pauli::I)
2242 .map(|(coordinate, basis)| {
2243 Ok((self.qubit(*coordinate, offset)?, pauli_map_basis(*basis)))
2244 })
2245 .collect::<Result<Vec<_>, LowerError>>()?
2246 .into_boxed_slice(),
2247 })
2248 }
2249}
2250
2251fn produces_bit(node: &ClassicalNode) -> bool {
2252 !matches!(node, ClassicalNode::Discard { .. })
2253}
2254
2255fn task_label(path: &LevelPath, node: BloqNodeId, role: &str) -> String {
2256 format!("{path:?}/n{}:{role}", node.0)
2257}
2258
2259fn add_duration(total: f64, duration: f64) -> Result<f64, LowerError> {
2260 let total = total + duration;
2261 total
2262 .is_finite()
2263 .then_some(total)
2264 .ok_or(LowerError::InvalidDuration)
2265}
2266
2267fn stream_duration(stream: &QuantumStream) -> Result<f64, LowerError> {
2268 stream
2269 .moments
2270 .iter()
2271 .try_fold(0.0, |total, moment| add_duration(total, moment.duration))
2272}
2273
2274fn instruction_duration(instruction: &Instruction) -> Result<f64, LowerError> {
2275 match instruction {
2276 Instruction::Quantum(task) => task
2277 .alternatives
2278 .iter()
2279 .map(|alternative| stream_duration(&alternative.stream))
2280 .try_fold(0.0_f64, |longest, duration| {
2281 duration.map(|duration| longest.max(duration))
2282 }),
2283 Instruction::MemoryRounds { stream, .. } => stream_duration(stream),
2284 _ => Ok(0.0),
2285 }
2286}
2287
2288fn instruction_stream(instruction: &Instruction) -> Option<&QuantumStream> {
2289 match instruction {
2290 Instruction::Quantum(task) => {
2291 let first = &task.alternatives.first()?.stream;
2292 if first
2293 .moments
2294 .iter()
2295 .any(|moment| moment.kind.is_none() && moment.duration == 0.0)
2296 {
2297 return None;
2298 }
2299 task.alternatives
2300 .iter()
2301 .all(|alternative| {
2302 alternative
2303 .stream
2304 .moments
2305 .iter()
2306 .map(|moment| moment.kind)
2307 .eq(first.moments.iter().map(|moment| moment.kind))
2308 })
2309 .then_some(first)
2310 }
2311 Instruction::MemoryRounds { stream, .. }
2312 if !stream
2313 .moments
2314 .iter()
2315 .any(|moment| moment.kind.is_none() && moment.duration == 0.0) =>
2316 {
2317 Some(stream)
2318 }
2319 Instruction::MemoryRounds { .. }
2321 | Instruction::WaitFor { .. }
2322 | Instruction::Eval(_)
2323 | Instruction::Accumulate(_)
2324 | Instruction::Bind(_)
2325 | Instruction::ReadoutRecipe { .. }
2326 | Instruction::Observable { .. }
2327 | Instruction::Decode(_)
2328 | Instruction::Discard(_)
2329 | Instruction::Rus { .. }
2330 | Instruction::SignalReady { .. }
2331 | Instruction::Idle { .. } => None,
2332 }
2333}
2334
2335fn rewrite_instruction_streams(
2336 instruction: &mut Instruction,
2337 positions: &FxHashMap<usize, (i64, usize)>,
2338 duration: f64,
2339 idle_qubits: &[QubitId],
2340 idle_probability: f64,
2341) -> Result<(), LowerError> {
2342 let idle = || Moment {
2343 kind: None,
2344 duration,
2345 operations: (idle_probability != 0.0 && !idle_qubits.is_empty())
2346 .then(|| QuantumOp::Depolarize1 {
2347 probability: idle_probability,
2348 qubits: idle_qubits.into(),
2349 })
2350 .into_iter()
2351 .collect(),
2352 };
2353 let rewrite = |stream: &mut QuantumStream| {
2354 if stream.moments.len() != positions.len() {
2355 return Err(LowerError::Unflattened("guarded moment-lane shape"));
2356 }
2357 let old = std::mem::take(&mut stream.moments).into_vec();
2358 let mut moments = Vec::new();
2359 let mut previous = None;
2360 for (index, moment) in old.into_iter().enumerate() {
2361 let &(layer, slot) = positions
2362 .get(&index)
2363 .ok_or(LowerError::Unflattened("aligned moment index"))?;
2364 let start = match previous {
2365 Some((previous_layer, previous_slot)) if layer == previous_layer => {
2366 previous_slot + 1
2367 }
2368 _ => 0,
2369 };
2370 for _ in start..slot {
2371 moments.push(idle());
2372 }
2373 moments.push(moment);
2374 previous = Some((layer, slot));
2375 }
2376 stream.moments = moments.into_boxed_slice();
2377 Ok(())
2378 };
2379 match instruction {
2380 Instruction::Quantum(task) => {
2381 for alternative in &mut task.alternatives {
2382 rewrite(&mut alternative.stream)?;
2383 }
2384 }
2385 Instruction::MemoryRounds { stream, .. } => rewrite(stream)?,
2386 _ => {}
2387 }
2388 Ok(())
2389}
2390
2391fn ir_moment_kind(kind: MomentKind) -> bloq_ir::MomentKind {
2392 match kind {
2393 MomentKind::Reset => bloq_ir::MomentKind::Reset,
2394 MomentKind::Rotation => bloq_ir::MomentKind::Rotation,
2395 MomentKind::Interaction => bloq_ir::MomentKind::Interaction,
2396 MomentKind::Measurement => bloq_ir::MomentKind::Measurement,
2397 }
2398}
2399
2400fn task_produces_bindings(instruction: &Instruction) -> bool {
2401 matches!(
2402 instruction,
2403 Instruction::Bind(_)
2404 | Instruction::ReadoutRecipe { .. }
2405 | Instruction::Observable { .. }
2406 | Instruction::Rus { .. }
2407 )
2408}
2409
2410#[derive(Default)]
2411struct BooleanFunctions {
2412 definitions: FxHashMap<ClassicalExpr, (Arc<BoolOp>, Box<[u32]>)>,
2413}
2414
2415impl BooleanFunctions {
2416 fn lower(
2417 &mut self,
2418 expr: &ClassicalExpr,
2419 inputs: &FxHashMap<u32, BitId>,
2420 node: BloqNodeId,
2421 ) -> Result<BoolOp, LowerError> {
2422 if matches!(expr, ClassicalExpr::In(_) | ClassicalExpr::Const(_)) {
2424 return lower_expr(expr, inputs, node);
2425 }
2426 if !self.definitions.contains_key(expr) {
2427 let mut slots = BTreeSet::new();
2428 expr.for_each_input(&mut |slot| {
2429 slots.insert(slot);
2430 });
2431 let slots = slots.into_iter().collect::<Box<[_]>>();
2432 let local = slots
2433 .iter()
2434 .enumerate()
2435 .map(|(index, &slot)| {
2436 Ok((
2437 slot,
2438 u32::try_from(index)
2439 .map_err(|_| LowerError::IdOverflow("function input"))?,
2440 ))
2441 })
2442 .collect::<Result<FxHashMap<_, _>, LowerError>>()?;
2443 let body = Arc::new(lower_expr(expr, &local, node)?);
2444 self.definitions.insert(expr.clone(), (body, slots));
2445 }
2446 let (body, slots) = &self.definitions[expr];
2447 let inputs = slots
2448 .iter()
2449 .map(|&slot| {
2450 inputs
2451 .get(&slot)
2452 .copied()
2453 .ok_or(LowerError::MissingBit { node: node.0, slot })
2454 })
2455 .collect::<Result<Box<[_]>, _>>()?;
2456 Ok(BoolOp::Call {
2457 body: Arc::clone(body),
2458 inputs,
2459 })
2460 }
2461}
2462
2463fn lower_expr(
2464 expr: &ClassicalExpr,
2465 inputs: &FxHashMap<u32, BitId>,
2466 node: BloqNodeId,
2467) -> Result<BoolOp, LowerError> {
2468 let input = |slot| {
2469 inputs
2470 .get(&slot)
2471 .copied()
2472 .ok_or(LowerError::MissingBit { node: node.0, slot })
2473 };
2474 Ok(match expr {
2475 ClassicalExpr::In(slot) => BoolOp::Copy(input(*slot)?),
2476 ClassicalExpr::Const(value) => BoolOp::Const(*value),
2477 ClassicalExpr::Not(inner) => BoolOp::Not(Box::new(lower_expr(inner, inputs, node)?)),
2478 ClassicalExpr::Parity {
2479 inputs: slots,
2480 constant,
2481 } => BoolOp::Parity {
2482 inputs: slots
2483 .iter()
2484 .map(|&slot| input(slot))
2485 .collect::<Result<Vec<_>, _>>()?
2486 .into_boxed_slice(),
2487 constant: *constant,
2488 },
2489 ClassicalExpr::Xor(operands) => BoolOp::Xor(
2490 operands
2491 .iter()
2492 .map(|expr| lower_expr(expr, inputs, node))
2493 .collect::<Result<Vec<_>, _>>()?
2494 .into_boxed_slice(),
2495 ),
2496 ClassicalExpr::And(operands) => BoolOp::And(
2497 operands
2498 .iter()
2499 .map(|expr| lower_expr(expr, inputs, node))
2500 .collect::<Result<Vec<_>, _>>()?
2501 .into_boxed_slice(),
2502 ),
2503 ClassicalExpr::Or(operands) => BoolOp::Or(
2504 operands
2505 .iter()
2506 .map(|expr| lower_expr(expr, inputs, node))
2507 .collect::<Result<Vec<_>, _>>()?
2508 .into_boxed_slice(),
2509 ),
2510 ClassicalExpr::Select(operands) => {
2511 let [condition, when_false, when_true] = operands.as_ref();
2512 BoolOp::Select {
2513 condition: Box::new(lower_expr(condition, inputs, node)?),
2514 when_false: Box::new(lower_expr(when_false, inputs, node)?),
2515 when_true: Box::new(lower_expr(when_true, inputs, node)?),
2516 }
2517 }
2518 })
2519}
2520
2521fn pauli(basis: PauliBasis) -> Pauli {
2522 match basis {
2523 PauliBasis::X => Pauli::X,
2524 PauliBasis::Y => Pauli::Y,
2525 PauliBasis::Z => Pauli::Z,
2526 }
2527}
2528
2529fn pauli_map_basis(basis: bloq_ir::circuit::Pauli) -> Pauli {
2530 match basis {
2531 bloq_ir::circuit::Pauli::X => Pauli::X,
2532 bloq_ir::circuit::Pauli::Y => Pauli::Y,
2533 bloq_ir::circuit::Pauli::Z => Pauli::Z,
2534 bloq_ir::circuit::Pauli::I => unreachable!("identity was filtered"),
2535 }
2536}
2537
2538fn vm_moment_kind(op: &Op) -> Option<MomentKind> {
2539 match op {
2540 Op::Gate { gate, .. } if gate.is_reset() => Some(MomentKind::Reset),
2541 Op::Gate { gate, .. } if gate.is_two_qubit_gate() => Some(MomentKind::Interaction),
2542 Op::Gate { .. } => Some(MomentKind::Rotation),
2543 Op::Measure { .. } | Op::MPP { .. } => Some(MomentKind::Measurement),
2544 Op::Tick
2545 | Op::Repeat { .. }
2546 | Op::ConditionalPauli(_)
2547 | Op::Depolarize1 { .. }
2548 | Op::Depolarize2 { .. }
2549 | Op::PauliError { .. } => None,
2550 }
2551}
2552
2553fn gate1(gate: GateType, qubit: QubitId) -> Option<QuantumOp> {
2554 use Clifford1 as C;
2555 use GateType as G;
2556 Some(match gate {
2557 G::RX => QuantumOp::Reset {
2558 basis: Pauli::X,
2559 qubit,
2560 },
2561 G::RY => QuantumOp::Reset {
2562 basis: Pauli::Y,
2563 qubit,
2564 },
2565 G::RZ => QuantumOp::Reset {
2566 basis: Pauli::Z,
2567 qubit,
2568 },
2569 G::I => return None,
2570 G::X => QuantumOp::Pauli {
2571 basis: Pauli::X,
2572 qubit,
2573 },
2574 G::Y => QuantumOp::Pauli {
2575 basis: Pauli::Y,
2576 qubit,
2577 },
2578 G::Z => QuantumOp::Pauli {
2579 basis: Pauli::Z,
2580 qubit,
2581 },
2582 G::T_YZ | G::T_YZ_DAG => QuantumOp::T {
2583 basis: Pauli::X,
2584 qubit,
2585 adjoint: gate == G::T_YZ_DAG,
2586 },
2587 G::T_XZ | G::T_XZ_DAG => QuantumOp::T {
2588 basis: Pauli::Y,
2589 qubit,
2590 adjoint: gate == G::T_XZ_DAG,
2591 },
2592 G::T | G::T_DAG => QuantumOp::T {
2593 basis: Pauli::Z,
2594 qubit,
2595 adjoint: gate == G::T_DAG,
2596 },
2597 G::H => QuantumOp::Gate1 { gate: C::H, qubit },
2598 G::H_XY => QuantumOp::Gate1 {
2599 gate: C::H_XY,
2600 qubit,
2601 },
2602 G::H_YZ => QuantumOp::Gate1 {
2603 gate: C::H_YZ,
2604 qubit,
2605 },
2606 G::H_NXY => QuantumOp::Gate1 {
2607 gate: C::H_NXY,
2608 qubit,
2609 },
2610 G::H_NXZ => QuantumOp::Gate1 {
2611 gate: C::H_NXZ,
2612 qubit,
2613 },
2614 G::H_NYZ => QuantumOp::Gate1 {
2615 gate: C::H_NYZ,
2616 qubit,
2617 },
2618 G::SQRT_X => QuantumOp::Gate1 {
2619 gate: C::SQRT_X,
2620 qubit,
2621 },
2622 G::SQRT_X_DAG => QuantumOp::Gate1 {
2623 gate: C::SQRT_X_DAG,
2624 qubit,
2625 },
2626 G::SQRT_Y => QuantumOp::Gate1 {
2627 gate: C::SQRT_Y,
2628 qubit,
2629 },
2630 G::SQRT_Y_DAG => QuantumOp::Gate1 {
2631 gate: C::SQRT_Y_DAG,
2632 qubit,
2633 },
2634 G::S => QuantumOp::Gate1 { gate: C::S, qubit },
2635 G::S_DAG => QuantumOp::Gate1 {
2636 gate: C::S_DAG,
2637 qubit,
2638 },
2639 G::C_XYZ => QuantumOp::Gate1 {
2640 gate: C::C_XYZ,
2641 qubit,
2642 },
2643 G::C_ZYX => QuantumOp::Gate1 {
2644 gate: C::C_ZYX,
2645 qubit,
2646 },
2647 G::C_NXYZ => QuantumOp::Gate1 {
2648 gate: C::C_NXYZ,
2649 qubit,
2650 },
2651 G::C_XNYZ => QuantumOp::Gate1 {
2652 gate: C::C_XNYZ,
2653 qubit,
2654 },
2655 G::C_XYNZ => QuantumOp::Gate1 {
2656 gate: C::C_XYNZ,
2657 qubit,
2658 },
2659 G::C_NZYX => QuantumOp::Gate1 {
2660 gate: C::C_NZYX,
2661 qubit,
2662 },
2663 G::C_ZNYX => QuantumOp::Gate1 {
2664 gate: C::C_ZNYX,
2665 qubit,
2666 },
2667 G::C_ZYNX => QuantumOp::Gate1 {
2668 gate: C::C_ZYNX,
2669 qubit,
2670 },
2671 G::XCX | G::XCY | G::XCZ | G::YCX | G::YCY | G::YCZ | G::CX | G::CY | G::CZ => {
2672 unreachable!("two-qubit gates are handled first")
2673 }
2674 })
2675}
2676
2677fn op_qubits(op: &QuantumOp) -> Box<dyn Iterator<Item = QubitId> + '_> {
2678 match op {
2679 QuantumOp::Gate1 { qubit, .. }
2680 | QuantumOp::Pauli { qubit, .. }
2681 | QuantumOp::T { qubit, .. }
2682 | QuantumOp::Reset { qubit, .. }
2683 | QuantumOp::ConditionalPauli { qubit, .. } => Box::new(std::iter::once(*qubit)),
2684 QuantumOp::Gate2 {
2685 control, target, ..
2686 } => Box::new([*control, *target].into_iter()),
2687 QuantumOp::Measure { observable, .. } => {
2688 Box::new(observable.terms.iter().map(|(qubit, _)| *qubit))
2689 }
2690 QuantumOp::Depolarize1 { qubits, .. } | QuantumOp::PauliError { qubits, .. } => {
2691 Box::new(qubits.iter().copied())
2692 }
2693 QuantumOp::Depolarize2 { pairs, .. } => {
2694 Box::new(pairs.iter().flat_map(|&(left, right)| [left, right]))
2695 }
2696 }
2697}
2698
2699#[cfg(test)]
2700mod tests {
2701 use std::collections::BTreeMap;
2702
2703 use super::*;
2704
2705 #[test]
2706 fn shared_boolean_functions_keep_invocation_bindings_and_sparse_slots() {
2707 let expr = ClassicalExpr::Select(Box::new([
2708 ClassicalExpr::In(9),
2709 ClassicalExpr::In(2),
2710 ClassicalExpr::parity([2, 2, 7], true),
2711 ]));
2712 let mut functions = BooleanFunctions::default();
2713 let first = functions
2714 .lower(
2715 &expr,
2716 &[(2, 20), (7, 70), (9, 90)].into_iter().collect(),
2717 BloqNodeId(0),
2718 )
2719 .unwrap();
2720 let second = functions
2721 .lower(
2722 &expr,
2723 &[(2, 21), (7, 71), (9, 91)].into_iter().collect(),
2724 BloqNodeId(1),
2725 )
2726 .unwrap();
2727 let (
2728 BoolOp::Call {
2729 body: a,
2730 inputs: first,
2731 },
2732 BoolOp::Call {
2733 body: b,
2734 inputs: second,
2735 },
2736 ) = (first, second)
2737 else {
2738 panic!("compound expressions lower to shared functions");
2739 };
2740 assert!(Arc::ptr_eq(&a, &b));
2741 assert_eq!(first.as_ref(), &[20, 70, 90]);
2742 assert_eq!(second.as_ref(), &[21, 71, 91]);
2743 assert!(matches!(
2744 functions.lower(
2745 &expr,
2746 &[(2, 20), (7, 70)].into_iter().collect(),
2747 BloqNodeId(3)
2748 ),
2749 Err(LowerError::MissingBit { node: 3, slot: 9 })
2750 ));
2751 }
2752 use bloq_compile::{CompileConfig, CompileContext};
2753 use bloq_graph::GalleryItem;
2754 use bloq_ir::circuit::CoordCircuit;
2755 use bloq_ir::lowering::BloqTemplate;
2756
2757 #[test]
2758 fn unrelated_decoder_query_does_not_retry_without_an_authored_predicate() {
2759 let mut body = SubGraph::new();
2760 let observable = body.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2761 let result = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2762 expr: ClassicalExpr::In(0),
2763 }));
2764 body.add_edge(observable, result, BloqEdge::flip(0));
2765 body.set_value_output(Some(result.into()));
2766 let mut bloq = Bloq::new();
2767 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2768 body,
2769 restart_condition: ClassicalExpr::Const(false),
2770 restart_source: None,
2771 }));
2772 bloq.validate().unwrap();
2773 let program = lower(&bloq, &LoweringConfig::default()).unwrap();
2774 for rejected_accuracy in [1.0, 0.0] {
2775 let result = crate::runtime::run(
2776 &program,
2777 crate::runtime::RuntimeConfig {
2778 decoder: crate::decoder::MockDecoderConfig {
2779 acceptance_script: vec![false],
2780 accepted_accuracy: 1.0,
2781 rejected_accuracy,
2782 ..Default::default()
2783 },
2784 ..Default::default()
2785 },
2786 )
2787 .unwrap();
2788 assert_eq!(result.artifact.retries.len(), 1);
2789 assert!(result.artifact.retries[0].accepted);
2790 assert_eq!(result.artifact.decoder_decisions.len(), 1);
2791 assert!(!result.artifact.decoder_decisions[0].decision.accepted);
2792 assert_eq!(
2793 result.artifact.decoder_decisions[0].decision.flip,
2794 rejected_accuracy == 0.0
2795 );
2796 }
2797 }
2798
2799 #[test]
2800 fn rus_retries_only_for_the_authored_flip_predicate() {
2801 let mut body = SubGraph::new();
2802 let observables: Vec<_> = (0..3)
2803 .map(|index| body.add_node(BloqNode::classical(ClassicalNode::observable(index))))
2804 .collect();
2805 let predicate = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2806 expr: ClassicalExpr::Or(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
2807 }));
2808 for (slot, &observable) in observables[..2].iter().enumerate() {
2809 body.add_edge(observable, predicate, BloqEdge::flip(slot as u32));
2810 }
2811 let mut bloq = Bloq::new();
2812 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2813 body,
2814 restart_condition: ClassicalExpr::In(0),
2815 restart_source: Some(predicate.into()),
2816 }));
2817 bloq.validate().unwrap();
2818 let program = lower(&bloq, &LoweringConfig::default()).unwrap();
2819 assert!(!program.instructions().any(|instruction| matches!(
2820 instruction,
2821 Instruction::Decode(DecodeRequest { index: 2, .. })
2822 )));
2823 for flips in [
2824 [false, false, false],
2825 [true, false, false],
2826 [false, true, false],
2827 [true, true, false],
2828 [false, false, true],
2829 ] {
2830 let result = crate::runtime::run(
2831 &program,
2832 crate::runtime::RuntimeConfig {
2833 decoder: crate::decoder::MockDecoderConfig {
2834 acceptance_script: flips
2835 .into_iter()
2836 .map(|flip| !flip)
2837 .chain([true; 3])
2838 .collect(),
2839 accepted_accuracy: 1.0,
2840 rejected_accuracy: 0.0,
2841 ..Default::default()
2842 },
2843 ..Default::default()
2844 },
2845 )
2846 .unwrap();
2847 let retry = flips[0] || flips[1];
2848 assert_eq!(
2849 result
2850 .artifact
2851 .retries
2852 .iter()
2853 .map(|attempt| attempt.accepted)
2854 .collect::<Vec<_>>(),
2855 if retry { vec![false, true] } else { vec![true] }
2856 );
2857 assert_eq!(
2858 result.artifact.decoder_decisions[..2]
2859 .iter()
2860 .map(|record| record.decision.flip)
2861 .collect::<Vec<_>>(),
2862 flips[..2]
2863 );
2864 assert_eq!(
2865 result.artifact.decoder_decisions.len(),
2866 if retry { 4 } else { 2 }
2867 );
2868 }
2869 }
2870
2871 #[test]
2872 fn observable_restart_source_requests_correction_without_replacing_body_result() {
2873 let mut body = SubGraph::new();
2874 let restart_source = body.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2875 let result = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2876 expr: ClassicalExpr::Const(true),
2877 }));
2878 body.set_value_output(Some(result.into()));
2879 let mut bloq = Bloq::new();
2880 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2881 body,
2882 restart_condition: ClassicalExpr::In(0),
2883 restart_source: Some(restart_source.into()),
2884 }));
2885 bloq.validate().unwrap();
2886 let program = lower(&bloq, &LoweringConfig::default()).unwrap();
2887 let decode = program
2888 .tasks
2889 .iter()
2890 .find(|task| matches!(task.instruction, Instruction::Decode(_)))
2891 .expect("restart source requests its corrected bit");
2892 let (body, restart) = program
2893 .instructions()
2894 .find_map(|instruction| match instruction {
2895 Instruction::Rus { body, restart, .. } => Some((body, restart)),
2896 _ => None,
2897 })
2898 .unwrap();
2899 let corrected = decode.output.unwrap();
2900 assert_eq!(restart, &BoolOp::Copy(corrected));
2901 assert_ne!(body.value, Some(corrected));
2902 assert!(body.value.is_some());
2903 }
2904
2905 #[test]
2906 fn composition_requests_no_solve_until_corrected_value_is_used() {
2907 for corrected_consumer in [false, true] {
2908 let mut bloq = Bloq::new();
2909 let value = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2910 expr: ClassicalExpr::Const(true),
2911 }));
2912 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2913 bloq.add_edge(value, observable, BloqEdge::value(0));
2914 let raw = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2915 Vec::new(),
2916 Vec::new(),
2917 )));
2918 bloq.add_edge(observable, raw, BloqEdge::compose(0));
2919 if corrected_consumer {
2920 let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2921 expr: ClassicalExpr::In(0),
2922 }));
2923 bloq.add_edge(observable, consumer, BloqEdge::value(0));
2924 }
2925 let program = lower(&bloq, &LoweringConfig::default()).unwrap();
2926 assert_eq!(
2927 program
2928 .instructions()
2929 .filter(|instruction| matches!(instruction, Instruction::Decode(_)))
2930 .count(),
2931 2 * usize::from(corrected_consumer),
2932 );
2933 let (raw_task_id, raw_task) = program
2934 .tasks
2935 .iter()
2936 .enumerate()
2937 .find(|(_, task)| {
2938 matches!(task.instruction, Instruction::Observable { index: 0, .. })
2939 })
2940 .unwrap();
2941 let projection = program
2942 .tasks
2943 .iter()
2944 .find(|task| task.label == task_label(&LevelPath::default(), raw, "classical"))
2945 .unwrap();
2946 assert_eq!(
2947 projection.instruction,
2948 Instruction::ReadoutRecipe {
2949 bits: Box::new([raw_task.output.unwrap()]),
2950 bindings: Box::new([raw_task_id as TaskId])
2951 }
2952 );
2953 }
2954 }
2955
2956 fn guarded_stage(bloq: &mut Bloq, guards: u32) -> BloqNodeId {
2957 let template = crate::test_support::one_qubit_template(bloq, GateType::H);
2958 let mut stage = BloqNode::from_members(vec![]);
2959 let quantum = stage.expect_quantum_mut();
2960 for input in 0..guards {
2961 let instance = TemplateInstanceId(input);
2962 quantum
2963 .instances
2964 .push(bloq_ir::lowering::TemplateInstance::new(
2965 instance,
2966 template,
2967 IVec2::new(input as i32, 0),
2968 ));
2969 quantum.guards.push(bloq_ir::QuantumGuard {
2970 input,
2971 instances: vec![instance],
2972 ..Default::default()
2973 });
2974 }
2975 bloq.add_node(stage)
2976 }
2977
2978 #[test]
2979 fn constant_guards_do_not_require_an_exponential_choice_table() {
2980 for limit in [1, usize::MAX] {
2981 let guards = 2 * usize::BITS;
2982 let mut bloq = Bloq::new();
2983 let quantum = guarded_stage(&mut bloq, guards);
2984 for input in 0..guards {
2985 let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2986 expr: ClassicalExpr::Const(true),
2987 }));
2988 bloq.add_edge(producer, quantum, BloqEdge::value(input));
2989 }
2990 let program = lower(
2991 &bloq,
2992 &LoweringConfig {
2993 max_quantum_variants: limit,
2994 ..Default::default()
2995 },
2996 )
2997 .unwrap();
2998 let alternatives = program
2999 .tasks
3000 .iter()
3001 .find_map(|task| match &task.instruction {
3002 Instruction::Quantum(quantum) => Some(&quantum.alternatives),
3003 _ => None,
3004 })
3005 .unwrap();
3006 assert_eq!(alternatives.len(), 1);
3007 assert_eq!(alternatives[0].when.len(), guards as usize);
3008 assert!(alternatives[0].when.iter().all(|&(_, value)| value));
3009 }
3010 }
3011
3012 #[test]
3013 fn correlated_guard_variants_keep_runtime_inputs_and_the_reachable_cap() {
3014 let mut bloq = Bloq::new();
3015 let raw = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3016 Vec::new(),
3017 Vec::new(),
3018 )));
3019 let inverse = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
3020 expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
3021 }));
3022 bloq.add_edge(raw, inverse, BloqEdge::value(0));
3023 let stage = guarded_stage(&mut bloq, 128);
3024 for input in 0..128 {
3025 bloq.add_edge(
3026 if input % 2 == 0 { raw } else { inverse },
3027 stage,
3028 BloqEdge::value(input),
3029 );
3030 }
3031 bloq.validate().unwrap();
3032 let program = lower(
3033 &bloq,
3034 &LoweringConfig {
3035 max_quantum_variants: 2,
3036 ..Default::default()
3037 },
3038 )
3039 .unwrap();
3040 let alternatives = program
3041 .tasks
3042 .iter()
3043 .find_map(|task| match &task.instruction {
3044 Instruction::Quantum(quantum) => Some(&quantum.alternatives),
3045 _ => None,
3046 })
3047 .unwrap();
3048 assert_eq!(alternatives.len(), 2);
3049 for (index, alternative) in alternatives.iter().enumerate() {
3050 assert_eq!(alternative.when.len(), 128);
3051 assert!(
3052 alternative
3053 .when
3054 .iter()
3055 .enumerate()
3056 .all(|(slot, &(_, value))| value == ((slot + index) % 2 == 0))
3057 );
3058 assert_eq!(
3059 alternative
3060 .stream
3061 .moments
3062 .iter()
3063 .map(|moment| moment.operations.len())
3064 .sum::<usize>(),
3065 64
3066 );
3067 }
3068 let artifact = crate::run(&program, crate::RuntimeConfig::default())
3069 .unwrap()
3070 .artifact;
3071 assert_eq!(artifact.stop_reason, None);
3072 assert!(artifact.events.iter().any(|event| matches!(
3073 event,
3074 crate::runtime::ExecutionEvent::QuantumAlternative { alternative: 1, .. }
3075 )));
3076 for limit in [0, 1] {
3077 let error = lower(
3078 &bloq,
3079 &LoweringConfig {
3080 max_quantum_variants: limit,
3081 ..Default::default()
3082 },
3083 )
3084 .unwrap_err();
3085 assert!(matches!(error, LowerError::QuantumVariantLimit {
3086 variants, limit: observed_limit, ..
3087 } if variants == limit + 1 && observed_limit == limit));
3088 }
3089 }
3090
3091 #[test]
3092 fn independent_guard_variants_remain_bounded() {
3093 for guards in [12, 13] {
3094 let mut bloq = Bloq::new();
3095 let quantum = guarded_stage(&mut bloq, guards);
3096 for input in 0..guards {
3097 let producer = bloq.add_node(BloqNode::classical(
3098 ClassicalNode::observable_fragment(Vec::new(), Vec::new()),
3099 ));
3100 bloq.add_edge(producer, quantum, BloqEdge::value(input));
3101 }
3102 let result = lower(&bloq, &LoweringConfig::default());
3103 if guards == 12 {
3104 let program = result.unwrap();
3105 assert!(program.tasks.iter().any(|task| matches!(
3106 &task.instruction,
3107 Instruction::Quantum(quantum)
3108 if quantum.alternatives.len() == DEFAULT_MAX_QUANTUM_VARIANTS
3109 )));
3110 } else {
3111 assert!(matches!(
3112 result,
3113 Err(LowerError::QuantumVariantLimit {
3114 guards: 13,
3115 variants: 4097,
3116 limit: DEFAULT_MAX_QUANTUM_VARIANTS,
3117 ..
3118 })
3119 ));
3120 }
3121 }
3122 }
3123
3124 #[test]
3125 fn vm_materialization_does_not_run_the_full_ir_audit() {
3126 let mut circuit = CoordCircuit::new();
3127 circuit.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
3128 let mut bloq = Bloq::new();
3129 let template = bloq.add_template(BloqTemplate::new(circuit));
3130 let quantum = bloq.add_node(crate::test_support::quantum_node(template, 0, IVec2::ZERO));
3131 let constant = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
3132 expr: ClassicalExpr::Const(true),
3133 }));
3134 bloq.add_edge(constant, quantum, BloqEdge::value(0));
3135
3136 assert!(matches!(
3137 bloq.validate(),
3138 Err(bloq_ir::BloqValidationError::UnusedQuantumValueInput {
3139 node,
3140 slot: 0
3141 }) if node == quantum
3142 ));
3143 crate::prepared::PreparedProgramCore::build(&bloq).unwrap();
3144 lower(&bloq, &LoweringConfig::default()).unwrap();
3145 }
3146
3147 #[test]
3148 fn multi_pipe_origin_keeps_every_site_and_deduplicates_time() {
3149 let origin = pipe_origin([
3150 TemporalPipeRef {
3151 src: IVec3::new(2, 3, 0),
3152 dst: IVec3::new(2, 3, 1),
3153 hadamard: false,
3154 },
3155 TemporalPipeRef {
3156 src: IVec3::new(4, 5, 2),
3157 dst: IVec3::new(4, 5, 3),
3158 hadamard: true,
3159 },
3160 ]);
3161 assert_eq!(origin.function, TaskFunction::Memory);
3162 assert_eq!(origin.sites.as_ref(), [[2, 3], [4, 5]]);
3163 assert_eq!(
3164 origin.members.as_ref(),
3165 [[2, 3, 0], [2, 3, 1], [4, 5, 2], [4, 5, 3]]
3166 );
3167 }
3168
3169 #[test]
3170 fn thth_sources_and_live_output_hold_have_causal_metadata() {
3171 let bloq = CompileContext::new(CompileConfig::new(3))
3172 .compile(&GalleryItem::THTH.build())
3173 .unwrap()
3174 .bloq;
3175 let program = lower(&bloq, &LoweringConfig::default()).unwrap();
3176 assert_eq!(program.task_origins.len(), program.tasks.len());
3177 for origin in &program.task_origins {
3178 assert!(origin.sites.windows(2).all(|pair| pair[0] < pair[1]));
3179 assert!(origin.members.windows(2).all(|pair| pair[0] < pair[1]));
3180 }
3181
3182 let sources = |role| {
3183 program
3184 .tasks
3185 .iter()
3186 .filter(|task| task.source == Some(role))
3187 .collect::<Vec<_>>()
3188 };
3189 let factories = sources(SourceRole::Factory);
3190 assert_eq!(factories.len(), 2);
3191 assert!(factories.iter().all(|task| task.release == 0.0));
3192 let input = sources(SourceRole::LogicalInput);
3193 assert_eq!(input.len(), 1);
3194 assert_eq!((input[0].release, input[0].duration), (0.0, 0.0));
3195 let clifford = sources(SourceRole::Clifford);
3196 assert_eq!(clifford.len(), 1);
3197 assert_eq!(clifford[0].release, 0.0);
3198 let prepared_y = sources(SourceRole::PreparedY);
3199 assert_eq!(prepared_y.len(), 2);
3200 assert!(prepared_y.iter().all(|task| {
3201 task.release + task.duration == clifford[0].release + clifford[0].duration
3202 }));
3203 let source_geometry = program
3204 .tasks
3205 .iter()
3206 .zip(&program.task_origins)
3207 .filter_map(|(task, origin)| task.source.map(|source| (source, origin)))
3208 .collect::<Vec<_>>();
3209 assert_eq!(
3210 source_geometry
3211 .iter()
3212 .filter(|(source, _)| *source == SourceRole::PreparedY)
3213 .flat_map(|(_, origin)| origin.sites.iter().copied())
3214 .collect::<BTreeSet<_>>(),
3215 BTreeSet::from([[3, 0], [3, 2]]),
3216 );
3217 assert!(source_geometry.iter().all(|(source, origin)| match source {
3218 SourceRole::Factory => origin.function == TaskFunction::Factory,
3219 SourceRole::LogicalInput => origin.function == TaskFunction::IdealBoundary,
3220 SourceRole::PreparedY => origin.function == TaskFunction::PreparedY,
3221 SourceRole::Clifford => {
3222 origin.function == TaskFunction::Cube
3223 && origin.sites.as_ref() == [[-1, 0], [-1, 1], [-1, 2], [0, 2]]
3224 }
3225 }));
3226 let functions = program
3227 .task_origins
3228 .iter()
3229 .map(|origin| origin.function)
3230 .collect::<Vec<_>>();
3231 for function in [
3232 TaskFunction::FactoryBody,
3233 TaskFunction::TemporalHadamard,
3234 TaskFunction::Memory,
3235 TaskFunction::AdaptiveMeasurement,
3236 TaskFunction::IdealBoundary,
3237 ] {
3238 assert!(functions.contains(&function), "missing {function:?}");
3239 }
3240 assert!(
3241 program
3242 .task_origins
3243 .iter()
3244 .filter(|origin| origin.function == TaskFunction::Memory)
3245 .all(|origin| !origin.sites.is_empty())
3246 );
3247
3248 assert_eq!(program.inputs.len(), 1);
3249 let input = &program.inputs[0];
3250 assert_eq!(
3251 program.tasks[input.task as usize].source,
3252 Some(SourceRole::LogicalInput)
3253 );
3254 assert!(!input.data_qubits.is_empty());
3255 assert!(!input.stabilizers.is_empty());
3256 assert!(!input.x.terms.is_empty() && !input.z.terms.is_empty());
3257
3258 for factory in factories {
3259 let Instruction::Rus {
3260 body,
3261 owned_qubits,
3262 retry_prepare,
3263 decoder_hold,
3264 cultivation_exits,
3265 ..
3266 } = &factory.instruction
3267 else {
3268 panic!("factory source is not RUS")
3269 };
3270 assert!(retry_prepare.moments.is_empty());
3271 let mut first_use = BTreeMap::<QubitId, bool>::new();
3272 for &task in &body.tasks {
3273 let Instruction::Quantum(quantum) = &program.tasks[task as usize].instruction
3274 else {
3275 continue;
3276 };
3277 let [alternative] = quantum.alternatives.as_ref() else {
3278 panic!("cultivation body quantum task is unguarded")
3279 };
3280 for moment in &alternative.stream.moments {
3281 for operation in &moment.operations {
3282 let (qubits, reset) = match operation {
3283 QuantumOp::Reset { qubit, .. } => (vec![*qubit], true),
3284 QuantumOp::Gate1 { qubit, .. }
3285 | QuantumOp::Pauli { qubit, .. }
3286 | QuantumOp::T { qubit, .. }
3287 | QuantumOp::ConditionalPauli { qubit, .. } => (vec![*qubit], false),
3288 QuantumOp::Gate2 {
3289 control, target, ..
3290 } => (vec![*control, *target], false),
3291 QuantumOp::Measure { observable, .. } => (
3292 observable.terms.iter().map(|(qubit, _)| *qubit).collect(),
3293 false,
3294 ),
3295 QuantumOp::Depolarize1 { .. }
3296 | QuantumOp::Depolarize2 { .. }
3297 | QuantumOp::PauliError { .. } => continue,
3298 };
3299 for qubit in qubits {
3300 first_use.entry(qubit).or_insert(reset);
3301 }
3302 }
3303 }
3304 }
3305 assert_eq!(
3306 first_use.keys().copied().collect::<BTreeSet<_>>(),
3307 owned_qubits.iter().copied().collect(),
3308 "every owned factory qubit has an authored first use"
3309 );
3310 assert!(
3311 first_use.values().all(|reset| *reset),
3312 "every owned factory qubit is reset before first use"
3313 );
3314 assert!(!cultivation_exits.is_empty());
3315 assert!(cultivation_exits.iter().all(|&task| {
3316 body.tasks.contains(&task)
3317 && matches!(
3318 program.tasks[task as usize].instruction,
3319 Instruction::Quantum(_)
3320 | Instruction::MemoryRounds { .. }
3321 | Instruction::Idle { .. }
3322 )
3323 }));
3324 assert!(!decoder_hold.is_empty());
3325 assert!(
3326 decoder_hold.iter().all(|cycle| {
3327 cycle.round_duration == 6.0 && !cycle.stream.moments.is_empty()
3328 })
3329 );
3330 assert!(body.tasks.iter().any(|&task| {
3331 matches!(
3332 program.tasks[task as usize].instruction,
3333 Instruction::Decode(DecodeRequest {
3334 timing: DecodeTiming::FactoryCompletion,
3335 ..
3336 })
3337 )
3338 }));
3339 }
3340
3341 let output = &program.outputs[0];
3342 let frame_tasks = [output.frame_x, output.frame_z].map(|bit| {
3343 program
3344 .tasks
3345 .iter()
3346 .position(|task| task.output == Some(bit))
3347 .unwrap() as TaskId
3348 });
3349 let terminal_hold =
3350 program
3351 .tasks
3352 .iter()
3353 .enumerate()
3354 .find_map(|(task, data)| match &data.instruction {
3355 Instruction::WaitFor { until, memory }
3356 if frame_tasks.iter().all(|frame| until.contains(frame)) =>
3357 {
3358 Some((task as TaskId, memory))
3359 }
3360 _ => None,
3361 });
3362 let (terminal_hold_task, terminal_hold) =
3363 terminal_hold.expect("output remains in QEC until both frames resolve");
3364 assert!(terminal_hold.iter().all(|cycle| {
3365 cycle.round_duration == 6.0
3366 && !cycle.stream.moments.is_empty()
3367 && !cycle.initializers.is_empty()
3368 }));
3369 let output_boundary = program
3370 .tasks
3371 .iter()
3372 .find(|task| task.dependencies.contains(&terminal_hold_task))
3373 .expect("terminal hold releases the output boundary");
3374 assert_eq!(output_boundary.duration, 0.0);
3375 let Instruction::Quantum(output_boundary) = &output_boundary.instruction else {
3376 panic!("output boundary is quantum")
3377 };
3378 assert!(output_boundary.alternatives.iter().all(|alternative| {
3379 alternative
3380 .stream
3381 .moments
3382 .iter()
3383 .all(|moment| moment.duration == 0.0)
3384 }));
3385 }
3386
3387 #[test]
3388 fn zero_latency_factory_keeps_the_reusable_gap_frontier() {
3389 let bloq = CompileContext::new(CompileConfig::new(3))
3390 .compile(&GalleryItem::T.build())
3391 .unwrap()
3392 .bloq;
3393 let program = lower(
3394 &bloq,
3395 &LoweringConfig {
3396 decoder_latency_rounds: 0,
3397 ..LoweringConfig::default()
3398 },
3399 )
3400 .unwrap();
3401 assert_eq!(program.decoder_latency_rounds, 0);
3402 let gaps = program
3403 .tasks
3404 .iter()
3405 .filter_map(|task| match &task.instruction {
3406 Instruction::Rus { decoder_hold, .. } => Some(decoder_hold),
3407 _ => None,
3408 })
3409 .collect::<Vec<_>>();
3410 assert_eq!(gaps.len(), 1);
3411 for gap in gaps {
3412 assert!(gap.iter().all(|cycle| {
3413 !cycle.stream.moments.is_empty()
3414 && !cycle.initializers.is_empty()
3415 && cycle
3416 .boundary_flows
3417 .iter()
3418 .any(|flow| flow.frontier_record.is_some())
3419 }));
3420 }
3421 }
3422
3423 #[test]
3424 fn postmerge_noise_does_not_add_vm_time() {
3425 let bloq = CompileContext::new(CompileConfig::new(3))
3426 .compile(&GalleryItem::T.build())
3427 .unwrap()
3428 .bloq;
3429 let ideal = lower(&bloq, &LoweringConfig::default()).unwrap();
3430 let noise = NoiseModel::uniform_depolarizing(1e-4);
3431 let noisy = lower(
3432 &bloq,
3433 &LoweringConfig {
3434 noise: Some(&noise),
3435 ..LoweringConfig::default()
3436 },
3437 )
3438 .unwrap();
3439
3440 assert_eq!(ideal.tasks.len(), noisy.tasks.len());
3441 for (ideal, noisy) in ideal.tasks.iter().zip(&noisy.tasks) {
3442 assert_eq!(ideal.label, noisy.label);
3443 assert_eq!(ideal.duration, noisy.duration, "{}", ideal.label);
3444 }
3445 }
3446
3447 #[test]
3448 fn rejects_unrepresentable_durations() {
3449 let bloq = CompileContext::new(CompileConfig::new(3))
3450 .compile(&GalleryItem::CNOT.build())
3451 .unwrap()
3452 .bloq;
3453 for gate_duration in [crate::runtime::TIME_EPSILON, f64::MAX] {
3454 let error = lower(
3455 &bloq,
3456 &LoweringConfig {
3457 gate_duration,
3458 ..LoweringConfig::default()
3459 },
3460 )
3461 .unwrap_err();
3462 assert!(matches!(error, LowerError::InvalidDuration));
3463 }
3464 }
3465 #[test]
3466 fn fragment_flip_requires_a_complete_observable_without_a_full_audit() {
3467 let mut bloq = Bloq::new();
3468 let fragment = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3469 Vec::new(),
3470 Vec::new(),
3471 )));
3472 let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
3473 expr: ClassicalExpr::In(0),
3474 }));
3475 bloq.add_edge(fragment, consumer, BloqEdge::flip(0));
3476 assert!(
3477 matches!(lower(&bloq, &LoweringConfig::default()), Err(LowerError::MissingBit { node, slot: 0 }) if node == consumer.0)
3478 );
3479 }
3480}