Skip to content

Documentation

Quantum Lab

Circuits, Hamiltonians, lattices and your own Python, run on QVArena compute nodes with a reproducibility certificate on every result.

What is Quantum Lab

Quantum Lab runs quantum simulations on QVArena compute nodes. Hand it an OpenQASM circuit, a Hamiltonian, a lattice, or a Python program; it is queued, picked up by a node sized to the job, and the result comes back with a certificate recording exactly how it was produced.

A submitted program runs inside a container with no network, a read-only filesystem, and a capped share of one node — it cannot reach the platform or another user's work.

Simulation methods

Choose a method explicitly, or leave it on auto and the node picks one for you.

  • Statevector — exact simulation, up to about 34 qubits.
  • Matrix Product State (MPS) — approximate simulation that scales with entanglement rather than qubit count; ideal for shallow circuits such as QAOA, reaching 100+ qubits.
  • Auto — statevector when the circuit fits a node's memory, otherwise matrix product state.

Works with any framework

The service accepts OpenQASM 2 and OpenQASM 3, which every major framework can export. Write your circuit in the tool you already use, export QASM, and submit it.

# Qiskit
from qiskit.qasm2 import dumps
qasm = dumps(circuit)

# Cirq
qasm = cirq.qasm(circuit)

# PennyLane
qasm = circuit.qtape.to_openqasm()

Python client quickstart

The client accepts a Qiskit circuit or a raw QASM string. Create an API token on the API tokens page, then submit and poll for the result.

from qvarena_lab import Sampler

sampler = Sampler(
    token="<access token>",
    base_url="https://arena-api.qvillager.com",
)
job = sampler.run(circuit, shots=4096)
print(job.result()["counts"])

Estimator: expectation values

Beyond sampling, you can request expectation values of weighted Pauli observables for a circuit — the primitive variational workflows such as VQE and QAOA are built on. Each observable is a Pauli string over I, X, Y, Z (qubit 0 first, leftmost) whose length equals the circuit's qubit count, with a coefficient.

Without noise, estimation uses an exact statevector and carries no shot noise; under a noise model it is an exact density-matrix expectation, and with mitigation a zero-noise-extrapolated (approximate) estimate.

from qvarena_lab import Estimator

estimator = Estimator(
    token="<access token>",
    base_url="https://arena-api.qvillager.com",
)
job = estimator.run(circuit, observables=[
    {"pauli": "ZZ", "coeff": 1.0},
    {"pauli": "IX", "coeff": 0.5},
])
print(job.result()["total"])

Optimizer: variational minimization

The estimator says what an observable is worth on a fixed circuit. The optimizer answers the question VQE and QAOA actually ask — which parameters minimize it — and runs the whole loop on a compute node, so nothing has to stay open on your side across hundreds of evaluations.

Write free parameters into the circuit as theta[0], theta[1], … with indices running consecutively from 0; the worker substitutes values before each evaluation.

Five optimizers are available. COBYLA and Nelder-Mead are derivative-free, SPSA takes two evaluations per step regardless of width, L-BFGS-B uses gradients, and rotosolve solves each coordinate in closed form.

Two of them can be exact rather than approximate, under the same condition: every parameter appearing exactly once as the whole argument of a rotation. Then L-BFGS-B uses the two-term parameter-shift rule, which is the same derivative real hardware would report, and each coordinate slice is a single sinusoid so rotosolve locates that coordinate's exact minimum in three evaluations. Without that condition L-BFGS-B falls back to central differences and rotosolve is refused rather than approximated.

None of the five wins everywhere: COBYLA is the most reliable default, and rotosolve needs no tuning and holds up better on wide ansaetze where the gradient method stalls.

POST /api/lab/optimize
Authorization: Bearer <token>
Content-Type: application/json

