Installation#
Quantuva targets Python 3.10 and later. Install from PyPI; the local simulators ship with the package, so there is no separate runtime to set up.
# install the SDK $ pip install quantuva # verify it imports and reports the simulator $ python -c "import quantuva; print(quantuva.__version__)"
Quickstart#
Build a GHZ state, transpile it, run it, and read the ⟨Z₀⊗Z₁⟩ stabilizer — with readout mitigation on.
from quantuva import Circuit, Observable, transpile # 1. build the circuit qc = Circuit(3) qc.h(0) qc.cx(0, 1) qc.cx(0, 2) qc.measure_all() # 2. compile against a target topology and gate set compiled = transpile(qc, target="superconducting-v2", opt_level=2) # 3. run — locally by default, hardware with a token obs = Observable("Z(0) @ Z(1)") result = compiled.run(shots=2048, mitigate="zne") # 4. read the calibrated expectation value print(result.expectation(obs))
That's the whole loop: build → compile → run → read. The circuit, the observable, and the mitigation are all explicit arguments — nothing is hidden in a default you didn't choose.
Circuit#
A Circuit holds qubits, gates, and measurements. Gates are named after the operations you'd write on paper — H, X, Y, Z, Rₓ, R_z, CX, SWAP — plus parameterized rotations for variational work.
qc = Circuit(4) qc.h(0) qc.rx(0.5, 1) qc.cx(0, 1) qc.cz(1, 2) qc.swap(2, 3) qc.measure_all() # the circuit knows its own depth and gate counts qc.depth() # -> int qc.num_qubits() # -> int
Parameterized circuits
Bind a circuit to parameters once and re-run it across a sweep without rebuilding. Useful for variational algorithms and for the noise-scaled circuits that ZNE needs.
qc = Circuit(2) qc.rx("θ", 0) qc.ry("φ", 1) qc.cx(0, 1) for θ, φ in [(0.1, 0.2), (0.3, 0.4)]: bound = qc.bind(θ=θ, φ=φ) bound.run(shots=1024)
Observable#
An Observable is a Pauli string over a subset of qubits. You declare what you mean to measure, and the SDK handles the basis rotations behind it.
obs = Observable("Z(0) @ Z(1)") # two-point correlator obs = Observable("X(0) + X(1) + X(2)") # a sum, measured term by term obs = Observable("Z(0) @ Z(1) @ Z(2)") # three-point, for GHZ stabilizers
Transpilation#
transpile rewrites a logical circuit for a physical backend: it decomposes multi-qubit gates into the native set, maps logical qubits onto the device topology, and inserts SWAP gates where the connectivity demands them. The output is a circuit the backend can actually schedule.
- gate decomposition — multi-controlled and arbitrary-angle gates become native single-qubit rotations and two-qubit gates.
- qubit mapping — logical qubits are placed on physical qubits to minimise the routing work that follows.
- SWAP routing — where the connectivity is sparse, SWAP gates move states so entangling gates can act on adjacent qubits.
- scheduling — gates are aligned to the backend's timing, not left as a bare dependency graph.
Simulation#
Three simulators, chosen by what you need to model. The default is a fast statevector simulator; add a noise model and you get the density-matrix path; shallow wide circuits can use the tensor-network backend.
# exact statevector result = compiled.run(shots=0) # 0 -> return the statevector # noisy density-matrix with a decoherence model result = compiled.run(noise="depolarizing(0.01)", shots=4096) # tensor-network path for shallow, wide circuits result = compiled.run(backend="mps", shots=2048)
Error mitigation#
Pass a mitigate argument to run. Each method trades classical sampling budget for a less biased answer, and each has regimes where it stops helping — see the lab notes for where.
# zero-noise extrapolation result = compiled.run(shots=2048, mitigate="zne") # Pauli twirling before sampling result = compiled.run(shots=2048, mitigate="twirl") # readout correction from a calibration matrix result = compiled.run(shots=2048, mitigate="readout") # or compose them result = compiled.run(shots=2048, mitigate=["twirl", "zne"])
Backends#
By default everything runs locally. To submit to hardware, pass a target name and a token; the same compiled circuit is serialized and scheduled on the partner device.
compiled = transpile(qc, target="superconducting-v2") result = compiled.run(shots=2048, token="...")
Supported target families: superconducting and trapped-ion backends, plus the local simulators. Each target advertises its native gate set and connectivity, so transpilation can be genuinely backend-aware rather than a fixed rewrite.