bloq_vm/lib.rs
1//! Execute compiled [`Bloq`](bloq_ir::Bloq) programs.
2//!
3//! [`run_bloq`] and its variants retain the noiseless physical-verification
4//! path. [`lower()`] produces the backend-independent [`Instruction`] stream
5//! consumed by [`runtime`], which adds dynamic timing, noise, retries, waits,
6//! idling, and mock streaming-decoder decisions.
7//!
8//! ```
9//! use bloq_compile::compile;
10//! use bloq_graph::GalleryItem;
11//! use bloq_vm::{LoweringConfig, lower};
12//!
13//! let ir = compile(&GalleryItem::CNOT.build(), 3)?;
14//! let options = LoweringConfig::default();
15//! let program = lower(&ir, &options)?;
16//! let result = program.run(options.runtime_config(42))?;
17//! assert!(!result.artifact.discarded);
18//! # Ok::<(), Box<dyn std::error::Error>>(())
19//! ```
20//!
21//! Physical verification executes compiled circuits, independently of
22//! `bloq_graph`'s logical QuiZX verifier. It supports non-Clifford programs;
23//! Clifford circuits can also be checked with Stim.
24//!
25//! The physical verifier adds no noise and assumes zero decoder flips. It still
26//! executes explicit noise instructions already present in a circuit. Its
27//! IR-mandated retries are bounded by [`DEFAULT_RUS_ATTEMPT_CAP`]. The dynamic
28//! runtime uses [`decoder`] instead.
29//!
30//! [`CircuitExecutor`] is the flat counterpart for a single
31//! [`CoordCircuit`](bloq_ir::circuit::CoordCircuit); [`ShotRecord`] is the
32//! per-shot measurement outcome map both paths produce.
33//!
34//! ## Where items live
35//!
36//! The crate root carries what it takes to *drive* the VM: [`lower()`] and its
37//! configuration, [`run()`] and its configuration, the [`run_bloq`] family, the
38//! [`Program`] they exchange, and the error types. The public modules carry the
39//! full *vocabularies* a consumer reads results back with, which are too large
40//! to flatten into one root namespace:
41//!
42//! - [`instruction`] — the whole lowered ISA ([`Task`](instruction::Task),
43//! [`QuantumOp`](instruction::QuantumOp), [`BoolOp`](instruction::BoolOp),
44//! [`MemoryCycle`](instruction::MemoryCycle), …).
45//! - [`runtime`] — the execution trace vocabulary
46//! ([`ExecutionEvent`](runtime::ExecutionEvent),
47//! [`MeasurementRecord`](runtime::MeasurementRecord),
48//! [`WaitReason`](runtime::WaitReason), …) behind an [`ExecutionArtifact`].
49//! - [`decoder`] — the mock streaming-decoder policy model.
50//! - [`verify`] — the physical verifier's report and expectation readouts.
51//!
52//! The names that appear in root signatures are re-exported at the root as
53//! well, so those have two paths on purpose: a signature stays nameable
54//! without reaching into a module. From [`instruction`] that is
55//! [`Program`], [`Instruction`], [`SourceRole`], [`SourceSchedule`],
56//! [`TaskOrigin`], [`TaskFunction`], and [`DEFAULT_DECODER_LATENCY_ROUNDS`];
57//! from [`runtime`] it is [`run`], [`RunResult`], [`RunMetadata`],
58//! [`RuntimeConfig`], [`RuntimeLimits`], [`RuntimeError`],
59//! [`ExecutionArtifact`], [`LogicalInputState`], and [`TaskMetadata`].
60//! Prefer the root path for those; reach into the module for the rest of the
61//! vocabulary. Everything in the private modules has exactly one path.
62//!
63//! The engine (`ticit`) vocabulary is re-exported at the root only, under
64//! `Engine`-prefixed aliases where its names would otherwise collide with this
65//! crate's own ([`EnginePauli`] against [`instruction::Pauli`],
66//! [`EngineInstruction`] against [`Instruction`]).
67mod backend;
68mod circuit;
69mod closure;
70pub mod decoder;
71mod gate_table;
72pub mod instruction;
73mod lower;
74mod prepared;
75mod program;
76pub mod runtime;
77#[cfg(test)]
78mod test_support;
79pub mod verify;
80
81// The engine's own vocabulary, re-exported so consumers of `bloq_vm` need
82// not name `ticit` directly.
83pub use backend::{
84 BatchOutcome, Gate1Q, Instruction as EngineInstruction, MeasureResult, Pauli as EnginePauli,
85 PauliBasis, PauliString as EnginePauliString, SimError, Simulator,
86};
87pub use circuit::{CircuitExecutor, ShotRecord};
88pub use instruction::{
89 DEFAULT_DECODER_LATENCY_ROUNDS, Instruction, Program, SourceRole, SourceSchedule, TaskFunction,
90 TaskOrigin,
91};
92pub use lower::{DEFAULT_MAX_QUANTUM_VARIANTS, LowerError, LoweringConfig, SourceTiming, lower};
93pub use program::{
94 DEFAULT_RUS_ATTEMPT_CAP, InputSeed, OutputLogical, PreparationContext, SavedOutput,
95 ShotContext, ShotFrame, run_bloq, run_bloq_with_captured_outputs, run_bloq_with_hook,
96 run_bloq_with_io,
97};
98pub use runtime::{
99 ExecutionArtifact, LogicalInputState, RunMetadata, RunResult, RuntimeConfig, RuntimeError,
100 RuntimeLimits, TaskMetadata, run,
101};
102
103// These two inherent impls sit at the crate root rather than beside their
104// types because each spans the lowering/runtime seam: neither `lower` nor
105// `runtime` may depend on the other, and both of these need names from both.
106
107impl LoweringConfig<'_> {
108 /// Runtime defaults consistent with this lowering configuration.
109 ///
110 /// Circuit noise is inserted while lowering. Dynamic idle noise instead
111 /// uses a per-time-unit rate, so this converts the circuit model's
112 /// per-tick `p_idle` by the configured gate duration.
113 #[must_use]
114 pub fn runtime_config(&self, seed: u64) -> RuntimeConfig {
115 RuntimeConfig {
116 seed,
117 idle_error_rate: self
118 .noise
119 .map_or(0.0, |noise| noise.p_idle / self.gate_duration),
120 ..RuntimeConfig::default()
121 }
122 }
123}
124
125impl Program {
126 /// Execute this lowered program with one runtime configuration.
127 ///
128 /// Reuse the same program with different seeds to run independent shots
129 /// without recompiling or lowering again.
130 ///
131 /// # Errors
132 ///
133 /// Returns a typed runtime, decoder, resource-limit, or backend failure.
134 pub fn run(&self, config: RuntimeConfig) -> Result<RunResult, RuntimeError> {
135 run(self, config)
136 }
137}
138
139#[cfg(test)]
140mod public_api_tests {
141 use bloq_ir::circuit::NoiseModel;
142
143 use super::*;
144
145 #[test]
146 fn runtime_config_preserves_idle_noise_per_gate_tick() {
147 let noise = NoiseModel::uniform_depolarizing(1e-3);
148 let unit = LoweringConfig {
149 noise: Some(&noise),
150 ..LoweringConfig::default()
151 };
152 let quarter = LoweringConfig {
153 gate_duration: 0.25,
154 noise: Some(&noise),
155 ..LoweringConfig::default()
156 };
157
158 assert_eq!(unit.runtime_config(7).seed, 7);
159 assert_eq!(unit.runtime_config(7).idle_error_rate, noise.p_idle);
160 assert_eq!(
161 quarter.runtime_config(7).idle_error_rate,
162 4.0 * noise.p_idle
163 );
164 assert_eq!(
165 quarter.gate_duration * quarter.runtime_config(7).idle_error_rate,
166 noise.p_idle
167 );
168 assert_eq!(
169 LoweringConfig::default().runtime_config(7).idle_error_rate,
170 0.0
171 );
172 }
173}
174
175use glam::IVec2;
176
177pub(crate) const MAX_PHYSICAL_BOUNDARY_BINDINGS: usize = 1_000_000;
178
179/// Failures on the *noiseless physical-verification* path.
180///
181/// That path is the [`run_bloq`] family, [`CircuitExecutor`], and the shared
182/// program-preparation and output-closure analysis in front of them — which
183/// [`lower()`] also runs, and reports as [`LowerError::Analysis`].
184///
185/// The dynamic causal runtime is the other execution engine in this crate and
186/// reports [`RuntimeError`] instead. Neither type is a superset of the other:
187/// this one covers *exact* execution and the static analysis in front of it,
188/// [`RuntimeError`] covers timing, retries, and mock decoding.
189#[derive(Debug, thiserror::Error)]
190#[non_exhaustive]
191pub enum ExecError {
192 /// Dense physical-verification identifiers exhausted `u32`.
193 #[error("{0} identifier space exhausted")]
194 IdOverflow(&'static str),
195 /// Expanding a complete physical readout's binding DAG exceeded the verifier's budget.
196 #[error("physical readout binding expansion exceeds {limit} work units")]
197 BoundaryBindingExpansionLimit {
198 /// Maximum recipe visits and operator occurrences for one query.
199 limit: usize,
200 },
201 /// The circuit still contains an [`Op::Repeat`](bloq_ir::circuit::Op); call
202 /// [`CoordCircuit::flatten`](bloq_ir::circuit::CoordCircuit::flatten) first.
203 #[error("circuit contains an un-flattened REPEAT; flatten before executing")]
204 UnflattenedRepeat,
205
206 /// A coordinate referenced by an op is absent from the coordinate layout.
207 #[error("operation references unknown qubit coordinate ({},{})", .0.x, .0.y)]
208 UnknownCoord(IVec2),
209
210 /// A raw [`CoordCircuit`](bloq_ir::circuit::CoordCircuit) failed the same
211 /// structural preflight used for template emission.
212 #[error("invalid circuit: {0}")]
213 InvalidCircuit(#[source] bloq_ir::lowering::NodeTemplateInstanceMergeError),
214
215 /// A required Bloq IR emission plan could not be materialized.
216 #[error("Bloq IR materialization failed: {0}")]
217 InvalidProgram(#[from] bloq_ir::BloqValidationError),
218
219 /// A shared detector use has an invalid bundle or owner binding.
220 #[error("{0}")]
221 DetectorBundle(#[from] bloq_ir::DetectorBundleError),
222
223 /// The full logical Pauli signature does not fit the platform's index width.
224 #[error("the 4^{outputs} logical Pauli signature size overflows usize")]
225 LogicalSignatureTooLarge {
226 /// Number of logical outputs requested.
227 outputs: usize,
228 },
229
230 /// Compact surface code injection requires one physical seed at the unique
231 /// intersection of the input patch's logical X and Z axes.
232 #[error("input port {port:?} is not a canonical single-seed injection patch")]
233 InvalidInjectionPatch {
234 /// Input port with invalid injection geometry.
235 port: glam::IVec3,
236 },
237
238 /// A deferred resource preparation named a port not present in the
239 /// compiled program's logical-input table.
240 #[error("resource preparation names unknown logical input port {port:?}")]
241 UnknownPreparationInput {
242 /// Unknown input port.
243 port: glam::IVec3,
244 },
245
246 /// A CCZ resource must bind three different logical input ports.
247 #[error("CCZ preparation requires three distinct logical input ports")]
248 InvalidCczPreparation,
249
250 /// A conditional Pauli / detector referenced a measurement id that has not
251 /// been produced yet in this shot.
252 #[error("reference to measurement record {0} that has not been produced")]
253 MissingRecord(u32),
254
255 /// A merged circuit record has no local-to-global identity in the prepared
256 /// program.
257 #[error("measurement record {0} has no local-to-global remap")]
258 MissingRecordRemap(u32),
259
260 /// An MPP product was empty (no Paulis).
261 #[error("MPP product is empty")]
262 EmptyMppProduct,
263
264 /// A detector referenced a loop-carried state term that the flattened
265 /// circuit did not resolve to concrete measurement records.
266 #[error("detector references an unresolved loop-carried state {0}")]
267 UnresolvedLoopState(u32),
268
269 /// Flattening Bloq IR failed.
270 #[error("Bloq IR flatten error: {0}")]
271 BloqFlatten(#[from] bloq_ir::FlattenError),
272
273 /// An instance offset moves a template qubit outside the global `i32`
274 /// coordinate lattice.
275 #[error("{0}")]
276 CoordinateOverflow(#[from] bloq_ir::CoordinateOverflowError),
277
278 /// The bloq node graph has a cycle, so no deterministic emit order exists.
279 #[error("bloq emit-order error: {0}")]
280 Cycle(#[from] bloq_ir::CycleDetected),
281
282 /// The bloq graph is structurally invalid for execution.
283 #[error("malformed bloq graph: {0}")]
284 MalformedGraph(&'static str),
285
286 /// A `Decode`'s named observable folds an `Output`-face boundary operator
287 /// whose support a quantum node scheduled AFTER the decode's cut touches: the
288 /// operator has no unresolved-but-modeled frontier, so the decoder estimate
289 /// computed at the cut would refer to a face the later node reopens/consumes
290 /// — a wrong-time boundary. Unlike the *terminal* read, which
291 /// `closure::plan_early_output_reads` moves in front of the consuming node,
292 /// a decode cut has no fixture and stays rejected up front by
293 /// `guard_decode_observable_closure`. `observable` is the `Observable` node
294 /// id, `decode`/`later` are node ids, `instance` the boundary operator's
295 /// instance.
296 #[error(
297 "decode {decode} estimates observable {observable}, which folds an Output-face \
298 boundary operator on instance {instance} whose support is touched by node {later} \
299 scheduled after the decode cut; the face is not closed at the cut"
300 )]
301 DecodeObservableNotClosed {
302 /// Decode node reading the observable.
303 decode: u32,
304 /// Observable node being read.
305 observable: u32,
306 /// Boundary-owning template instance.
307 instance: u32,
308 /// Later node touching the boundary.
309 later: u32,
310 },
311
312 /// Incompatible partial observable reads cannot be split across output cuts
313 /// without destroying their joint correlations.
314 #[error(
315 "observables {first} and {second} require anticommuting partial output reads before node {before}; their remaining factors are unavailable or gated separately"
316 )]
317 IncompatibleEarlyOutputReads {
318 /// First incompatible observable.
319 first: u32,
320 /// Second incompatible observable.
321 second: u32,
322 /// Node before which both reads are required.
323 before: u32,
324 },
325
326 /// An exported boundary's condition is unavailable before its output is consumed.
327 #[error("observable {observable} has an unavailable output-binding gate before node {before}")]
328 EarlyOutputGateUnavailable {
329 /// Observable whose gate is unavailable.
330 observable: u32,
331 /// Node requiring the early read.
332 before: u32,
333 },
334
335 /// The engine raised an error mid-shot.
336 #[error("engine error: {0}")]
337 Engine(#[from] backend::SimError),
338}