Skip to content

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:

  1. 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.
  2. Evolve — the state vector is advanced instruction by instruction.
  3. 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.