Skip to content

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) — rustup with the stable toolchain is sufficient
  • maturin to 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:

import exaqt
print(exaqt.ExaqtSV.zero(2).amplitudes())   # [1+0j, 0j, 0j, 0j]

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 the unitary-checks feature; 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