API Reference¶
For a guided tour see Getting Started and Circuit Execution.
Gate / operator matrix inputs
Methods that take a 1- or 2-qubit gate or operator (apply_gate_1q,
apply_gate_2q, expectation_1q, expectation_2q, apply_kraus_*,
apply_mixed_unitary_*) expect a numpy complex128 array: a (2, 2)
array for a 1-qubit gate, a (4, 4) array for a 2-qubit gate, and a
(n_ops, dim, dim) stack for a set of Kraus / mixed-unitary operators.
Qubit / bitstring ordering
Exaqt is little-endian by qubit index: qubit 0 is the
least-significant bit of the amplitude index, so bs[0] is qubit 0.
This matches Qiskit's little-endian ordering and MimiqCircuitsBase,
the Julia implementation.
State vector — ExaqtSV¶
The dense quantum register. Amplitudes are complex128, indexed
little-endian (qubit 0 = LSB).
Constructors:
| Method | Description |
|---|---|
ExaqtSV.zero(num_qubits) |
\|0...0> state on num_qubits qubits |
ExaqtSV.from_bitstring(bits) |
Product state from a 0/1 sequence (qubit 0 = LSB) |
ExaqtSV.from_amplitudes(num_qubits, arr) |
State from a complex128 amplitude array |
There is no fixed cap on num_qubits. Host memory is what binds — an
amplitude is 16 bytes, so a 30-qubit state is 16 GiB — and that limit is
reported rather than predicted: zero raises MemoryError when the
buffer cannot be reserved. The only absolute bound is arithmetic, at 60
qubits on a 64-bit host (16 TiB), which no machine approaches.
Properties & inspection:
| Member | Returns |
|---|---|
sv.num_qubits |
int — number of qubits |
sv.len / len(sv) |
int — number of amplitudes, 2**num_qubits |
sv.get(idx) |
complex — amplitude of basis state idx |
sv.probability(idx) |
float — probability of basis state idx |
sv.amplitudes() |
complex128 numpy array of all amplitudes |
sv.norm_squared() |
float — squared norm |
sv.normalize() |
float — norm before normalising; renormalises in place |
sv.reset_to_zero() |
Reset to \|0...0> in place |
sv.clone() |
Deep copy |
Single-qubit gates (target qubit last, angles first):
| Method | Description |
|---|---|
sv.apply_x/y/z(target) |
Pauli gates |
sv.apply_h(target) |
Hadamard |
sv.apply_s/sdg/t/tdg/sx(target) |
Phase / T / SX gates |
sv.apply_p(lmbda, target) |
Phase gate P(lambda) |
sv.apply_rx/ry/rz(angle, target) |
Rotation gates |
sv.apply_u(theta, phi, lmbda, target) |
General U gate |
sv.apply_gate_1q(gate, target) |
Arbitrary 1-qubit unitary, (2, 2) complex128 |
Two-qubit gates:
| Method | Description |
|---|---|
sv.apply_cx/cy/cz(a, b) |
Controlled Pauli gates |
sv.apply_swap/iswap(q1, q2) |
(i)SWAP |
sv.apply_cp(lmbda, q1, q2) |
Controlled phase |
sv.apply_crx/cry/crz(angle, control, target) |
Controlled rotations |
sv.apply_rxx/ryy/rzz(theta, q1, q2) |
Two-qubit rotations |
sv.apply_gate_2q(gate, q1, q2) |
Arbitrary 2-qubit unitary, (4, 4) complex128 |
Multi-controlled gates:
| Method | Description |
|---|---|
sv.apply_mcx(controls, target) |
Multi-controlled X |
sv.apply_controlled_gate_1q(gate, controls, target) |
Controlled 1-qubit unitary |
sv.apply_controlled_gate_2q(gate, controls, q1, q2) |
Controlled 2-qubit unitary |
Measurement & sampling (all take a seeded Rng):
| Method | Returns |
|---|---|
sv.prob_zero(target) |
float — probability of measuring 0 on target |
sv.measure_qubit(target, rng) |
int — outcome, collapses the state |
sv.reset_qubit(target, rng) |
int — pre-reset outcome, resets qubit to \|0> |
sv.sample_bitstring(rng) |
uint8 array — one sampled bitstring |
sv.sample(rng, nsamples) |
(nsamples, num_qubits) uint8 array |
Noise channels (each takes a seeded Rng):
| Method | Description |
|---|---|
sv.apply_kraus_1q(kraus_ops, target, rng) |
1-qubit Kraus channel, (n_ops, 2, 2) |
sv.apply_kraus_2q(kraus_ops, q1, q2, rng) |
2-qubit Kraus channel, (n_ops, 4, 4) |
sv.apply_mixed_unitary_1q(unitaries, probs, target, rng) |
1-qubit mixed-unitary channel |
sv.apply_mixed_unitary_2q(unitaries, probs, q1, q2, rng) |
2-qubit mixed-unitary channel |
Expectation values:
| Method | Returns |
|---|---|
sv.expectation_1q(op, target) |
complex — <psi\|O\|psi> for a (2, 2) operator |
sv.expectation_2q(op, q1, q2) |
complex — <psi\|O\|psi> for a (4, 4) operator |
sv.expectation_pauli(paulis, qubits) |
complex — expectation of a Pauli string (e.g. "ZZZ") |
Simulator — ExaqtQCS¶
exaqt.ExaqtQCS
¶
Bases: LocalBackend
State-vector quantum circuit simulator.
Subclasses :class:mimiqcircuits.backends.LocalBackend and
declares the Julia ExaqtQCS capability set plus
parametric (Python-only — supported through the inherited
slow-path :meth:bind; the Julia wrapper omits the token).
Parametric circuits are supported via the inherited slow-path
:meth:bind (substitute then re-compile); there is no native
compile-once / bind-many fast path yet.
Pauli-string expectations on any number of qubits are supported:
ExpectationValue(PauliString("XYZ...")) dispatches to the
native mask-based expectation_pauli (cost O(2^n) in the
state size, independent of the string length). Only expectations
of non-Pauli operators (e.g. a GateCustom matrix, or a
Control/Power/Inverse-wrapped Pauli) keep the dense two-qubit cap.
Caveat — noise + :class:ExactFidelity: _apply_kraus
renormalises the state after sampling a Kraus branch, so the
per-shot fidelity is always 1.0 and the typed wrap is
:class:ExactFidelity. That is honest for trace-preserving
channels (the sampled trajectory is exact under renormalisation).
For non-trace-preserving channels (e.g. amplitude damping with
explicit loss) the surviving-trajectory probability is not
tracked, and :class:ExactFidelity over-states reliability.
Attributes:
| Name | Type | Description |
|---|---|---|
seed |
instance-level RNG seed. |
|
reorderqubits |
qubit-reordering policy for the default pipeline
(:meth: Ignored entirely when |
|
dynamic_qubits |
whether to size the amplitude buffer to the
live qubits instead of reserving the full
Because the kernels are DRAM-bandwidth-bound past ~20 qubits,
where the number of sweeps is what costs, releasing Qubit indices stay in your own numbering; Note that Setting this to |
Examples:
>>> import mimiqcircuits as mc
>>> from exaqt import ExaqtQCS
>>> sim = 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 = sim.execute(c, nsamples=1000)
__init__(seed=None, reorderqubits=None, dynamic_qubits=False)
¶
execute(circuit, nsamples=1000, *, seed=None, rng=None, num_qubits=None, passes=None, callback=None, param_grid=None, strict_pass_order=True, stopped=None, progress=False)
¶
Execute a circuit and return :class:mimiqcircuits.QCSResults.
seed and rng are mutually exclusive entropy sources;
pass at most one. With neither, the simulator's instance
seed (set at construction) is used, falling back to fresh
OS entropy. Amplitudes are requested via in-circuit
:class:Amplitude ops that write into results.zstates.
With no passes, :meth:default_passes runs — gate fusion
above a cache-derived qubit threshold, then qubit reordering per
the reorderqubits constructor option. Pass
passes=PassPipeline() to disable both or a custom pipeline to
retune them.
evolvezerostate(circuit, *, seed=None)
¶
Evolve the zero state through circuit; return
(state, fidelity) with a plain float fidelity (always
1.0 for Exaqt's renormalising path).
Convenience: matches the engines-side SimulatorState.zero(...)
+ evolve(state, c, rng=...) two-step. The dual-shape
:meth:evolve covers the more general case where the caller
owns the state.
Seed resolution mirrors :meth:execute: call-site seed= →
instance self.seed → fresh cryptographic entropy from
:mod:secrets.
For circuits with Kraus channels or mid-circuit measurements
this is a single stochastic trajectory; use :meth:execute
for ensemble statistics.
evolve(state, compiled, *, rng=None, callback=None, stopped=None)
¶
Evolve state under compiled. Returns
(state, ExactFidelity) — state-vector evolution is exact
per trajectory, so no truncation bound is reported.
compile(circuit)
¶
Identity-wrap the circuit.
NOTE: Exaqt dispatches every supported gate natively, so
no Julia-side convertcircuit impurity applies here (cf. the
engines wrappers). The wrap is a pure identity over the circuit.
expectation(state, op, *qubits)
¶
Compute ⟨ψ|op|ψ⟩ on state without mutating it.
Supports 1- and 2-qubit operators. qubits is the list of
qubit indices op acts on (0-based, matching the rest of
the Python API).
Raises :class:ValueError for any other operator size.
capabilities()
¶
GPU simulator — ExaqtSVGpu / ExaqtQCSGpu¶
Off by default
GPU support is a build-time opt-in. A stock wheel does not carry it:
the extension must be built with the gpu cargo feature
(maturin develop --release --features gpu) on a host with a CUDA
toolkit and a cuQuantum SDK ($CUQUANTUM_ROOT). Probe at runtime with
exaqt.gpu_available() — ExaqtSVGpu, ExaqtQCSGpu, CudaError and
CudaUnavailableError only exist when it returns True.
These members are documented by hand rather than generated, precisely because they are absent from a CPU-only build.
ExaqtSVGpu is a state vector living in device memory, backed by NVIDIA's
cuStateVec. Every method it has is named and shaped exactly like its
ExaqtSV counterpart — same gate set, same little-endian qubit order, same
column-major matrix convention — and ExaqtQCSGpu advertises the same
capability set as ExaqtQCS. Any circuit that runs on one runs on the other.
import exaqt
if exaqt.gpu_available():
results = exaqt.ExaqtQCSGpu().execute(circuit, nsamples=1000)
Reproducible across backends. Every stochastic operation — measurement,
reset, sampling, Kraus and mixed-unitary branch selection — draws from the
same seeded Xoshiro256++ stream in the same order and count as the CPU core.
So ExaqtQCSGpu(seed=n) and ExaqtQCS(seed=n) follow the same trajectory,
and a run can be reproduced on a laptop without a GPU.
The one place exact agreement is not guaranteed by construction is sample:
cuStateVec searches its own cumulative array, so an exact tie between two
basis states can in principle resolve differently in the last bit. The
distribution is identical either way.
How wide a state may be. There is no fixed qubit cap — the card's
free memory decides, and ExaqtSVGpu.max_qubits_on_device() reports it:
the widest state the current device will hold right now, 0 if not even
one qubit fits.
The constructors check the same thing before allocating. A state is
2**n * 16 bytes, so 30 qubits needs 16 GiB and fits a 24 GB card, while
an 80 GB card reaches 32. A request the device has no room for raises
MemoryError naming the byte counts, rather than a bare CUDA status.
The figure is a snapshot, not a constant — memory held by an existing state, or by another process on the same card, lowers it. Reading it also creates and destroys a CUDA context, so it measures the device as a constructor leaves it; that makes it a capability query rather than something to call in a loop.
Nothing is padded onto the measurement. A margin would be a guess, and a
guess in that direction refuses states that would have run; the marginal
case is left to cudaMalloc, which reports rather than crashing.
The minimum is 1 qubit, unlike ExaqtSV: cuStateVec has no 0-qubit state.
Three shape differences from ExaqtSV:
| Member | Description |
|---|---|
sv.synchronize() |
GPU-only. Block until queued kernels finish — gate calls only enqueue work, so any timing loop needs this. |
sv.set_amplitude(idx, value) |
GPU-only. Overwrite one amplitude in place (no re-normalisation). |
sv.clone() |
Can raise CudaError. Device memory is scarcer than host memory, so a failed allocation reports rather than aborting. |
sv.amplitudes() exists but copies the whole buffer back to the host — 16
bytes per amplitude, so 4 GiB at 28 qubits. Prefer sv.get(idx).
expectation_pauli returns a value whose imaginary part is exactly zero,
where the CPU version carries floating-point noise of order 1e-16: cuStateVec
computes a Pauli product's expectation as a real number. Compare with a
tolerance, not for equality.
Differences in ExaqtQCSGpu's default pipeline. Fusion runs at every
circuit size, with no cache-derived qubit threshold: on a GPU a fused block
costs one pass over the state whatever its width, so fusing n gates is worth
about n×. Qubit reordering is omitted — ExaqtReorderQubitsPass scores the
CPU's per-slot SIMD kernels, which say nothing about cuStateVec. EXAQT_FUSE,
EXAQT_FUSE_THRESHOLD and EXAQT_FUSE_MAX_SUPPORT are honoured as on the CPU.
Errors. Failures that come from the shared core (a bad qubit index, a
malformed gate matrix) raise the same exceptions as the CPU backend, so
except clauses stay backend-agnostic. Two are GPU-specific: CudaError
(a CUDA or cuStateVec call returned a non-success status) and
CudaUnavailableError (the extension carries the feature but was built
without a CUDA runtime). Both derive from ExaqtError. A state too wide
for the card raises plain MemoryError — the same exception the CPU
backend raises for a host allocation it cannot make, so one except
covers both.
Composite state — ExaqtState¶
exaqt.ExaqtState
¶
Bases: State
Composite simulation state.
Use one of the staticmethod factories — :meth:zero,
:meth:one, :meth:product — to construct a state, then pass it
to :func:exaqt.evolve or :func:exaqt.apply_instruction.
Attributes:
| Name | Type | Description |
|---|---|---|
q |
ExaqtSV
|
the dense quantum register. |
c |
list[int]
|
classical-bit register, length |
z |
list[complex]
|
complex z-variable register, length
|
zero(num_qubits, num_bits=0, num_zvars=0, dynamic_qubits=False)
staticmethod
¶
|0…0⟩ quantum state, classical/z registers initialised to 0.
dynamic_qubits=True enables dynamic qubit allocation — see
:class:~exaqt.ExaqtQCS.
one(num_qubits, num_bits=0, num_zvars=0, dynamic_qubits=False)
staticmethod
¶
|1…1⟩ quantum state, classical/z registers initialised to 1.
product(bits, num_bits=0, num_zvars=0, dynamic_qubits=False)
staticmethod
¶
Product state from a 0/1 bitstring (qubit 0 = LSB).
amplitude(bitstring)
¶
Get the amplitude for a given computational basis state.
sample(nsamples, rng=None, *, seed=None)
¶
Sample measurement outcomes from the quantum state.
Either rng (a :class:random.Random) or seed (an
int) may be passed, but not both. The Rust Rng is
constructed from a 63-bit derived seed when rng is given.
reset()
¶
Reset all four registers to their zero state in place.
Per-instruction primitives¶
exaqt.evolve(state, circuit, *, rng=None, callback=None)
¶
Evolve state through every instruction in circuit in place.
Returns (state, fidelity). Fidelity is always 1.0 for the
state-vector simulator.
callback, when given, is called as callback(i, total) after
each applied instruction — the execute driver turns this into the
per-step execution bar.
exaqt.apply_instruction(state, inst, rng=None)
¶
Apply a single circuit instruction to state in place.
Returns fidelity (1.0 — Exaqt is a noiseless simulator).
Raises :class:NotImplementedError for operations the rust core does
not support yet (Kraus channels, BondDim, SchmidtRank).
Compilation passes¶
exaqt.ExaqtFlattenContainersPass
¶
Bases: AbstractPass
Expand container operations into the instructions they hold.
A container — a GateDecl call (what push_suzukitrotter /
push_lietrotter and the UCCSD builders emit), a Block, a
Repeat / Parallel replicator, a PauliString, or any
Control / Inverse / Power wrapped around one — stands for a
sub-circuit, not for a matrix. :func:exaqt.execute.apply_instruction
can expand one at apply time, and that is enough to get the right
answer — a container reports its own arity and unitarity honestly, so
the sampling-versus-trajectory analysis classifies it correctly without
looking inside.
What apply-time expansion is too late for is the optimisation passes.
:class:mimiqcircuits.FusePass and :class:ExaqtReorderQubitsPass see
one opaque wide operation and have nothing to work with, so a circuit
built entirely out of Trotter steps runs neither fused nor reordered.
Running this pass first gives them the real gates.
Qubit indices are unchanged (the rewrite happens in the parent's index space), so the pass returns no permutation, and a circuit that holds no container is returned untouched.
exaqt.ExaqtReorderQubitsPass
¶
Bases: AbstractPass
Reorder qubits to land gates on the fastest kernels.
Uses the native optimize_ordering to minimise the estimated
runtime Σ weight · kernel_cost(slot) over qubit permutations,
where kernel_cost captures the two-qubit SIMD threshold (lower
index >= 2) and the slower 1-qubit slots 0/1/2.
Parameters¶
method:
"greedy" (default, deterministic) or "sa" /
"simulated_annealing". True is a back-compat shorthand
for "greedy".
seed:
Seed for "sa". None draws one from the pass-context RNG.
qubit_threshold:
Minimum qubit count for the pass to act; smaller circuits pass
through unchanged. 0 (the default) always reorders — the
gate is meant for the default pipeline, which sets it so
reordering only kicks in once the state vector outgrows the
cache.
spec()
¶
Identify this pass and its method / optional seed.
preserves(feature)
¶
Always True: relabelling preserves every circuit feature;
the pipeline maps the output cstate back to the user's frame.
apply(ctx, circuit)
¶
Run the optimiser and return the rewritten circuit plus the
permutation it chose (None if it left the circuit unchanged).
Random number generator — Rng¶
Rng(seed) is a seeded PRNG handle used by every measurement, sampling,
and noise-channel method. The same seed reproduces the same Xoshiro256++
stream across the Rust core and the Python and Julia wrappers.
Package metadata¶
| Member | Returns |
|---|---|
exaqt.__version__ |
str — installed version of the mimiq-exaqt distribution |
exaqt.docs_dir()
¶
Path to the HTML documentation bundled with the package.
Returns the directory holding the built documentation site
(index.html and assets) that ships inside the wheel. Open it with
the exaqt docs console command. The directory is absent in builds
made without the documentation site, in which case the online copy at
https://docs.qperfect.io/exaqt-python/ applies.
Exceptions¶
All exceptions subclass ExaqtError (itself a RuntimeError):
| Exception | Raised when |
|---|---|
GateShapeError |
a gate matrix has the wrong shape or layout |
QubitIndexError |
a qubit index is out of bounds, or duplicated |
DegenerateStateError |
sampling a state with zero / non-finite norm |