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 apply
  • needs_normalization(gate) == true (projective gates): normalize + truncate
source
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).

source
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))
end
source
apply!(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).

source
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.

source
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).

source
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)

  • outcomes must 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 Pointer geometry, Σ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_spacetime in 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 a Circuit for simulate!)
  • apply!(state, gate, geometry): deterministic, unconditional application
source
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)

  • outcomes must 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 Pointer geometry, Σ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))
    ])
end
source
QuantumCircuitsMPS.CircuitType
Circuit(; L::Int, bc::Symbol, operations=NamedTuple[])

Lazy representation of a quantum circuit as a sequence of symbolic operations.

1D only

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 system
  • bc::Symbol: Boundary conditions (:periodic or :open)
  • operations::Vector{NamedTuple}: Internal symbolic operation list
  • params::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)
end

Execution

Pass the circuit to simulate! to execute:

state = SimulationState(...)
simulate!(circuit, state; n_steps=50)  # Runs the circuit 50 times

See Also

  • expand_circuit: Expands symbolic operations to concrete site lists
  • simulate!: Executes a circuit on a simulation state
source
QuantumCircuitsMPS.expand_circuitFunction
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 length n_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

source
QuantumCircuitsMPS.expand_circuit_groupedFunction
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

Groups with no operations (stochastic all-identity selections) are omitted.

See Also

  • expand_circuit: Flat version (no grouping) for backward compatibility
  • select_outcome_index: The shared engine/visualization selection rule
source
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-place
  • n_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:

  1. 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).
  2. 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 (remainder 1 - Σp = identity).
  3. 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's record!(c[, names...]) marker positions, in the op stream (NOT at the structural step boundary). A circuit with M markers yields exactly M * n_steps records 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 (ArgumentError otherwise).
  • Custom function (ctx::RecordingContext) -> Bool: evaluated once per element slot (with is_step_boundary=false), once per marker (with at_mark=true, mark_index set), and once at the structural end of each step (with is_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 in circuit.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 (nothing for 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 loop
  • at_mark::Bool / mark_index::Int: Marker evaluations carry at_mark=true and 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 an rng= 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)
end

Validation

  • Throws ArgumentError if n_steps < 1
  • Throws ArgumentError for unknown symbol presets

See Also

  • expand_circuit: Visualize which gates will be applied
  • record!: Manual observable recording
  • RecordingContext: Context passed to custom recording functions
  • every_n_gates, every_n_steps: Helper functions for common patterns
source
QuantumCircuitsMPS.ExpandedOpType
ExpandedOp

Represents 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. nothing marks a record! marker pseudo-op (no gate, no sites — see is_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.

source
QuantumCircuitsMPS.RecordingContextType
RecordingContext

Context 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 within circuit.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 (nothing for identity slots, marker evaluations, and the step-boundary evaluation)
  • is_step_boundary::Bool: True only for the structural step-boundary evaluation after the op loop
  • at_mark::Bool: True when this evaluation happens at a record!(c) marker pseudo-op
  • mark_index::Int: 1-based ordinal of the marker among the circuit's markers (stable across steps; 0 when at_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)
source
QuantumCircuitsMPS.every_n_gatesFunction
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))
source
QuantumCircuitsMPS.every_n_stepsFunction
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))
source
QuantumCircuitsMPS.print_circuitFunction
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 visualize
  • n_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

  1. Expands the circuit with expand_circuit_grouped (one group per apply! call)
  2. 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
  3. Letters identify groups (not sub-rows): single-group steps are labeled "1:", multi-group steps "1a:", "1b:", etc.; empty steps → one wire-only row
  4. Calculates fixed column width based on longest gate label (+2 for box chars)
  5. Renders header row with qubit labels (e.g., "q1", "q2", "q3", "q4")
  6. 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 realization
  • ExpandedOp: Concrete operation representation
source
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; ...).

source
QuantumCircuitsMPS.plot_circuitFunction
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")
source