SDK Documentation
Everything runs through one Python package. Write circuits in Qiskit, Cirq, PennyLane, pyQuil, or Braket, and we convert them for you; we handle where and how they execute.
1. Install
pip install qsim-sdk
2. Get a token
Create an account at app.zksf.org. Your API token is on the dashboard.
3. Run your first circuit
import qsim_sdk
from qiskit import QuantumCircuit
qc = QuantumCircuit(60) # yes, sixty qubits
qc.h(0)
for i in range(59):
qc.cx(i, i + 1)
client = qsim_sdk.Client(token="YOUR_TOKEN")
job = client.run(qc, shots=1000)
print(job["result"]["counts"]) # the answer
print(job["result"]["error_info"]) # how much to trust itThe router inspected your circuit, saw it was Clifford, and ran it with a stabilizer engine: exactly, in milliseconds, for a fraction of a cent. A 60-qubit statevector would have needed 16 million terabytes of RAM.
4. Estimate before you spend
est = client.estimate(qc)
# {'engine': 'mps.quimb.cpu', 'predicted_seconds': 5.1,
# 'predicted_cost_usd': 0.00014, 'reason': '60 qubits, low
# entangling density -> MPS attempt with convergence check'}Estimates are free, instant, and deliberately conservative. No job runs without you knowing the ceiling.
5. Understand the error certificate (ZCC-v0.1)
Every approximate result carries a ZCC-v0.1 statement of how far it can be from the truth. By default it is a fast convergence check; pass certified=True for a rigorous, single-run bound.
job["result"]["error_info"]
# {'method': 'MPS (quimb)', 'max_bond': 64,
# 'convergence_deviation': 0.0, 'converged': True}client.run(qc, engine="mps.quimb.cpu", certified=True)
# error_info = {'protocol': 'ZCC-v0.1',
# 'method': 'MPS, rigorous discarded-weight bound',
# 'truncation_weight': 3.2e-08, # exact discarded SVD weight
# 'error_bound': 2.5e-04, # max per-outcome prob error
# 'certified': True}convergence_deviation: how much the top outcome probabilities moved when we re-ran at double the bond dimension. 0.0 means the approximation captured the state.certified=True: builds the state with renormalization off and reads the exact accumulated discarded SVD weight from the norm deficit. The per-outcome probability error is then rigorously bounded bysqrt(2 · truncation_weight), measured in a single run rather than extrapolated.- Pauli propagation reports the same protocol:
error_boundis the total discarded coefficient mass, which bounds the expectation-value error. - Exact engines (statevector, stabilizer) report
truncation_error: 0.0; only shot noise applies.
Any finished job can be exported as a downloadable PDF certificate with a public verification link, so a result you report can be independently checked by anyone. See the methodology write-up for the full protocol.
6. Understand the hardware certificate (ZHF-v0.1)
QPU results carry a different statement: not how close an approximation is to the truth, but how closely a real device’s measured counts track the exact ideal distribution. That is ZHF-v0.1.
client.run(qc, engine="qpu.ionq", shots=100)
# certificate = {'protocol': 'ZHF-v0.1',
# 'device': 'IonQ Forte-1',
# 'fidelity_mode': 'direct',
# 'fidelity': 0.9774}fidelity: the Hellinger fidelity between the measured hardware counts and the exact ideal distribution computed for the same circuit, in [0, 1]. 1.0 would mean the measured counts matched the ideal distribution exactly.fidelity_mode: "direct": the circuit was small enough (≤ 24 qubits) to also simulate exactly, so the fidelity is measured directly against that ideal reference, not asserted from a vendor specification.- Above that qubit count, direct verification is not yet available; the certificate says so honestly rather than reporting a number it cannot back up.
Like ZCC-v0.1, any finished QPU job can be exported as a downloadable PDF certificate with a public verification link. See the methodology write-up for both protocols.
7. Choose an engine explicitly (optional)
client.run(qc, engine="mps.quimb.cpu", max_bond=128) # force MPS client.run(qc, engine="exact.cpu") # force exact
| Engine | Best for | Limits |
|---|---|---|
| exact.cpu | Ground truth, any gate set | ≤ 30 qubits |
| exact.gpu | Exact results, GPU-accelerated | ≤ 32 qubits |
| clifford | Stabilizer/error-correction circuits | Clifford gates only, 5,000+ qubits |
| mps.quimb.cpu | QAOA, ansätze, structured circuits | entanglement-dependent, ~128 qubits; certified error bounds |
| mps.aer.cpu | Independent tensor-network cross-check | entanglement-dependent |
| pauli.cpu | Expectation values of large circuits | needs an observable; 100 to 200 qubits, no bitstring counts |
| noisy.cpu | Preview hardware noise before you pay | device-noise model; optional error mitigation (ZNE) |
| qpu.rigetti | Real superconducting hardware | Rigetti Cepheus 108 qubits; scheduled queue, physical noise |
| qpu.ionq | Real trapped-ion hardware | IonQ Forte-1 36 qubits, 100-5000 shots; scheduled queue |
8. Error mitigation (opt-in)
Any run that carries an observable can request zero-noise error mitigation. The circuit is evaluated at several amplified noise levels and the observable is extrapolated back to the zero-noise limit, which removes much of the modelled gate noise from the reported value. It applies to the noisy.cpu simulator and to the qpu.rigetti and qpu.ionq hardware engines. On hardware it submits three folded circuits, so it bills three times the single-run cost.
client.run(qc, engine="qpu.rigetti", shots=200,
observable=[[1.0, "ZZ"]], mitigate=True)
# error_info: { protocol: "ZCC-Estimate-v0.1",
# technique: "zero-noise extrapolation",
# raw_expectation: 0.81, mitigated_expectation: 0.82,
# error_bound: 5.0e-02, estimated: true, certified: false }The returned figure is a mitigated value with a ZCC-Estimate-v0.1 uncertainty: the larger of the shot noise propagated through the extrapolation and the disagreement between the linear, quadratic, and Richardson extrapolations of the same measured data. It is computed without reference to the true answer, so the same statement carries over to hardware. Because an extrapolated value cannot be given a hard mathematical bound the way the exact, stabilizer, and tensor-network paths can, certified reads false by design: the result is reported as a well-founded statistical estimate, not as a guarantee, and the exported certificate states this explicitly.
9. When we say no
qsim_sdk.JobRejected: intractable classically at this structure: 60 qubits, depth 99, 826 entangling gates (13.8/qubit). Options: reduce entangling density below ~8/qubit for MPS, restrict to Clifford gates, or shrink to <= 30 qubits for exact simulation.
Deep, wide, highly entangled circuits are intractable for every classical method, and beyond today’s noisy hardware too. We reject them with the reason and your options, instead of billing you for results that cannot be trusted.
10. Worked example: real test runs on every tier
Everything above, shown on real jobs: five runs across CPU, GPU, and both quantum processors, with the environment, the results, and what they cost. You can reproduce them from the console or the SDK.
Run A (CPU): the benchmark suite, on a laptop
Environment: an ordinary consumer laptop (Intel i7-12700H, 32 GB RAM), CPU only: no GPU, no cluster, no quantum hardware. Circuits were submitted with the engine field left blank, so the router chose the method each time.
| Circuit | Qubits | Depth | Engine picked | Wall time | Accuracy |
|---|---|---|---|---|---|
| GHZ (Clifford) | 5,000 | 5,000 | clifford | 0.56 s | exact |
| QAOA MaxCut, p=3 | 100 | 304 | mps.quimb.cpu | 5.9 s | converged (dev 0.0) |
| Layered ansatz | 80 | 9 | mps.quimb.cpu | 4.3 s | converged (dev 0.0) |
| Exact statevector | 26 | 9 | exact.cpu | 2.7 s | exact (ground truth) |
Reading the accuracy column: exact means the method makes no approximation at all, so only shot noise applies. Converged (dev 0.0) means the job was automatically re-run at double the bond dimension and the top outcome probabilities did not move; the compression captured the state. Every approximate job you run gets this same check, in its error_info.
Run B (GPU): 8-qubit GHZ, production cloud
Environment: our production GPU tier, submitted through the console exactly as any signed-in user would. The circuit is an 8-qubit GHZ state: one Hadamard, a chain of CNOTs, measure everything.
OPENQASM 2.0; include "qelib1.inc"; qreg q[8]; creg c[8]; h q[0]; cx q[0],q[1]; cx q[1],q[2]; cx q[2],q[3]; cx q[3],q[4]; cx q[4],q[5]; cx q[5],q[6]; cx q[6],q[7]; measure q -> c;
Steps: paste the circuit, set shots to 1,000, pick exact.gpu from the engine menu, run. The result:
counts: {"00000000": 502, "11111111": 498}
error_info: {"method": "exact statevector (GPU)",
"truncation_error": 0, "shot_noise_only": true,
"shots": 1000}
resources: {"device": "GPU", "wall_seconds": 0.51}A GHZ state should collapse to all-zeros or all-ones with equal probability, so the ideal split over 1,000 shots is 500/500; the measured 502/498 is textbook shot noise. Execution took half a second on the GPU. The first GPU job after a quiet period can add a short warm-up delay while a worker spins up; it is not billed.
Run C (GPU): 32 qubits, the memory wall
The GPU tier’s exact statevector reaches 32 qubits, the point where a 32 GB laptop runs out of memory. A 32-qubit GHZ circuit (100 shots) holds a full 64 GiB statevector on the GPU, well past what a consumer machine can allocate.
counts: {"00000000000000000000000000000000": 52,
"11111111111111111111111111111111": 48}
error_info: {"method": "exact statevector (GPU)",
"truncation_error": 0, "shot_noise_only": true,
"shots": 100}
resources: {"device": "GPU", "qubits": 32,
"statevector": "64 GiB", "wall_seconds": 8.2}52/48 across the two GHZ states is the expected 50/50 split within shot noise, computed exactly (no approximation, truncation error 0) in 8.2 seconds. This is the crossover the platform is built for: the same SDK line that ran on a laptop scales onto GPU memory a laptop does not have.
Run D (QPU): Rigetti superconducting hardware
The same experiment on a real quantum processor: a 3-qubit GHZ circuit, 50 shots, engine qpu.rigetti, submitted through the console. Hardware runs are scheduled on the physical device, so this one returned after roughly 53 minutes in the queue; you can close the page and the result attaches to the job. The result:
counts: {"000": 23, "111": 22,
"011": 3, "010": 1, "001": 1}
error_info: {"method": "quantum hardware (Rigetti)",
"device": "Cepheus-1-108Q",
"shot_noise_and_device_noise": true,
"shots": 50,
"note": "raw hardware counts; no error mitigation applied"}45 of 50 shots landed in the two GHZ states, and 5 shots show bit-flip errors from real device noise: the physical signature of superconducting qubits, reported exactly as measured. Comparing this against the simulator runs above is itself a useful experiment; the same circuit can also be submitted with zero-noise error mitigation (section 8) to return a mitigated observable with a stated uncertainty, at three times the single-run cost.
Run E (QPU): IonQ Forte-1 trapped-ion hardware
The same family of experiment on a different kind of quantum computer: a 2-qubit GHZ (Bell) circuit, 100 shots, engine qpu.ionq, on IonQ’s Forte-1 trapped-ion processor. Trapped-ion queues are scheduled the same way; this run returned after about 5 hours. The result:
counts: {"00": 54, "11": 44, "01": 2}
error_info: {"method": "quantum hardware (IonQ)",
"device": "Forte-1",
"shot_noise_and_device_noise": true,
"shots": 100,
"note": "raw hardware counts; no error mitigation applied"}98 of 100 shots landed in the two GHZ states (00 and 11), with just 2 shots of device noise, so about 98% of shots fell in the ideal outcomes. Trapped-ion qubits carry different noise characteristics from superconducting ones; running the identical circuit on both, next to the exact simulator, is exactly the kind of comparison the platform is meant to make easy.
Run F (CPU): how to reproduce a 192-qubit result for a tenth of a cent
Statevector simulation stops around 34 qubits because memory doubles with every qubit. Pauli propagation does not track the full state; it evolves the observable backward through the circuit, so its cost scales with circuit structure rather than qubit count. That reaches well past 100 qubits on an ordinary CPU. Here is a 192-qubit GHZ circuit with an all-Z observable, engine pauli.cpu, which you can run yourself from the SDK in three lines:
import qsim_sdk
from qiskit import QuantumCircuit
n = 192
qc = QuantumCircuit(n)
qc.h(0)
for i in range(n - 1):
qc.cx(i, i + 1) # a 192-qubit GHZ state
client = qsim_sdk.Client(token="YOUR_TOKEN")
job = client.run(
qc,
engine="pauli.cpu",
observable=[[1.0, "Z" * n]], # measure <Z...Z> across all 192 qubits
)
print(job["result"]["expectation"]) # 1.0
print(job["result"]["error_info"]) # certified, error_bound 0expectation: 1.0
error_info: {"protocol": "ZCC-v0.1",
"method": "Pauli propagation (Heisenberg picture)",
"truncated_coefficient_mass": 0, "error_bound": 0,
"certified": true, "converged": true}The exact answer is known analytically: for a GHZ state the all-Z expectation value is exactly +1, and the engine returns 1.0 with a rigorous ZCC-v0.1 error bound of 0 (no Pauli terms were truncated). The run completed in under a minute and cost $0.001, a tenth of a cent, at the standard pauli.cpu rate. The result carries a public, independently verifiable certificate: api.zksf.org/certify/5b8b2c4309d44d41.
Summary: one circuit family, every tier
| Tier | Circuit | Qubits | Result | Time |
|---|---|---|---|---|
| CPU | GHZ (Clifford) | 5,000 | exact | 0.56 s |
| CPU | QAOA MaxCut, p=3 | 100 | converged (dev 0.0) | 5.9 s |
| GPU | GHZ, 1,000 shots | 8 | 502/498 (ideal 500/500) | 0.5 s |
| GPU | GHZ, 100 shots | 32 | 52/48 (ideal 50/50); 64 GiB statevector | 8.2 s |
| QPU | GHZ, 50 shots (Rigetti) | 3 | 45/50 in GHZ states, 5 noise | ~53 min incl. queue |
| QPU | GHZ, 100 shots (IonQ) | 2 | 98/100 in GHZ states, 2 noise | ~5 h incl. queue |
| CPU | GHZ, Pauli propagation, all-Z | 192 | <Z…Z> = 1.0, certified (bound 0) | < 1 min |
11. API surface (v0)
Client(base_url, token): construct once, reuse.client.estimate(circuit, shots): free feasibility + cost forecast.client.run(circuit, shots, engine=None, **params): submit and wait; raisesJobRejected/JobFailed.client.submit(...)/client.job(job_id): non-blocking variant.
Questions, bugs, or a circuit you think we should handle but don’t? info@zksf.org. Benchmark challenges welcome.