{
  "qasm": "OPENQASM 2.0; ... ry(theta[0]) q[0]; ry(theta[1]) q[1]; cx q[0],q[1];",
  "observables": [
    { "pauli": "ZI", "coeff": 0.5 },
    { "pauli": "IZ", "coeff": 0.5 },
    { "pauli": "XX", "coeff": 0.2 }
  ],
  "optimizer": "l-bfgs-b",
  "max_iterations": 150
}

What a variational result carries

Beyond the optimum and the full convergence trace: up to 10 qubits the Hamiltonian is diagonalized directly, so the exact minimum, its degeneracy, and the overlap with the ground eigenspace sit beside the value that was found - a variational number means little without the answer it was chasing. The gradient norm at the reported optimum says whether the run reached a genuine stationary point or simply ran out of budget, which the energy alone cannot distinguish.

Asking for plateau samples additionally measures the gradient at random points and reports its variance: the barren-plateau measurement, reported as a number rather than a verdict, since a plateau is a statement about how that variance scales with width.

Hamiltonian time evolution

Sampling asks what a fixed circuit outputs; estimation what an observable is worth on it; optimization which parameters minimize one. None answer the question most of physics and chemistry actually asks — given this Hamiltonian, what does the system do over time?

Read the accuracy block first. exp(-iHt) has no exact circuit for a general H, so the trajectory is an approximation whose error is invisible in the output. Where the system is small enough the exact trajectory is computed alongside and the difference reported; above that size the error is unknown rather than small.

from qvarena_lab import Evolver, transverse_field_ising

evolver = Evolver(token="qvl_...")
hamiltonian = transverse_field_ising(4, coupling=1.0, field=1.0)

job = evolver.run(
    hamiltonian,
    observables=[{"pauli": "ZIII", "coeff": 1.0}],
    total_time=5.0,
    initial_state="neel",
)
result = job.result()

# Read this first: it says how far the trajectory can be trusted.
print(result["accuracy"])
# {'reference': 'exact', 'lowest_fidelity': 0.9999906,
#  'worst_observable_error': 0.0018, 'error_scaling': 'O(t * dt^2)',
#  'suggested_steps_for_10x': 560}

for row in result["trajectory"][:3]:
    print(row["time"], row["values"], row.get("exact_values"))

Analysing a circuit

Five modes that describe what a circuit is rather than what it emits: its matrix, its channel with noise composed in, how its qubits are correlated, the state after every gate, and Clifford+T sampling whose cost tracks non-Clifford count instead of qubit count.

The step trace is the one to reach for when a histogram has not made the circuit clear; the entanglement mode is the one that can tell GHZ from a Bell pair, which the single-qubit numbers cannot.

from qvarena_lab import Analyzer
from qiskit import QuantumCircuit

circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 1)
circuit.cx(1, 2)

analyzer = Analyzer(token="qvl_...")

# Where did the entanglement come from?
trace = analyzer.run(circuit, mode="step_trace").result()
print(trace["entangling_step"])          # 2 — the first cx
for row in trace["trace"]:
    if "entropies" in row:
        print(row["gate"], row["entropies"])

# In GHZ every qubit is maximally mixed, exactly as in a Bell pair — and no
# pair is entangled on its own. Only the pairwise numbers can say that.
ent = analyzer.run(circuit, mode="entanglement").result()
print([s["entropy"] for s in ent["single_qubit"]])   # all ln 2 = 0.6931
print([p["negativity"] for p in ent["pairs"]])       # all 0

REST API

Every request is authenticated with your access token as a Bearer header. Responses use the standard { success, data } envelope.

Submit a circuit. Returns the job id, status, and queue position.

POST /api/lab/jobs
Authorization: Bearer <token>
Content-Type: application/json

{ "qasm": "OPENQASM 2.0; ...", "shots": 4096, "method": "auto" }

Evolve a state under a Hamiltonian and report observables over time.

POST /api/lab/evolve
Authorization: Bearer <token>
Content-Type: application/json

