Skip to main content

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}