ZKSF logo, a neon quantum brainZKSF

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 it

The 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 by sqrt(2 · truncation_weight), measured in a single run rather than extrapolated.
  • Pauli propagation reports the same protocol: error_bound is 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
EngineBest forLimits
exact.cpuGround truth, any gate set≤ 30 qubits
exact.gpuExact results, GPU-accelerated≤ 32 qubits
cliffordStabilizer/error-correction circuitsClifford gates only, 5,000+ qubits
mps.quimb.cpuQAOA, ansätze, structured circuitsentanglement-dependent, ~128 qubits; certified error bounds
mps.aer.cpuIndependent tensor-network cross-checkentanglement-dependent
pauli.cpuExpectation values of large circuitsneeds an observable; 100 to 200 qubits, no bitstring counts
noisy.cpuPreview hardware noise before you paydevice-noise model; optional error mitigation (ZNE)
qpu.rigettiReal superconducting hardwareRigetti Cepheus 108 qubits; scheduled queue, physical noise
qpu.ionqReal trapped-ion hardwareIonQ 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.

CircuitQubitsDepthEngine pickedWall timeAccuracy
GHZ (Clifford)5,0005,000clifford0.56 sexact
QAOA MaxCut, p=3100304mps.quimb.cpu5.9 sconverged (dev 0.0)
Layered ansatz809mps.quimb.cpu4.3 sconverged (dev 0.0)
Exact statevector269exact.cpu2.7 sexact (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 0
expectation: 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

TierCircuitQubitsResultTime
CPUGHZ (Clifford)5,000exact0.56 s
CPUQAOA MaxCut, p=3100converged (dev 0.0)5.9 s
GPUGHZ, 1,000 shots8502/498 (ideal 500/500)0.5 s
GPUGHZ, 100 shots3252/48 (ideal 50/50); 64 GiB statevector8.2 s
QPUGHZ, 50 shots (Rigetti)345/50 in GHZ states, 5 noise~53 min incl. queue
QPUGHZ, 100 shots (IonQ)298/100 in GHZ states, 2 noise~5 h incl. queue
CPUGHZ, Pauli propagation, all-Z192<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; raises JobRejected / 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.