Skip to main content

bloq_graph/
lib.rs

1//! Block graph representation for fault-tolerant surface code quantum computing.
2//!
3//! This crate provides the core data structures for expressing quantum
4//! computations as graphs of surface code blocks connected by pipes
5//! (lattice surgery operations). It includes the `.blog` DSL parser,
6//! a block-graph gallery of standard circuits, ZX-calculus stabilizer
7//! analysis, and GLTF export for 3D visualization.
8//!
9//! # Public dependency: `glam`
10//!
11//! `glam` is a public dependency: coordinates in the public API use
12//! [`glam::IVec3`], so its types appear in this crate's signatures. The version
13//! is pinned to match the `glam` that `bevy` re-exports, keeping the editor and
14//! this crate on one vector type. A `glam` major-version bump is therefore a
15//! breaking change for this crate's API even when nothing else here changes.
16
17use std::path::PathBuf;
18use std::sync::Arc;
19
20use glam::IVec3;
21use thiserror::Error;
22
23mod action;
24mod action_dag;
25mod block;
26mod branch;
27mod cancellation;
28#[doc(hidden)]
29pub mod composition;
30mod gallery;
31#[cfg(feature = "gltf")]
32mod gltf;
33mod graph;
34mod guarded;
35mod height;
36pub use composition::ModuleJoinOptions;
37mod module_view;
38mod parser;
39mod program;
40mod summary;
41pub(crate) mod validate;
42#[cfg(feature = "verify")]
43pub mod verify;
44#[cfg(feature = "verify")]
45pub use verify::feedback::{
46    FeedbackInferenceError, FeedbackOptions, infer_feedback, infer_feedback_with,
47};
48mod zx;
49
50pub use action::{Action, BinaryOp, Expr, FeedbackTarget, MeasureTarget};
51pub use action_dag::{
52    ActionDag, ActionDependency, ActionNode, ActionOwner, MeasurementObservable,
53    ResolveDomainError, ResolveValueDomain,
54};
55pub use block::{
56    Block, BlockError, BlockKind, CubeKind, PatchRotationKind, Pipe, SelectiveKind,
57    WalkingBoundaryKind, WalkingKind, is_valid_tag,
58};
59// Deliberate facade: re-export the shared `bloq_utils` primitives so downstream
60// code can import them from `bloq_graph` rather than depending on `bloq_utils`
61// directly.
62#[doc(no_inline)]
63pub use bloq_utils::{
64    Basis, Direction, DirectionParseError, Pauli, PauliBasis, PauliError, PauliString,
65    PhasedPauliString, PortRole, RGBA, UDirection,
66};
67pub use branch::{Branch, BranchArm, BranchCut, BranchProjection, BranchRegion};
68pub use cancellation::{CancellationToken, ComputationCancelled};
69pub use gallery::{GalleryCategory, GalleryEntry, GalleryItem};
70#[cfg(feature = "gltf")]
71pub use gltf::{
72    EDITOR_AMBIENT_BRIGHTNESS, EDITOR_DIRECTIONAL_ILLUMINANCE, GltfData, GltfFaceSelector,
73    block_as_gltf_data, block_as_gltf_data_with_pipe_length, block_graph_as_gltf_data,
74    block_graph_as_gltf_data_with_popped_faces, pipe_as_gltf_data,
75    pipe_between_positions_as_gltf_data, stabilizer_as_gltf_data,
76};
77pub use graph::{AnalyzedBranchProjections, BlockGraph, BlockLayerView};
78pub use guarded::{GuardedProjection, GuardedTopology, GuardedVariable, selective_selector_name};
79pub use height::{CubeHeight, MAX_CUBE_HEIGHT_CELLS};
80pub use module_view::{ModuleView, ModuleViewModule, module_color};
81#[cfg(test)]
82pub(crate) use parser::load_graph_with_resolver_and_limits;
83#[cfg(test)]
84pub(crate) use parser::parse_inline_graph;
85pub(crate) use parser::parse_inline_graph_with_limits;
86pub use parser::{
87    ParseError, ast, lower_blog_ast_deferred, lower_blog_ast_lenient,
88    lower_blog_graph_ast_deferred, parse_actions, parse_blog_program_to_ast, parse_blog_to_ast,
89    parse_blog_to_graph, parse_blog_to_graph_with_limits,
90};
91pub use program::{
92    BitBinding, BitOutput, BitRef, InstancePort, LeafModuleCertificate, LinkedModuleDefinition,
93    MaterializedModuleSite, ModuleCertificationError, ModuleCertificationLimits, ModuleError,
94    ModuleInstance, ModuleInterface, ModuleOrientation, ModuleRotation, PortDirection,
95    QuantumConnection, QuantumPort, UnknownCertificationLimit, flatten_module_definition,
96    qualified_name,
97};
98pub use summary::{ModuleSummary, default_module_jobs, map_jobs};
99// `Stabilizer`'s support maps and `SelectiveFixings` are public `FxHashMap`s,
100// so re-export the type rather than force downstream code onto a matching
101// `rustc-hash` version to name them.
102#[doc(no_inline)]
103pub use rustc_hash::FxHashMap;
104pub use validate::{InvalidActionError, InvalidBlockGraphError};
105pub use zx::{
106    DerivedSurface, FillPortsError, GuardedLocalSurface, GuardedReadoutPlan, GuardedSurface,
107    GuardedSurfaceKind, GuardedSurfaceSpace, LocalPauliSurface, NamedReadout, NodeKind,
108    OutputCorrectionError, OutputCorrectionRow, ReadoutCoordinates, ReadoutPlan, RuntimeBasisError,
109    RuntimeStabilizerBasis, SelectiveFixing, SelectiveFixingTarget, SelectiveFixings, Stabilizer,
110    StabilizerError, StabilizerGenerator, StabilizerGenerators, StabilizerRowKind, SurfaceSupport,
111    SymbolicBasisError, SymbolicFramePair, SymbolicOutputCorrection, SymbolicSite,
112    SymbolicStabilizerBasis, ZXEdge, ZXError, ZXGraph, ZXLayerView, ZXNode, selective_fixings,
113    solve_output_correction_symbolic, solve_output_correction_symbolic_prefix,
114};
115
116/// The most commonly used items, for `use bloq_graph::prelude::*`.
117///
118/// Aggregated into `bloq::prelude` by the `bloq` facade crate.
119pub mod prelude {
120    #[doc(no_inline)]
121    pub use crate::{
122        Block, BlockGraph, BlockGraphError, BlockKind, Branch, BranchArm, CubeHeight, CubeKind,
123        GalleryItem, Pipe, parse_blog_to_graph,
124    };
125    #[doc(no_inline)]
126    pub use bloq_utils::{
127        Basis, Direction, Pauli, PauliBasis, PauliString, PortRole, RGBA, UDirection,
128    };
129}
130
131/// Errors from constructing, editing, parsing, validating, transforming, or
132/// exporting a [`BlockGraph`].
133#[derive(Error, Debug, Clone)]
134#[non_exhaustive]
135pub enum BlockGraphError {
136    /// A block already occupies the requested anchor position.
137    #[error("block already exists at position {0}")]
138    BlockExists(IVec3),
139    /// A block footprint overlaps an occupied lattice cell.
140    #[error("block footprint would overlap occupied position {0}")]
141    BlockPositionOccupied(IVec3),
142    /// Two materialized module blocks overlap at one lattice cell.
143    #[error("module block footprints conflict at cell {position}: {first}; {second}")]
144    ModuleBlockOverlap {
145        /// Conflicting lattice cell.
146        position: IVec3,
147        /// First module site occupying the cell.
148        first: Arc<MaterializedModuleSite>,
149        /// Second module site occupying the cell.
150        second: Arc<MaterializedModuleSite>,
151    },
152    /// No block occupies the requested position.
153    #[error("no block found at position {0}")]
154    BlockNotFound(IVec3),
155    /// A tag is empty or contains a forbidden character.
156    #[error(
157        "invalid tag `{tag}`: tags must be non-empty and contain no whitespace, controls, `<`, or `>`"
158    )]
159    InvalidTag {
160        /// Rejected tag text.
161        tag: String,
162    },
163    /// No pipe connects the requested endpoints.
164    #[error("no pipe found between positions {0} and {1}")]
165    PipeNotFound(IVec3, IVec3),
166    /// A pipe already connects the requested endpoints.
167    #[error("pipe already exists between positions {0} and {1}")]
168    PipeExists(IVec3, IVec3),
169    /// Adding an offset to a lattice position overflowed.
170    #[error("coordinate overflow while adding offset {offset} to position {position}")]
171    CoordinateOverflow {
172        /// Original lattice position.
173        position: IVec3,
174        /// Offset that could not be added.
175        offset: IVec3,
176    },
177    /// Normalizing the graph's time span would overflow coordinates.
178    #[error(
179        "cannot normalize z span {min_z}..={max_z} to start at zero within the i32 coordinate range"
180    )]
181    CoordinateNormalizationOverflow {
182        /// Minimum time coordinate before normalization.
183        min_z: i32,
184        /// Maximum time coordinate before normalization.
185        max_z: i32,
186    },
187    /// Rotating a position would negate an unrepresentable coordinate.
188    #[error("rotation of {position} negates unrepresentable {axis} component")]
189    CoordinateRotationOverflow {
190        /// Position that could not be rotated.
191        position: IVec3,
192        /// Rotation axis whose component overflowed.
193        axis: UDirection,
194    },
195    /// A measurement pipe has no inferable temporal basis.
196    #[error("measurement edge from {0} to {1} does not infer a time basis")]
197    MeasurementEdgeMissingTimeBasis(IVec3, IVec3),
198    /// The resulting block graph violates a graph invariant.
199    #[error("{0}")]
200    Invalid(#[from] validate::InvalidBlockGraphError),
201    /// A classical action violates an action invariant.
202    #[error("{0}")]
203    InvalidAction(#[from] validate::InvalidActionError),
204    /// Reading or writing a graph file failed.
205    #[error("I/O error at {path}: {source}")]
206    Io {
207        /// File path being accessed.
208        path: PathBuf,
209        /// Underlying I/O failure.
210        #[source]
211        source: Arc<std::io::Error>,
212    },
213    /// A block kind or property is invalid.
214    #[error("{0}")]
215    Block(#[from] block::BlockError),
216    /// Bloq graph text could not be parsed.
217    #[error("{0}")]
218    Parse(#[from] parser::ParseError),
219    /// Executable BLOG module declarations or imports are invalid.
220    #[error("{0}")]
221    ModuleSource(#[source] Arc<ModuleError>),
222    /// A flat geometry operation was requested on authored module structure.
223    #[error("{operation} requires a flat graph; call BlockGraph::flatten() explicitly first")]
224    HierarchyRequiresFlatten {
225        /// Operation that cannot preserve the authored hierarchy or interface.
226        operation: &'static str,
227    },
228    /// A BLOG module hierarchy could not be materialized as a graph.
229    #[error("{0}")]
230    ModuleMaterialization(#[source] Arc<ModuleCertificationError>),
231    /// ZX graph construction or analysis failed.
232    #[error("{0}")]
233    Zx(#[from] zx::ZXError),
234    /// Open ports could not be filled consistently.
235    #[error("{0}")]
236    FillPorts(#[from] zx::FillPortsError),
237    /// Stabilizer construction or analysis failed.
238    #[error("{0}")]
239    Stabilizer(#[from] zx::StabilizerError),
240    #[cfg(feature = "gltf")]
241    /// glTF JSON serialization failed.
242    #[error("glTF JSON serialization failed")]
243    GltfSerialize(#[source] Arc<gltf_json::Error>),
244    /// Dynamic blocks support rotation only around the time axis.
245    #[error("T and Selective blocks can only rotate around the time axis")]
246    RotationUnsupportedDynamicBlocks,
247    /// Y blocks require rotations by an even number of quarter turns.
248    #[error("block graphs containing Y blocks can only rotate in 180-degree increments")]
249    RotationRequiresHalfTurns,
250    /// Walking and patch-rotation blocks support only time-axis rotation.
251    #[error("Walking and PatchRotation blocks can only rotate around the time axis")]
252    RotationWalkingRequiresTimeAxis,
253    /// Non-default cube heights support only time-axis rotation.
254    #[error("cube blocks with a non-default height can only rotate around the time axis")]
255    RotationCubeHeightRequiresTimeAxis,
256}
257
258impl From<bloq_utils::boolean::BooleanResourceError> for StabilizerError {
259    fn from(error: bloq_utils::boolean::BooleanResourceError) -> Self {
260        Self::ResourceLimited {
261            phase: error.resource,
262            observed: error.observed,
263            limit: error.limit,
264        }
265    }
266}
267
268impl From<bloq_utils::boolean::BooleanResourceError> for BlockGraphError {
269    fn from(error: bloq_utils::boolean::BooleanResourceError) -> Self {
270        StabilizerError::from(error).into()
271    }
272}
273
274impl From<bloq_utils::boolean::BooleanResourceError> for RuntimeBasisError {
275    fn from(error: bloq_utils::boolean::BooleanResourceError) -> Self {
276        StabilizerError::from(error).into()
277    }
278}
279
280impl BlockGraphError {
281    /// Whether this action error can be repaired by adding another action.
282    pub fn is_incomplete_action_program(&self) -> bool {
283        matches!(
284            self,
285            Self::InvalidAction(
286                InvalidActionError::UndefinedVariable(_)
287                    | InvalidActionError::MissingResolveForSelective { .. }
288                    | InvalidActionError::MissingResolveForBranch(_)
289            )
290        )
291    }
292}
293
294/// Adds `offset` to `position`, rejecting an `i32` coordinate overflow.
295///
296/// # Errors
297///
298/// Returns [`BlockGraphError::CoordinateOverflow`] if any component overflows.
299pub fn checked_add_position(position: IVec3, offset: IVec3) -> Result<IVec3, BlockGraphError> {
300    let Some(x) = position.x.checked_add(offset.x) else {
301        return Err(BlockGraphError::CoordinateOverflow { position, offset });
302    };
303    let Some(y) = position.y.checked_add(offset.y) else {
304        return Err(BlockGraphError::CoordinateOverflow { position, offset });
305    };
306    let Some(z) = position.z.checked_add(offset.z) else {
307        return Err(BlockGraphError::CoordinateOverflow { position, offset });
308    };
309    Ok(IVec3::new(x, y, z))
310}