Circuit
The lazy-mode do-block circuit builder, its expansion into a flat operation list, and the eager/lazy gate-application entry points.
QuantumCircuitsMPS.apply! — Function
apply!(state::SimulationState, gate::AbstractGate, geo::AbstractGeometry)Apply a gate to the state at sites specified by geometry. Modifies state.backend.mps in-place.
Geometry dispatch resolves the target region(s); each region is executed through the uniform execute!(state, gate, region) protocol.
Normalization dispatch (Contract 3.5) is trait-based:
needs_normalization(gate) == false(unitaries): NO normalize after applyneeds_normalization(gate) == true(projective gates): normalize + truncate
apply!(state::SimulationState, gate::AbstractGate, sites::Vector{Int})Apply a gate to specific physical sites. Direct site specification. Routes through the uniform execute! protocol (so measurement-like gates such as Measure/Reset work with explicit site vectors too).
apply!(builder::CircuitBuilder, gate, geometry)Record a deterministic gate operation in the circuit builder.
Stores operation as: (type=:deterministic, gate=gate, geometry=geometry)
A Bricklayer(:odd)/Bricklayer(:even) geometry with odd L under bc=:periodic emits a one-time warning at this recording step (no valid brickwork tiling exists — see _warn_bricklayer_odd_pbc); the operation is still recorded unchanged.
Example
Circuit(L=4, bc=:periodic) do c
apply!(c, Hadamard(), SingleSite(1))
apply!(c, CNOT(), StaircaseRight(1))
endapply!(state::SimulationState, pg::ProductGate)Apply a ProductGate eagerly with its geometry omitted (recommended form): equivalent to apply!(state, pg, Sites(u)) with u the union of elements(pg.region_geometry, state.L, state.bc).
apply!(builder::CircuitBuilder, pg::ProductGate)Record a deterministic ProductGate operation with its geometry omitted (recommended form): the canonical region Sites(union) is filled in from the builder's L/bc.
apply!(builder::CircuitBuilder, pg::ProductGate, geometry)Explicit form of recording a deterministic ProductGate: geometry MUST be Sites(u) with u exactly the union of the product's elements (validated at BUILD time; any other geometry throws an ArgumentError).
QuantumCircuitsMPS.apply_with_prob! — Function
apply_with_prob!(state::SimulationState; outcomes)Eagerly execute ONE stochastic operation on state under the v0.1 unified stochastic rule — identical semantics to recording the same outcomes in a Circuit and running one step of simulate!, but applied immediately.
Each outcome is a NamedTuple with fields:
probability::Real(required)gate::AbstractGate(required)geometry::AbstractGeometry(required)
Semantics (v0.1 unified stochastic rule)
Each outcome's geometry expands to elements (elements(geo, L, bc) for broadcast geometries; set geometries are a single element); all outcomes must expand to the SAME element count K. For each element k = 1..K, exactly ONE scalar coin is drawn from the :gates_spacetime stream and a categorical selection is made among the outcomes via select_outcome_index (the engine's single source of truth); the remainder 1 - Σp selects identity (nothing applied, staircases not advanced). The winning outcome's gate is executed at its k-th element via the uniform execute! protocol.
A selected staircase advances after application and other staircase geometries in outcomes are synced to its position (sync_staircase_positions!) — exactly as in simulate!. Unlike simulate!, the eager form never resets geometry positions: the caller owns staircase/Pointer state across calls.
Call-time validation (all ArgumentError, thrown BEFORE any coin is drawn
or the state is touched — the lazy form runs the same checks at build time)
outcomesmust be non-empty- Σp must be ≤ 1 (tolerance
1e-10) - Equal-K: every outcome's geometry must expand to the same element count (the error names each outcome's geometry and K)
- Staircase/
Pointerphysics guard: if any outcome uses a staircase orPointergeometry, Σp must equal 1 (an identity remainder would silently stall the random walk — see the builder docstring for the CIPT rationale) - The removed
rng=keyword (or any other keyword) throws with a migration message: all coins come from:gates_spacetimein v0.1
Event log
When the state was constructed with log_events=true, each applied gate emits a GateApplied event. Eager calls happen outside an engine run, so step and op_idx are the documented 0 sentinels; element_idx is the real element index k.
Example
apply_with_prob!(state;
outcomes = [
(probability=p, gate=Measure(:Z), geometry=AllSites())
]
)Example (Σp < 1: remaining 0.3 = identity)
apply_with_prob!(state;
outcomes = [
(probability=0.3, gate=PauliX(), geometry=site),
(probability=0.4, gate=PauliY(), geometry=site)
]
)See Also
apply_with_prob!(c::CircuitBuilder; outcomes): lazy form (records into aCircuitforsimulate!)apply!(state, gate, geometry): deterministic, unconditional application
apply_with_prob!(builder::CircuitBuilder; outcomes)Record a stochastic operation in the circuit builder (v0.1 unified rule).
Stores operation as: (type=:stochastic, rng=:gates_spacetime, outcomes=collect(outcomes))
All stochastic coins are drawn from the :gates_spacetime stream — the pre-v0.1 rng= keyword was REMOVED (passing it throws an ArgumentError with a migration message).
Arguments
outcomes: Vector of NamedTuples with keys(:probability, :gate, :geometry)
Semantics (v0.1 unified stochastic rule)
Each outcome's geometry expands to elements (elements(geo, L, bc)); all outcomes must expand to the SAME element count K. Per element k = 1..K, the engine draws ONE :gates_spacetime coin and makes a categorical selection among the outcomes; the remainder 1 - Σp selects identity (nothing applied).
Build-time validations (all ArgumentError)
outcomesmust be non-empty- Σp must be ≤ 1 (tolerance
1e-10) - Equal-K: every outcome's geometry must expand to the same element count (the error names each outcome's geometry and K)
- Staircase/Pointer physics guard: if any outcome uses a staircase or
Pointergeometry, Σp must equal 1. The CIPT random walk advances via the selected staircase every step; an identity remainder (Σp < 1) would silently stall the walk. - The removed
rng=keyword (or any other keyword) throws with a migration message
Example
Circuit(L=4, bc=:periodic) do c
apply_with_prob!(c; outcomes=[
(probability=0.3, gate=PauliX(), geometry=SingleSite(1)),
(probability=0.2, gate=PauliZ(), geometry=SingleSite(1))
])
endQuantumCircuitsMPS.Circuit — Type
Circuit(; L::Int, bc::Symbol, operations=NamedTuple[])Lazy representation of a quantum circuit as a sequence of symbolic operations.
Circuits are currently one-dimensional: L sites on a chain with :open/:periodic boundary conditions. Higher-dimensional (2D+) circuit geometries are a planned future direction (see ROADMAP.md).
A Circuit does NOT execute immediately - it stores operations symbolically and expands them to concrete gate applications only when passed to simulate!.
A Circuit always represents ONE time step. Repeat execution is controlled by simulate!(circuit, state; n_steps=...).
Fields
L::Int: Number of physical sites in the systembc::Symbol: Boundary conditions (:periodicor:open)operations::Vector{NamedTuple}: Internal symbolic operation listparams::Dict{Symbol,Any}: User-defined parameters (default: empty Dict)
Operation Representation
Operations are stored as NamedTuples with different formats:
Deterministic gates:
(type = :deterministic, gate = gate, geometry = geometry)Stochastic outcomes (applywithprob!):
(type = :stochastic, rng = :gates_spacetime, outcomes = [(probability=p, gate=g, geometry=geo), ...])Construction
Users construct circuits via the do-block API (see CircuitBuilder):
circuit = Circuit(L=10, bc=:periodic) do c
apply!(c, Hadamard(), SingleSite(1))
apply!(c, CNOT(), StaircaseRight(1), steps=5)
endExecution
Pass the circuit to simulate! to execute:
state = SimulationState(...)
simulate!(circuit, state; n_steps=50) # Runs the circuit 50 timesSee Also
expand_circuit: Expands symbolic operations to concrete site listssimulate!: Executes a circuit on a simulation state
QuantumCircuitsMPS.expand_circuit — Function
expand_circuit(circuit::Circuit; n_steps::Int=1, seed::Int=0) -> Vector{Vector{ExpandedOp}}Expand a symbolic circuit to a flat list of concrete gate operations per timestep.
This is the backward-compatible flat version. For visualization, prefer expand_circuit_grouped which preserves operation group boundaries.
Stochastic operations are expanded with the engine's shared selection function select_outcome_index (v0.1 unified rule) — see expand_circuit_grouped for details. Record markers appear as gate-less pseudo-ops (see is_record_mark).
Arguments
circuit::Circuit: The circuit to expand (one time step)n_steps::Int: Number of times to repeat the circuit step (default: 1)seed::Int: RNG seed for stochastic branch selection (default: 0)
Returns
Vector{Vector{ExpandedOp}}: Outer vector has lengthn_steps, inner vectors contain all operations for that timestep (flattened across groups). Inner vectors may be empty if the identity remainder is selected for all stochastic elements.
See Also
expand_circuit_grouped: Grouped version for visualization
QuantumCircuitsMPS.expand_circuit_grouped — Function
expand_circuit_grouped(circuit::Circuit; n_steps::Int=1, seed::Int=0) -> Vector{Vector{Vector{ExpandedOp}}}Expand a symbolic circuit to concrete gate operations, preserving operation groups.
Each apply! / apply_with_prob! / record! call in the circuit definition becomes (at most) one group. This grouping is essential for visualization: gates within the same group (e.g., all pairs in a Bricklayer) can be rendered on the same row, while different groups (e.g., a Bricklayer followed by AllSites measurements) get separate rows.
Stochastic selection (v0.1 unified rule)
Stochastic operations are expanded with the ENGINE's single-source selection function select_outcome_index — one scalar coin per element k = 1..K, categorical selection among the outcomes, remainder 1 - Σp = identity (element omitted). This is the exact rule simulate! executes: expanding with seed=X shows the same selections the engine makes when its :gates_spacetime stream is seeded with X.
Record markers
(type=:record_mark, ...) pseudo-ops (from record!(c[, names...])) become gate-less marker ExpandedOps (see is_record_mark) in their own group. Markers consume no RNG. Unknown operation types are skipped (never an error), keeping expansion forward-compatible.
Arguments
circuit::Circuit: The circuit to expand (one time step)n_steps::Int: Number of times to repeat the circuit step (default: 1)seed::Int: RNG seed for stochastic branch selection (default: 0)
Returns
Vector{Vector{Vector{ExpandedOp}}}: steps → groups → ops.- Outer vector: one entry per timestep (length
n_steps) - Middle vector: one entry per circuit call that produced ops
- Inner vector: concrete gate operations from that call
- Outer vector: one entry per timestep (length
Groups with no operations (stochastic all-identity selections) are omitted.
See Also
expand_circuit: Flat version (no grouping) for backward compatibilityselect_outcome_index: The shared engine/visualization selection rule
QuantumCircuitsMPS.simulate! — Function
simulate!(circuit::Circuit, state::SimulationState; n_steps::Int=1, record_when::Union{Symbol,Function}=:every_step)Execute a circuit on a simulation state, applying gates and recording observables.
A Circuit represents ONE time step. This function runs that step n_steps times, processing all operations in circuit.operations on each iteration.
Arguments
circuit::Circuit: The circuit to execute (symbolic operations)state::SimulationState: The state to modify in-placen_steps::Int: Number of times to execute the circuit step (default: 1)record_when::Union{Symbol,Function}: Controls when observables are recorded (default::every_step)
Unified stochastic rule (v0.1)
Every stochastic operation (apply_with_prob!) is executed as follows:
- All outcomes expand to the SAME number of elements K (validated at build time; broadcast geometries expand via
elements(geo, L, bc), set geometries are a single element). - For each element k = 1..K: exactly ONE scalar coin is drawn from the
:gates_spacetimestream and a categorical selection is made among the outcomes viaselect_outcome_index(remainder1 - Σp= identity). - The winning outcome's gate is executed at its k-th element; identity applies nothing (and does NOT advance staircases).
Coin consumption is therefore data-independent: K draws per stochastic op per step, always — see expected_draws.
Recording Options
The record_when parameter accepts:
:every_step(default): Record once per step, after the last operation. This fires STRUCTURALLY every step — even when every stochastic op selects the identity ("do nothing") branch.:every_gate: Record after every actual gate execution:final_only: Record only after the final step completes:marks: Record exactly at the circuit'srecord!(c[, names...])marker positions, in the op stream (NOT at the structural step boundary). A circuit with M markers yields exactlyM * n_stepsrecords per (matching) observable, deterministically — independent of stochastic outcomes. Selective markers (record!(c, :entropy)) grow only the named observables' vectors. Requires the circuit to contain markers (ArgumentErrorotherwise).- Custom function
(ctx::RecordingContext) -> Bool: evaluated once per element slot (withis_step_boundary=false), once per marker (withat_mark=true,mark_indexset), and once at the structural end of each step (withis_step_boundary=true,gate_type=nothing); if any evaluation returns true, ONE record is taken at the end of the step.
Marker/policy conflict (ArgumentError)
If the circuit contains record!(c) markers, record_when MUST be :marks or a custom predicate. Passing (or defaulting to) :every_step, :every_gate, or :final_only throws an ArgumentError — those policies would silently ignore the markers.
RecordingContext Fields
Custom recording functions receive a RecordingContext with:
step_idx::Int: Current step index (1 to n_steps)gate_idx::Int: Cumulative element-slot count across all steps (never resets). Advances once per element slot REGARDLESS of the stochastic outcome (identity included), so gate_idx-based recording schedules are trajectory-independent. Markers do NOT advance it.op_idx::Int: Position of the current op incircuit.operations(0 for the step-boundary evaluation)element_idx::Int: Element index within the current op (0 for step-boundary and marker evaluations)gate_type::Any: The gate applied at this slot (nothingfor identity slots, marker evaluations, and the structural step-boundary evaluation)is_step_boundary::Bool: True only for the structural step-boundary evaluation after the op loopat_mark::Bool/mark_index::Int: Marker evaluations carryat_mark=trueand the marker's 1-based ordinal among the circuit's markers (0/false everywhere else)
Examples
# Basic execution with 5 steps
circuit = Circuit(L=4, bc=:periodic) do c
apply_with_prob!(c; outcomes=[
(probability=0.5, gate=Reset(), geometry=StaircaseRight(1)),
(probability=0.5, gate=HaarRandom(), geometry=StaircaseLeft(4))
])
end
rng = RNGRegistry(gates_spacetime=42, gates_realization=44, born_measurement=45)
state = SimulationState(L=4, bc=:periodic, rng=rng)
initialize!(state, ProductState(binary_int=1))
track!(state, :dw => DomainWall(order=1, i1_fn=() -> 1))
# Record after every step (default)
simulate!(circuit, state; n_steps=5)
# Record after every gate
simulate!(circuit, state; n_steps=5, record_when=:every_gate)
# Record only at the end
simulate!(circuit, state; n_steps=5, record_when=:final_only)
# Custom: record every 10 element slots
simulate!(circuit, state; n_steps=5, record_when=every_n_gates(10))
# Custom: record every 2 steps
simulate!(circuit, state; n_steps=10, record_when=every_n_steps(2))RNG Contract
- All stochastic coins come from
:gates_spacetime(the builder no longer accepts anrng=keyword). - Scalar draws only; element order = documented enumeration order of the outcome geometries (
elements). - Consumption is data-independent:
expected_draws(circuit, n_steps)coins.
Thread safety
A Circuit holding staircase/Pointer geometries is MUTABLE (positions advance during simulate!). For per-trajectory parallelism use copy(circuit) (a deepcopy preserving intra-circuit geometry aliasing) so each trajectory owns private geometry state:
Threads.@threads for seed in seeds
c = copy(circuit)
st = SimulationState(...; rng=RNGRegistry(gates_spacetime=seed, ...))
initialize!(st, ProductState(binary_int=0))
simulate!(c, st; n_steps=n)
endValidation
- Throws
ArgumentErrorifn_steps < 1 - Throws
ArgumentErrorfor unknown symbol presets
See Also
expand_circuit: Visualize which gates will be appliedrecord!: Manual observable recordingRecordingContext: Context passed to custom recording functionsevery_n_gates,every_n_steps: Helper functions for common patterns
QuantumCircuitsMPS.ExpandedOp — Type
ExpandedOpRepresents a concrete gate operation at specific sites for a specific timestep.
Fields
step::Int: Circuit timestep (1-indexed)gate::Union{AbstractGate, Nothing}: The gate to apply.nothingmarks arecord!marker pseudo-op (no gate, no sites — seeis_record_mark).sites::Vector{Int}: Physical sites for this operation (empty for markers)label::String: Short label for visualization (e.g., "Rst", "Haar", "CZ";"[R]"/"[R:names]"for record markers)
Usage
Produced by expand_circuit for visualization and manual execution.
QuantumCircuitsMPS.RecordingContext — Type
RecordingContextContext information passed to recording predicate functions during circuit execution.
Fields
step_idx::Int: Current step index (1 to n_steps passed to simulate!)gate_idx::Int: Cumulative element-slot count across all steps (never resets). Advances once per element slot regardless of stochastic outcome;record!(c)markers do NOT advance it.op_idx::Int: 1-based position of the current operation withincircuit.operations(0 for the structural step-boundary evaluation)element_idx::Int: 1-based element index within the current operation (0 for step-boundary and marker evaluations)gate_type::Any: The gate applied at this slot (nothingfor identity slots, marker evaluations, and the step-boundary evaluation)is_step_boundary::Bool: True only for the structural step-boundary evaluation after the op loopat_mark::Bool: True when this evaluation happens at arecord!(c)marker pseudo-opmark_index::Int: 1-based ordinal of the marker among the circuit's markers (stable across steps; 0 whenat_mark == false)
Example
# Custom recording function
function my_recorder(ctx::RecordingContext)
return ctx.gate_idx > 10 && ctx.is_step_boundary
end
# Record only at the second marker of each step
simulate!(circuit, state; record_when = ctx -> ctx.at_mark && ctx.mark_index == 2)QuantumCircuitsMPS.every_n_gates — Function
every_n_gates(n::Int)Create a recording predicate that triggers every n gates.
Arguments
n::Int: Record every n gates (based on cumulative gate_idx)
Returns
Function that takes a RecordingContext and returns Bool
Example
simulate!(state, circuit; record_when=every_n_gates(5))QuantumCircuitsMPS.every_n_steps — Function
every_n_steps(n::Int)Create a recording predicate that triggers every n steps at step boundaries.
Records once per n steps, only when is_step_boundary is true (after all gates in the step have been executed).
Arguments
n::Int: Record every n steps (based on step_idx)
Returns
Function that takes a RecordingContext and returns Bool
Example
simulate!(state, circuit; record_when=every_n_steps(2))QuantumCircuitsMPS.print_circuit — Function
print_circuit(circuit::Circuit; n_steps::Int=1, gates_spacetime::Int=0, io::IO=stdout, unicode::Bool=true)Print an ASCII visualization of a quantum circuit realization.
Shows the stochastic realization determined by gates_spacetime RNG seed, matching what expand_circuit(circuit; seed=gates_spacetime) produces.
Arguments
circuit::Circuit: Circuit to visualizen_steps::Int=1: Number of circuit steps to visualize (default: 1)gates_spacetime::Int=0: RNG seed controlling which stochastic branches fire.io::IO: Output stream (default: stdout)unicode::Bool: Use Unicode box-drawing characters (default: true)
Character Sets
- Unicode mode (default): Uses
─(wire),┤(left box),├(right box) - ASCII mode: Uses
-' (wire),|` (both box edges)
Record Markers
record!(c[, names...]) markers appear as their own row with the marker glyph on every wire: ▽ in Unicode mode, [R] in ASCII mode. Named markers print their names after the wires (e.g. ▽ ... ▽ [R:entropy]).
Layout Algorithm
- Expands the circuit with
expand_circuit_grouped(one group perapply!call) - For each group, packs ops into non-overlapping layers with
pack_ops_into_layers; each layer becomes one visual row, and the group label is printed only on the first row of the group - Letters identify groups (not sub-rows): single-group steps are labeled
"1:", multi-group steps"1a:","1b:", etc.; empty steps → one wire-only row - Calculates fixed column width based on longest gate label (+2 for box chars)
- Renders header row with qubit labels (e.g., "q1", "q2", "q3", "q4")
- Renders time step rows with gate boxes or wire segments
Multi-Qubit Gates
For gates spanning multiple sites (e.g., CZ on sites [2, 3]):
- Gate label appears ONCE in a box on the minimum site
- Other sites show continuation boxes (box edges without label)
Examples
# Basic usage
circuit = Circuit(L=4, bc=:periodic) do c
apply!(c, Reset(), StaircaseRight(1))
end
print_circuit(circuit; n_steps=4)
# Output to file
open("circuit.txt", "w") do io
print_circuit(io, circuit; n_steps=4)
end
# ASCII mode (no Unicode)
print_circuit(circuit; n_steps=4, unicode=false)See Also
expand_circuit: Get a concrete stochastic realizationExpandedOp: Concrete operation representation
print_circuit(circuit::Circuit; n_steps::Int=1, gates_spacetime::Int=0, io::IO=stdout, unicode::Bool=true)Convenience form. Calls print_circuit(io, circuit; ...).
QuantumCircuitsMPS.plot_circuit — Function
plot_circuit(circuit::Circuit; n_steps=1, gates_spacetime=0, filename=nothing)Export an SVG diagram of circuit's deterministic template (all stochastic branches, with probability annotations), resolved for n_steps repetitions using the gates_spacetime seed for stochastic-branch layout — matching expand_circuit(circuit; seed=gates_spacetime).
Requires using Luxor to be loaded first (the implementation lives in the Luxor package extension, ext/QuantumCircuitsMPSLuxorExt.jl); calling this without Luxor loaded throws an ArgumentError telling you to load Luxor. When filename is nothing, the SVG is written to a temporary file and its path returned; otherwise it is written to filename.
See also print_circuit for a Luxor-free ASCII/Unicode terminal visualization of the same circuit template.
Example
using QuantumCircuitsMPS, Luxor
circuit = Circuit(L=4, bc=:periodic) do c
apply!(c, Reset(), StaircaseRight(1))
end
plot_circuit(circuit; gates_spacetime=42, filename="diagram.svg")