Run your first quantum circuit in under two minutes. No account, no credit card, no hardware required.
One POST request returns a key immediately. The key is displayed once — save it somewhere safe.
# cURL
curl -s -X POST https://api.qumulator.com/keys \
-H "Content-Type: application/json" \
-d '{"name": "my-first-key"}'
# Response
{
"key": "qum_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "my-first-key",
"created_at": "2026-04-20T12:00:00Z"
}
pip install qumulator-sdk
from qumulator import QumulatorClient
client = QumulatorClient(
api_url="https://api.qumulator.com",
api_key="qum_xxxxxxxx...",
)
# Build and run a 2-qubit Bell state
eng = client.circuit.engine(n_qubits=2)
eng.apply('h', 0).apply('cx', [0, 1])
counts = eng.sample(shots=1024)
print(counts) # {'00': ~512, '11': ~512}
QUMULATOR_API_KEY and read it with
os.environ["QUMULATOR_API_KEY"] to keep it out of source code.
Every request to the API (except POST /keys) must include your API key in the
X-API-Key header.
curl https://api.qumulator.com/circuits \
-H "X-API-Key: qum_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
The Python SDK handles this automatically once you pass api_key to
QumulatorClient.
POST /keys. There is no limit on how many keys you can create.
| Property | Value |
|---|---|
| Base URL | https://api.qumulator.com |
| Protocol | HTTPS only |
| Request format | JSON — Content-Type: application/json |
| Interactive docs | api.qumulator.com/docs (Swagger UI) |
The API is currently unversioned (no /v1/ prefix). Breaking changes will be
announced in advance.
pip install qumulator-sdk
Requires Python 3.10 or later. The core package has a single dependency: httpx.
Framework adapters (Qiskit, Cirq) require those packages installed separately.
# With Qiskit adapter
pip install qumulator-sdk qiskit
# With Cirq adapter
pip install qumulator-sdk cirq
QumulatorClient is the entry point for all computation types.
from qumulator import QumulatorClient
client = QumulatorClient(
api_url="https://api.qumulator.com",
api_key="qum_...",
)
| Attribute | Type | Purpose |
|---|---|---|
| client.circuit | CircuitClient | Quantum circuit simulation (gate builder, QASM, Hamiltonian evolution) |
| client.klt | KLTClient | Spin ground states and Pauli Hamiltonian energy |
| client.hafnian | HafnianClient | Photonic amplitudes (hafnian, permanent, GBS) |
| client.homo | HomoClient | Molecular HOMO/LUMO frontier orbital energies |
| client.notebook | NotebookClient | Remote Jupyter notebook execution |
There are two ways to construct a circuit: the fluent gate builder and the direct gate-list API. Both are equivalent and produce identical results. No simulation runs locally — all execution is server-side.
Obtain a CircuitEngine from client.circuit.engine(),
accumulate gates with .apply(), then submit with
.run() or .sample().
eng = client.circuit.engine(n_qubits=3)
eng.apply('h', 0 ) # Hadamard on qubit 0
.apply('cx', [0, 1] ) # CNOT: control 0, target 1
.apply('cx', [0, 2] ) # CNOT: control 0, target 2
.apply('rz', 2, params=[1.5708]) # Rz(pi/2) on qubit 2
result = eng.run(
shots=4096,
return_statevector=True,
return_entropy_map=True,
)
print(result.counts) # {'000': ~2048, '111': ~2048}
print(result.entropy_map) # [1.0, 0.0, 0.0] — qubit 0 maximally entangled
print(result.statevector) # complex ndarray, length 8
result = client.circuit.run(
gates=[
('h', 0),
('cx', [0, 1]),
],
n_qubits=2,
shots=2048,
)
| Gate(s) | Qubits | Params | Description |
|---|---|---|---|
| h, x, y, z | 1 | — | Pauli and Hadamard |
| s, t, sdg, tdg | 1 | — | Phase / T gates and their adjoints |
| rx, ry, rz | 1 | [θ] | Rotation gates; angle in radians |
| u | 1 | [θ, φ, λ] | General single-qubit unitary (IBM U gate) |
| cx, cnot | 2 | — | CNOT (control, target) |
| cz | 2 | — | Controlled-Z |
| swap | 2 | — | SWAP |
| iswap | 2 | — | iSWAP |
| fsim | 2 | [θ, φ] | fSim gate (Google Sycamore / Willow family) |
| syc | 2 | — | Sycamore gate |
| ecr | 2 | — | Echoed cross-resonance (IBM native gate) |
| ccx, toffoli | 3 | — | Toffoli (CCNOT) |
| cswap, fredkin | 3 | — | Fredkin |
| unitary | 1–N | matrix | Arbitrary unitary; pass a 2N×2N complex matrix via params |
eng.apply('cx', [0, 1]).
Bitstrings in results are MSB-first: qubit 0 is the leftmost character.
| Parameter | Type | Description | |
|---|---|---|---|
| n_qubits | int | required | Number of qubits in the register |
| mode | str | Simulation mode. Default: "auto". See Simulation
Modes. |
|
| bond_dim | int | Bond dimension cap for "compressed" and "tensor"
modes |
| Parameter | Type | Default | Description |
|---|---|---|---|
| shots | int | 1024 | Measurement samples (1 to 1 000 000) |
| seed | int | None | RNG seed for reproducible sampling |
| return_statevector | bool | False | Include complex amplitude vector (requires N ≤ 25) |
| return_probabilities | bool | False | Include probability vector (requires N ≤ 25) |
| return_entropy_map | bool | False | Include per-qubit entanglement entropy values |
Both OpenQASM 2 and OpenQASM 3 source strings are accepted. Pass the source to
client.circuit.run_qasm().
qasm_source = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[3] c;
h q[0];
cx q[0], q[1];
cx q[0], q[2];
measure q -> c;
"""
result = client.circuit.run_qasm(
qasm=qasm_source,
shots=2048,
mode="exact",
return_entropy_map=True,
)
print(result.counts) # {'000': ~1024, '111': ~1024}
stdgates.inc,
qelib1.inc, and custom gate definitions are all supported.
For gates with no standard QASM name (e.g. the SYC gate in Google Willow circuits), use the
instructions input with an explicit matrix:
import numpy as np
# Arbitrary 2-qubit unitary (e.g. Google SYC gate)
my_gate = np.array([
[1, 0, 0, 0 ],
[0, 0, -1j, 0 ],
[0, -1j, 0, 0 ],
[0, 0, 0, np.exp(1j * np.pi / 6)],
])
eng = client.circuit.engine(n_qubits=4)
eng.apply('unitary', [0, 1], params=my_gate.tolist())
.apply('unitary', [2, 3], params=my_gate.tolist())
result = eng.run(shots=1024)
Pass mode= to engine() or
run(). The default "auto" lets the server
select the optimal backend for your circuit.
| Mode | Best for | N limit |
|---|---|---|
| "auto" | General use. Server selects the backend based on circuit size and structure. | ≤ 1000 |
| "exact" | Small circuits requiring full amplitude precision. Statevector and probability arrays available. | ≤ 25 |
| "compressed" | Structured circuits with moderate entanglement — VQE, QAOA, chemistry ansätze, shallow random circuits. | ≤ 1000 |
| "tensor" | Circuits with a 1D or near-1D connectivity pattern. | ≤ 1000 |
| "hamiltonian" | Direct time evolution under a Pauli-string Hamiltonian. Use with evolve_hamiltonian(). |
≤ 1000 |
| "gaussian" | Clifford-heavy circuits. Returns a GaussianCertificate classifying non-Clifford content. Memory scales as O(N²). | ≤ 1000 |
Use mode="hamiltonian" and call evolve_hamiltonian()
to apply e−iHt directly from a Pauli-string Hamiltonian, without
requiring a gate decomposition.
eng = client.circuit.engine(n_qubits=4, mode="hamiltonian")
eng.apply('h', 0).apply('h', 1) # initial state preparation
eng.evolve_hamiltonian(
pauli_terms=[
( 0.5, 'ZZII'), # ZZ coupling on sites 0-1
( 0.5, 'IZZI'), # ZZ coupling on sites 1-2
(-1.0, 'XIIX'), # transverse field
],
t=1.0, # evolution time
)
result = eng.run(shots=2048, return_entropy_map=True)
Pauli strings use I X Y Z per qubit; the leftmost character is qubit 0.
eng = client.circuit.engine(n_qubits=20, mode="gaussian")
for i in range(20):
eng.apply('h', i)
for i in range(0, 19, 2):
eng.apply('cz', [i, i+1])
eng.apply('t', 5) # non-Clifford gate
result = eng.run(shots=1024)
cert = result.gaussian_certificate
print(cert.rcs_certificate) # "LIKELY_GAUSSIAN"
print(cert.entanglement_regime) # "area_law"
print(cert.gaussian_fidelity) # 0.991
print(cert.wigner_negativity_estimate) # small positive float
| Certificate label | Meaning |
|---|---|
| GAUSSIAN_SIMULABLE | Purely Clifford circuit; Gaussian approximation is exact. |
| LIKELY_GAUSSIAN | Non-Clifford content is small; high-fidelity approximation. |
| NON_GAUSSIAN_CORRECTION_NEEDED | Substantial non-Clifford content; switch to "exact" or "compressed" for full accuracy. |
QumulatorBackend is a drop-in replacement for any Qiskit simulator backend.
Build your circuit in Qiskit as normal; the adapter transpiles and runs it on Qumulator.
from qumulator import QumulatorClient
from qumulator.backends.qiskit_backend import QumulatorBackend
from qiskit import QuantumCircuit
client = QumulatorClient(api_url="https://api.qumulator.com", api_key="qum_...")
backend = QumulatorBackend(client)
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
job = backend.run(qc, shots=2048)
result = job.result()
counts = result.get_counts()
print(counts) # {'00': ~1024, '11': ~1024}
get_counts() returns Qiskit-standard keys.
No changes to your existing code are needed.
# compressed mode for a large variational circuit
backend = QumulatorBackend(client, mode="compressed")
job = backend.run(qc, shots=8192)
Any Qiskit circuit can be sent to Qumulator. The adapter automatically transpiles to the supported
gate set. Gates that expose gate.to_matrix() are submitted as arbitrary
unitaries and simulated exactly.
# Run your parameterised ansatz on Qumulator instead of a local simulator
bound_circuit = ansatz.assign_parameters(theta_values)
bound_circuit.measure_all()
job = backend.run(bound_circuit, shots=4096)
counts = job.result().get_counts()
expectation_value = compute_energy(counts, hamiltonian)
QumulatorSimulator implements Cirq's Simulator
interface and supports both sampling and statevector simulation.
import cirq
from qumulator import QumulatorClient
from qumulator.backends.cirq_simulator import QumulatorSimulator
client = QumulatorClient(api_url="https://api.qumulator.com", api_key="qum_...")
sim = QumulatorSimulator(client)
q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
cirq.H(q0),
cirq.CNOT(q0, q1),
cirq.measure(q0, q1, key='result'),
)
# Sampling
result = sim.run(circuit, repetitions=1024)
print(result.histogram(key='result')) # Counter({0: ~512, 3: ~512})
# Statevector
sv = sim.simulate(circuit)
print(sv.final_state_vector)
# array([0.707+0j, 0+0j, 0+0j, 0.707+0j])
cirq.unitary(), so any gate with a
unitary representation is supported — including custom operations and Sycamore-family gates.
q = cirq.LineQubit.range(4)
circuit = cirq.Circuit(
[cirq.H(qi) for qi in q],
cirq.SYC(q[0], q[1]),
cirq.SYC(q[2], q[3]),
[cirq.measure(qi, key=str(i)) for i, qi in enumerate(q)],
)
result = sim.run(circuit, repetitions=2048)
Compute ground-state energies for interacting spin systems or arbitrary Pauli-string Hamiltonians. Returns the minimum energy configuration plus a per-site entanglement entropy map.
Pass a dictionary of Pauli-string → coefficient entries. Each key is an N-character string
over {I, X, Y, Z}, one character per site.
# H2 molecule — STO-3G, Jordan-Wigner mapping (2 sites)
result = client.klt.run(
pauli_hamiltonian={
"II": -1.8572750, # constant / nuclear repulsion
"ZI": -0.3979374,
"IZ": -0.3979374,
"ZZ": 0.3980202,
"XX": 0.1809270,
"YY": 0.1809270,
},
)
print(result.energy) # ground-state energy in Hartree
print(result.entropy_list) # per-site entanglement entropy
import numpy as np
N = 16
J = np.random.randn(N, N)
J = (J + J.T) / 2 # symmetrize
np.fill_diagonal(J, 0)
result = client.klt.run(interaction_matrix=J.tolist())
print(result.energy) # ground-state energy
print(result.states) # per-site spin expectation values
print(result.max_S) # max bipartite entanglement entropy
| Field | Type | Description |
|---|---|---|
| energy | float | Ground-state energy |
| states | list[float] | Per-site expectation values |
| entropy_list | list[float] | Per-site entanglement entropy |
| max_S | float | Maximum bipartite entanglement entropy |
| mean_S | float | Mean entanglement entropy across all bipartitions |
Compute the hafnian, permanent, or Gaussian boson sampling amplitudes from a symmetric coupling matrix. The result is a complex number computed to configurable precision.
import numpy as np
n = 8
A = np.random.randn(n, n)
A = (A + A.T) / 2
np.fill_diagonal(A, 0)
result = client.hafnian.run(matrix_real=A.tolist())
print(result.value) # complex: haf_real + 1j*haf_imag
print(result.elapsed) # server compute time in seconds
# Complex coupling matrix, 50 decimal digits of precision
result = client.hafnian.run(
matrix_real=A_real.tolist(),
matrix_imag=A_imag.tolist(),
mp_dps=50, # default: 34
)
| Field | Type | Description |
|---|---|---|
| haf_real | float | Real part of the hafnian |
| haf_imag | float | Imaginary part |
| value | complex | Property: haf_real + 1j*haf_imag |
| elapsed | float | Server-side compute time (seconds) |
| max_S | float | Maximum coupling entropy |
| n_edges | int | Number of non-trivial coupling pairs |
| est_matchings | float | Estimated number of perfect matchings |
Compute HOMO and LUMO frontier orbital energies from a SMILES string. Results include the HOMO–LUMO gap and per-atom orbital density weights.
# Resveratrol
result = client.homo.run("Oc1ccc(/C=C/c2cc(O)cc(O)c2)cc1")
print(result.homo_E_eV) # e.g. -5.88 eV
print(result.lumo_E_eV) # e.g. -1.40 eV
print(result.gap_eV) # HOMO-LUMO gap in eV
print(result.heavy_symbols) # ['C', 'C', ..., 'O', 'O']
print(result.homo_density) # per-heavy-atom HOMO weight
# Use a larger basis set and a different functional
result = client.homo.run(
"c1ccccc1", # benzene
basis="6-31g",
xc="pbe0",
)
| Field | Type | Description |
|---|---|---|
| homo_E_eV | float | HOMO energy in eV |
| lumo_E_eV | float | LUMO energy in eV |
| gap_eV | float | HOMO–LUMO gap in eV |
| homo_density | list[float] | Per-heavy-atom HOMO orbital density |
| lumo_density | list[float] | Per-heavy-atom LUMO orbital density |
| heavy_symbols | list[str] | Element symbol for each heavy atom |
| n_occ | int | Number of occupied orbitals |
| basis | str | Basis set used (default: sto-3g) |
| xc | str | Exchange-correlation functional (default: b3lyp) |
Submit a Jupyter notebook (.ipynb) for remote execution. The notebook runs
in an isolated sandbox environment with qumulator-sdk and the full scientific
Python stack pre-installed.
# Submit and wait for completion
with open("my_experiment.ipynb", "rb") as f:
nb_bytes = f.read()
status = client.notebook.run(nb_bytes)
print(status.status) # "completed"
# status.result contains the executed notebook with outputs
import time
with open("my_experiment.ipynb", "rb") as f:
job_id = client.notebook.submit(f.read())
print("Job submitted:", job_id)
while True:
status = client.notebook.status(job_id)
if status.is_done:
break
time.sleep(3)
if status.ok:
executed_notebook = status.result
else:
print("Failed:", status.error)
numpy,
scipy, matplotlib,
pandas, qiskit,
cirq, and qumulator-sdk.
Additional packages can be installed at the top of your notebook with
!pip install ....
No authentication required.
| Field | Type | Description |
|---|---|---|
| name | string | Human-readable label |
# Response 201
{
"key": "qum_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "my-key",
"created_at": "2026-04-20T12:00:00Z"
}
Returns job_id immediately. Simulation runs asynchronously.
| Field | Type | Description | |
|---|---|---|---|
| qasm | string | OpenQASM 2 or 3 source. Provide either qasm or instructions. |
|
| instructions | array | JSON gate instruction list. Requires n_qubits. |
|
| n_qubits | int | Register width. Required when using instructions. |
|
| shots | int | Measurement samples. Default: 1024. Max: 1 000 000. | |
| mode | string | Simulation mode. Default: statevector. |
|
| seed | int | RNG seed for reproducible sampling. | |
| return_statevector | bool | Include complex amplitude vector in result. | |
| return_entropy_map | bool | Include per-qubit entropy values in result. | |
| bond_dim | int | Bond dimension cap for tensor-network modes. |
# Submit via cURL
curl -X POST https://api.qumulator.com/circuits \
-H "X-API-Key: qum_..." \
-H "Content-Type: application/json" \
-d '{
"qasm": "OPENQASM 3.0; include \"stdgates.inc\"; qubit[2] q; h q[0]; cx q[0],q[1]; bit[2] c; measure q->c;",
"shots": 1024,
"mode": "statevector"
}'
# Response 202
{ "job_id": "01HX8M7PVKTA..." }
Returns the most recent circuit jobs for your API key.
All simulation endpoints return a job_id immediately. Poll until
status is "completed" or
"failed".
# Poll a job
curl https://api.qumulator.com/circuits/01HX8M7PVKTA \
-H "X-API-Key: qum_..."
# While running
{ "job_id": "01HX...", "status": "running", "result": null }
# When complete
{
"job_id": "01HX...",
"status": "completed",
"result": {
"counts": { "00": 512, "11": 512 },
"n_qubits": 2,
"shots": 1024,
"entropy_map": [1.0, 1.0]
}
}
| Status | Meaning |
|---|---|
| queued | Accepted and waiting for a worker |
| running | Simulation in progress |
| completed | Result available in result field |
| failed | Error message in error field |
| Field | Type | Description |
|---|---|---|
| counts | dict[str, int] | Measurement outcome counts. Keys are MSB-first bitstrings (qubit 0 leftmost). |
| n_qubits | int | Register width |
| shots | int | Total measurement samples |
| statevector | ndarray|None | Complex amplitude vector of length 2N. Populated when return_statevector=True. |
| probabilities | ndarray|None | Probability vector. Populated when return_probabilities=True. |
| entropy_map | list[float]|None | Per-qubit entanglement entropy in bits. 1.0 = maximally entangled; 0.0 = product state. |
| gaussian_certificate | GaussianCertificate|None | Populated when mode="gaussian". |
| most_probable | str (property) | Bitstring with the highest count |
| Field | Type | Description |
|---|---|---|
| rcs_certificate | str | GAUSSIAN_SIMULABLE | LIKELY_GAUSSIAN | NON_GAUSSIAN_CORRECTION_NEEDED |
| entanglement_regime | str|None | area_law | transitional | volume_law |
| wigner_negativity_estimate | float|None | Estimated non-Clifford correction from T-gate content |
| gaussian_fidelity | float|None | Estimated fidelity of the Gaussian approximation (0–1) |
| xeb_lower_bound | float|None | Cross-entropy benchmark lower bound |
| Field | Type | Description |
|---|---|---|
| job_id | str | Job identifier |
| status | str | queued | running | completed | failed |
| result | dict|None | Engine result when completed |
| error | str|None | Error message when failed |
| is_done | bool | Property: True when completed or failed |
| ok | bool | Property: True when completed |
Circuit depth, not qubit count, determines memory. Requests exceeding a tier limit return HTTP 422 with a self-documenting error message.
| Tier | Qubit range | Max depth | Peak memory | Notes |
|---|---|---|---|---|
| 1 | 1–20 | 20 layers | ~335 MB | Full-depth VQE, QAOA, variational ansätze at any structure. |
| 2 | 21–54 | 6 layers | ~3.5 MB | Covers IBM Eagle (27 q) and Osprey (54 q) layouts. |
| 3 | 55–105 | 5 layers | ~1.7 MB | Covers Willow-scale (105 q) near-term superconducting layouts. |
| 4 | 106–1000 | 3 layers | < 1 MB | A 1000-qubit depth-3 circuit uses less memory than a JPEG image. |
| N > 1000 | Rejected (HTTP 422). Contact us for large-scale access. | |||
"exact" mode is limited to N ≤ 25. All modes follow the tier
depth limits above.
| Limit | Value |
|---|---|
| Requests per key per minute | 60 |
| Window type | Sliding (per-second resolution) |
| HTTP status when exceeded | 429 with Retry-After header |
The Retry-After header tells you exactly how many seconds to wait.
The SDK surfaces this as QumulatorHTTPError(status_code=429).
from qumulator import QumulatorHTTPError
import time
try:
result = eng.run(shots=1024)
except QumulatorHTTPError as e:
if e.status_code == 429:
retry_after = int(e.response.headers.get("Retry-After", 10))
time.sleep(retry_after)
result = eng.run(shots=1024) # retry once
All error responses use the shape {"detail": "...message..."}.
| Status | Meaning |
|---|---|
| 400 | Malformed JSON or missing required field. |
| 401 | Missing or invalid X-API-Key header. |
| 422 | Validation error — circuit exceeds a tier limit, unknown gate, or unsupported mode. The response body includes a self-documenting message with the full tier table. |
| 429 | Rate limit exceeded. Check the Retry-After header. |
| 500 | Internal server error. Retry after a moment; contact us if it persists. |
# Example 422 response (circuit exceeds tier limit)
{
"detail": "Limit Exceeded: 30 qubits at depth 15. Tier 2 (21-54 q) max depth is 6.
Tiers: T1(1-20q,d≤20) T2(21-54q,d≤6) T3(55-105q,d≤5) T4(106-1000q,d≤3)"
}