Skip to content

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. None means "fresh entropy on every :meth:execute". An integer makes the instance reproducible. Per-call seed= / rng= (mutually exclusive) overrides the instance value.

reorderqubits

qubit-reordering policy for the default pipeline (:meth:default_passes). None (the default) reorders automatically once a circuit is wide enough to benefit (>= 13 qubits); False never reorders; True / "greedy" / "sa" always reorders with that method. Exaqt relabels qubits to land gates on the fastest SIMD kernels — the state-vector analogue of the distance-based reorderqubits used by tensor-network backends. Instance-only (the local backend has no execute(..., reorderqubits=) knob); for finer control pass a :class:~exaqt.ExaqtReorderQubitsPass via execute(..., passes=).

Ignored entirely when dynamic_qubits=True — dynamic allocation assigns physical slots in first-touch order, so no relabelling survives to affect throughput. Setting both raises a :class:RuntimeWarning.

dynamic_qubits

whether to size the amplitude buffer to the live qubits instead of reserving the full 2**num_qubits up front. Default False (reserve everything).

True enables dynamic qubit allocation: the buffer is sized to the live qubits instead. Each is allocated on first use, and every Reset / MeasureReset releases its qubit, halving the buffer. Truncation is exact — a reset qubit is |0⟩ and hence unentangled, so the discarded half is all zeros. (What stops being unitary at runtime is Reset itself, not this optimisation.)

Because the kernels are DRAM-bandwidth-bound past ~20 qubits, where the number of sweeps is what costs, releasing k ancillas makes every later gate 2**k times cheaper — plus a further jump if the state drops back into cache. Worth it for circuits that measure ancillas out partway through (iterative phase estimation, arithmetic, distillation). No benefit, and a negligible per-gate mapping cost, for circuits that never reset.

Qubit indices stay in your own numbering; num_qubits keeps reporting the declared width while :attr:~exaqt.ExaqtState.live_qubits reports the physical one. If the full 2**num_qubits cannot be reserved, state construction fails rather than falling back to a smaller state vector.

Note that randstate is always fully allocated (a random full-support state has nothing to gain), so resets on one will not shrink the buffer.

Setting this to False also disables the qubit-reordering pass (see :attr:reorderqubits and :meth:default_passes): first-touch slot assignment overrides the relabelling, so the search costs time and buys nothing. The two optimisations want opposite slot assignments — reordering wants busy qubits in high slots, releasing wants dying ones there — so they need one merged cost model rather than two stacked passes. Fusion is unaffected and still applies.

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

z list[complex]

complex z-variable register, length num_zvars.

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.

spec()

Identify this pass. It takes no parameters.

preserves(feature)

Always True: expanding a container adds no qubit and relabels none.

apply(ctx, circuit)

Expand every container in circuit, recursively.

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.

from exaqt import Rng
rng = Rng(seed=42)

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