Getting Started¶
This page covers installing Exaqt from source and driving the
low-level ExaqtSV state vector directly. If you want to run
mimiqcircuits.Circuit objects and collect QCSResults, see
Circuit Execution.
Prerequisites¶
- Python >= 3.10
- A Rust toolchain (for building from source) —
rustupwith the stable toolchain is sufficient maturinto build the PyO3 extension
Installation¶
Exaqt is distributed as mimiq-exaqt and imported as exaqt.
Wheels are published to the project's GitLab PyPI package registry on
each release tag:
pip install mimiq-exaqt --index-url https://gitlab.qperfect.io/api/v4/projects/<id>/packages/pypi/simple
One abi3 wheel per platform covers Python 3.10 and every later version.
Building from source¶
For development, the Rust core (exaqt-rs) must be checked out alongside
the Python package — the build expects ../exaqt-rs to exist.
git clone git@gitlab.qperfect.io:development/exaqt/exaqt-rs.git
git clone git@gitlab.qperfect.io:development/exaqt/exaqt-python.git
cd exaqt-python
# Create an environment and install the runtime dependencies.
uv venv
uv pip install mimiqcircuits numpy
# Build the extension in place (editable, optimised).
uv run maturin develop --release
Verify the install:
Reading the documentation offline¶
The wheel ships this documentation site inside it, so the version you read always matches the version you installed:
exaqt docs # open the bundled site in a browser
exaqt docs --print-path # or print the path to its index.html
exaqt.docs_dir() returns the same directory from Python. A source build
(maturin develop) does not build the docs, so the command reports them as
missing and points at the online copy instead.
The state vector¶
ExaqtSV is the dense quantum register. Amplitudes are stored in
little-endian order: index i of the amplitude array is the basis state
whose bit k is set when qubit k is |1>. Qubit 0 is therefore the
least-significant bit.
import numpy as np
from exaqt import ExaqtSV
# Allocate |000> — three qubits, 2**3 = 8 amplitudes.
sv = ExaqtSV.zero(3)
print(sv.num_qubits) # 3
print(len(sv)) # 8
# Build a GHZ state: H on qubit 0, then fan out with CX.
sv.apply_h(0)
sv.apply_cx(0, 1)
sv.apply_cx(0, 2)
# Read the full amplitude vector (numpy complex128, length 2**3).
amps = sv.amplitudes()
print(amps[0], amps[7]) # (0.707..+0j) (0.707..+0j) — |000> and |111>
# Probability of a single basis state, without materialising the vector.
print(sv.probability(0)) # 0.5
print(sv.probability(7)) # 0.5
Gates¶
Common gates have dedicated methods; the target qubit is always the last positional argument, and rotation angles come first.
import math
from exaqt import ExaqtSV
sv = ExaqtSV.zero(2)
# Non-parametric single-qubit gates.
sv.apply_x(0) # Pauli-X on qubit 0
sv.apply_h(1) # Hadamard on qubit 1
# Parametric single-qubit gates: angle(s) first, target last.
sv.apply_rx(math.pi / 2, 0) # RX(pi/2) on qubit 0
sv.apply_u(math.pi, 0.0, math.pi, 1) # U(theta, phi, lambda) on qubit 1
# Two-qubit gates.
sv.apply_cx(0, 1) # control 0, target 1
sv.apply_rzz(math.pi / 4, 0, 1) # RZZ(pi/4) on qubits 0 and 1
For any unitary not covered by a named method, pass the matrix as a numpy
complex128 array:
import numpy as np
from exaqt import ExaqtSV
sv = ExaqtSV.zero(1)
# Hadamard as an explicit 2x2 matrix.
h = np.array([[1, 1], [1, -1]], dtype=np.complex128) / np.sqrt(2)
sv.apply_gate_1q(h, 0)
# A 4x4 array goes to apply_gate_2q(gate, q1, q2).
Expectation values¶
Compute <psi|O|psi> without mutating the state. Pass 1- or 2-qubit
operators as numpy matrices, or a Pauli string spanning any number of
qubits.
import numpy as np
from exaqt import ExaqtSV
sv = ExaqtSV.zero(3)
sv.apply_h(0)
sv.apply_cx(0, 1)
sv.apply_cx(0, 2) # GHZ state
# Pauli-string expectation — no need to build a 2**k x 2**k matrix.
# XXX is a GHZ stabiliser, so its expectation is +1.
print(sv.expectation_pauli("XXX", [0, 1, 2])) # (1+0j)
# 1-qubit operator expectation from a matrix.
z = np.array([[1, 0], [0, -1]], dtype=np.complex128)
print(sv.expectation_1q(z, 0)) # (0+0j) — <Z> = 0 on qubit 0
Measurement and sampling¶
Sampling and measurement draw from a seeded Rng. The same seed always
reproduces the same stream.
from exaqt import ExaqtSV, Rng
sv = ExaqtSV.zero(2)
sv.apply_h(0)
sv.apply_cx(0, 1) # Bell state
rng = Rng(seed=42)
# Draw 1000 shots at once — shape (1000, 2), dtype uint8, qubit 0 first.
shots = sv.sample(rng, nsamples=1000)
print(shots.shape) # (1000, 2)
# A single destructive measurement of one qubit (collapses the state).
outcome = sv.measure_qubit(0, rng)
print(outcome) # 0 or 1
Errors¶
The wrapper raises subclasses of ExaqtError:
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.NonUnitaryError— only raised when the Rust core is built with theunitary-checksfeature; flags a non-unitary user matrix.
A MemoryError is raised (instead of aborting the process) for
state-vector allocations that will not fit — for example
ExaqtSV.zero(40).
Next steps¶
- Circuit Execution — run whole
mimiqcircuitscircuits. - API Reference — the complete public surface.