Circuit Execution¶
The high-level way to run a circuit is ExaqtQCS, a simulator that
subclasses mimiqcircuits.backends.LocalBackend. You build a circuit with
mimiqcircuits, call execute(), and receive the same QCSResults
object MIMIQ uses everywhere else.
import mimiqcircuits as mc
from exaqt import ExaqtQCS
# Build a Bell-state circuit with measurements.
c = mc.Circuit()
c.push(mc.GateH(), 0)
c.push(mc.GateCX(), 0, 1)
c.push(mc.Measure(), 0, 0)
c.push(mc.Measure(), 1, 1)
sim = ExaqtQCS()
results = sim.execute(c, nsamples=1000)
print(results.histogram()) # e.g. {bs"00": 498, bs"11": 502}
print(results.fidelities[0]) # 1.0 — state-vector evolution is exact
print(results.timings) # timing breakdown, e.g. apply / sample
How it works¶
ExaqtQCS.execute() walks the mimiqcircuits.Circuit and dispatches
each instruction to the native ExaqtSV state vector:
- Dispatch — every supported gate is applied natively. Named gates
(
GateH,GateCX,GateRX, …) hit specialised kernels; multi-controlled gates go through the direct multi-control path; anything else that is a 1- or 2-qubit unitary falls back to a generic dense-matrix apply. - Evolve — the state vector is advanced instruction by instruction.
- Sample / measure — measurement outcomes are collected into the
classical registers and returned as
QCSResults.
The simulator is exact per trajectory, so results.fidelities is always
1.0.
Execution modes¶
execute() selects one of two modes automatically, based on circuit
content:
Sampling mode¶
Used when every non-unitary instruction is a trailing measurement.
The state is evolved once and the final distribution is sampled
nsamples times — fast, because the expensive evolution happens only
once.
import mimiqcircuits as mc
from exaqt import ExaqtQCS
c = mc.Circuit()
c.push(mc.GateH(), 0)
c.push(mc.GateCX(), 0, 1)
c.push(mc.Measure(), 0, 0)
c.push(mc.Measure(), 1, 1)
results = ExaqtQCS().execute(c, nsamples=1000)
print(results.histogram()) # {bs"00": ~500, bs"11": ~500}
Trajectory mode¶
Used when the circuit contains mid-circuit measurements, resets,
classically conditioned operations (IfStatement), or noise channels
(krauschannel). Each of the nsamples shots evolves a fresh state
through the whole circuit independently.
import mimiqcircuits as mc
from exaqt import ExaqtQCS
c = mc.Circuit()
c.push(mc.GateH(), 0)
c.push(mc.Measure(), 0, 0) # mid-circuit measurement -> trajectory mode
c.push(mc.GateX(), 1)
c.push(mc.GateCX(), 1, 2)
c.push(mc.Measure(), 1, 1)
c.push(mc.Measure(), 2, 2)
results = ExaqtQCS().execute(c, nsamples=100)
print(len(results.fidelities)) # 100 — one per trajectory
Reproducibility¶
execute() takes mutually exclusive seed= and rng= entropy sources;
pass at most one. With neither, the simulator's instance seed (set at
construction) is used, falling back to fresh OS entropy.
from exaqt import ExaqtQCS
# Reproducible from a per-call seed ...
results = ExaqtQCS().execute(c, nsamples=1000, seed=1234)
# ... or make the whole instance reproducible.
sim = ExaqtQCS(seed=1234)
results = sim.execute(c, nsamples=1000)
Amplitudes and expectation values¶
Non-destructive observations are requested with in-circuit ops that write
into the results. An Amplitude op records a single basis-state amplitude
into results.zstates; an ExpectationValue op records <psi|O|psi>.
import mimiqcircuits as mc
from exaqt import ExaqtQCS
c = mc.Circuit()
c.push(mc.GateH(), 0)
c.push(mc.GateCX(), 0, 1)
# Record the amplitude of |00> into z-variable 0.
c.push(mc.Amplitude(mc.BitString("00")), 0)
results = ExaqtQCS().execute(c, nsamples=1)
print(results.zstates[0][0]) # (0.707..+0j)
ExpectationValue supports 1- and 2-qubit operators. For a Pauli-string
expectation on more than two qubits, drop to the low-level
ExaqtSV.expectation_pauli method on the state vector.
Noise¶
Kraus and mixed-unitary channels (for example Depolarizing,
PauliNoise, AmplitudeDamping) are sampled per trajectory. Adding any
noise channel puts execute() into trajectory mode, so run enough
nsamples to gather ensemble statistics.
import mimiqcircuits as mc
from exaqt import ExaqtQCS
c = mc.Circuit()
c.push(mc.GateH(), 0)
c.push(mc.GateCX(), 0, 1)
c.push(mc.Depolarizing1(0.05), 0) # 5% depolarising noise on qubit 0
c.push(mc.Measure(), 0, 0)
c.push(mc.Measure(), 1, 1)
results = ExaqtQCS().execute(c, nsamples=1000, seed=7)
print(len(results.cstates)) # 1000 — one measurement record per shot
print(list(results.cstates[0])) # e.g. [0, 0] or [1, 1]
Qubit reordering¶
Gate throughput depends on which qubit indices a gate touches. Because the backend is all-to-all, qubits can be relabelled exactly — no SWAP gates, no approximation — to land gates on the fastest kernels. Results are mapped back to your original qubit frame, so you see identical outcomes, just faster.
This runs by default for circuits of 13 or more qubits — the point past
which the state vector outgrows the cache and the per-slot kernel-cost gap
starts to matter. The reorderqubits constructor option governs it (it is the
state-vector analogue of the reorderqubits knob tensor-network backends use):
from exaqt import ExaqtQCS
ExaqtQCS() # reorder wide circuits (the default)
ExaqtQCS(reorderqubits=False) # never reorder
ExaqtQCS(reorderqubits="sa") # always reorder, simulated-annealing search
For finer control, drive ExaqtReorderQubitsPass yourself — e.g. to force
it on a narrow circuit, or to compare against no reordering:
import mimiqcircuits as mc
from mimiqcircuits.backends import PassPipeline
from exaqt import ExaqtQCS, ExaqtReorderQubitsPass
sim = ExaqtQCS()
passes = PassPipeline([ExaqtReorderQubitsPass(method="greedy")])
results = sim.execute(c, nsamples=1000, passes=passes)
Driving evolution manually¶
For incremental control, use the module-level evolve and
apply_instruction helpers on a ExaqtState. This is the same
machinery execute() drives internally.
import mimiqcircuits as mc
from exaqt import ExaqtState, evolve, Rng
c = mc.Circuit()
c.push(mc.GateH(), 0)
c.push(mc.GateCX(), 0, 1)
state = ExaqtState.zero(num_qubits=2)
state, fidelity = evolve(state, c, rng=Rng(seed=42))
print(state.q.amplitudes()) # the underlying ExaqtSV amplitudes
See the API Reference for the full signatures.