{
  "hamiltonian": [
    { "pauli": "ZZI", "coeff": -1.0 },
    { "pauli": "IZZ", "coeff": -1.0 },
    { "pauli": "XII", "coeff": -1.0 },
    { "pauli": "IXI", "coeff": -1.0 },
    { "pauli": "IIX", "coeff": -1.0 }
  ],
  "observables": [{ "pauli": "ZII", "coeff": 1.0 }],
  "qubits": 3,
  "total_time": 5.0,
  "steps": 400,
  "order": 2,
  "initial_state": "neel"
}

Compute one of the analysis modes over a circuit.

POST /api/lab/analyze
Authorization: Bearer <token>
Content-Type: application/json

{
  "qasm": "OPENQASM 2.0; include \"qelib1.inc\"; qreg q[2]; h q[0]; cx q[0],q[1];",
  "mode": "step_trace"
}

List your own jobs, newest first.

GET /api/lab/jobs
Authorization: Bearer <token>

Fetch one job, including its result and certificate once complete.

GET /api/lab/jobs/<id>
Authorization: Bearer <token>

Permanently delete one of your finished jobs (completed, failed, or cancelled). Active jobs must be cancelled first.

DELETE /api/lab/jobs/<id>
Authorization: Bearer <token>

Submit your own Python. The source runs inside a container; stdout comes back as the run's log, and JSON written to LAB_RESULT_PATH comes back as its result.

POST /api/lab/script
Authorization: Bearer <token>
Content-Type: application/json

{
  "source": "import numpy, json, os\nprint(numpy.__version__)",
  "timeout_seconds": 300,
  "memory_mb": 4096,
  "cpus": 2
}

Every job type. The request shape is the same for all of them — what differs is the parameters, which are on each type's own page.

POST /api/lab/scriptRun a Python program in a sandboxed container
POST /api/lab/jobsSample a circuit and count outcomes
POST /api/lab/estimateExpectation values of Pauli observables
POST /api/lab/optimizeVariational loop over circuit parameters
POST /api/lab/analyzeTake a circuit apart: unitary, channel, entanglement
POST /api/lab/evolveTime evolution under a Hamiltonian
POST /api/lab/scheduleA Hamiltonian that changes with time; Floquet
POST /api/lab/lindbladOpen systems: relaxation, jumps, emission, Redfield
POST /api/lab/spectrumThermal averages, level statistics, OTOC, S(k,w)
POST /api/lab/mpsGround states of long chains by DMRG
POST /api/lab/walkA particle on a graph; spatial search
POST /api/lab/bosonicFock space: phase space, evolution, photon counts
POST /api/lab/fermionicFermions to qubits: Jordan-Wigner and friends
POST /api/lab/tomographyReconstruct a state, a process, or classical shadows
POST /api/lab/qecError-correcting codes: logical error rate and threshold
POST /api/lab/controlPulse shapes that produce a target gate
GET /api/lab/jobsList your runs, newest first
GET /api/lab/jobs/<id>One run, with its result and certificate
GET /api/lab/nodesCompute nodes, capacity and queue depth
GET /api/lab/quotaJobs used and remaining in the rolling window
GET /api/lab/job-typesWhat this deployment accepts, rather than what a client remembers
GET /api/lab/usageCompute time spent, by day
GET /api/lab/public/<id>A shared result, with no token — the only route here that takes none
DELETE /api/lab/jobs/<id>Delete a finished run

Limits

Circuits may use up to 36 qubits and up to 100,000 shots. Each account may submit up to 50 jobs per rolling 24 hours. Circuits too large for statevector simulation must request the matrix product state method.

Reproducibility certificate

Every completed job carries a certificate: the SHA-256 of the submitted QASM, the method used, the random seed, the shot count, the exact Qiskit and Aer versions, the compute node, and the start and finish timestamps. Anyone with the same circuit and seed can reproduce and verify the result.