Schemas
CircuitSpec, BackendSpec, ExecutionOptions
and QuantumResult are plain Pydantic v2 models. They serialize to JSON,
validate on construction, and never import a quantum SDK.
QPUBench allows you to easily set up quantum computing benchmark studies and generate one common results format for easy and rigorous scientific comparison. The data-schema-oriented framework allows you to use QPUBench with an agentic system in the driver's seat, setting up benchmark studies with just a single prompt.
# One circuit, one backend, one data point.
from qpubench import BenchmarkRunner, CircuitSpec, Pauli
from qpubench.backends import AerAdapter
bell_qasm = """OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
h q[0];
cx q[0],q[1];"""
circuit = CircuitSpec(
num_qubits=2,
serialized=bell_qasm,
observables=[Pauli("Z0 Z1")],
)
runner = BenchmarkRunner(store="results/bell.ndjson")
runner.register(AerAdapter(), name="aer")
record = runner.run(circuit, "aer", shots=4096)
ev = record.result.expectation_values[0]
# <ZZ> = 0.9986 ± 0.0156
# The same circuit across two backends and three shot counts,
# grouped under one run_id. `circuit` is the CircuitSpec above.
from qpubench import BenchmarkRunner, ExecutionOptions
from qpubench.backends import AerAdapter, IBMAdapter
runner = BenchmarkRunner(store="results/sweep.ndjson")
runner.register(AerAdapter(), name="aer")
runner.register(IBMAdapter(backend_name="ibm_torino"), name="ibm")
records = runner.sweep(
circuits=[circuit],
backend_names=["aer", "ibm"],
options_list=[ExecutionOptions(shots=s)
for s in (512, 2048, 8192)],
run_id="bell_shots",
)
for r in records:
ev = r.result.expectation_values[0]
print(r.backend.name, r.options.shots, ev.value, ev.std_error)
# aer 512 0.9883 0.0442
# aer 8192 0.9985 0.0110
# ibm 8192 0.8734 0.0112
Describe the study instead of writing it. QPUBench is schema-oriented precisely so an agent can drive it, and the framing below is sent along with your question.
Name a simulator or QPU and it is used as written; name none and the prompt asks for the PennyLane lightning.qubit simulator, so the study still runs.
CircuitSpec, BackendSpec, ExecutionOptions
and QuantumResult are plain Pydantic v2 models. They serialize to JSON,
validate on construction, and never import a quantum SDK.
Any object with spec, validate() and run()
is a backend. Libraries that build their own circuits implement the sibling
AlgorithmAdapter protocol instead. No base class to inherit.
NDJSONStore (append-only, zero-dependency), ParquetStore
(columnar, for pandas) and S3Store (one object per record, safe for
distributed sweeps) share one interface.
Every project below is reachable from QPUBench, either as a runnable adapter that executes circuits, or as a schema bridge that captures its run configuration and results in the shared record format. Schema bridges never import the external library, so installing QPUBench pulls in none of them. Follow a tile for that project's own QPUBench documentation, or the “Upstream” link for the repository QPUBench integrates against — and, where the schema was read off a publication, the paper beside it.
Simulators and QPUs with a runnable adapter in qpubench.backends. Register one with a BenchmarkRunner and every run lands in the same record format.
Packages whose run configurations, wavefunctions and results are mirrored as typed schemas, so their output is directly comparable with everything else in the store.
Beyond gate-based superconducting qubits. ComputingModel and QubitModality are independent axes on every record, so these runs share one store with everything above.
The layers around execution: making a noisy run usable, making a large circuit fit, and running the whole campaign as scheduled compute.
Marks shown here are generated placeholder wordmarks, not vendor artwork; see docs/site/README for how to drop in a real logo.
A note on “chemical accuracy”. QPUBench reports an energy error, the gap between a run and a classically computed reference; it flags whether that error is below 1.6 mHartree. That is a numerical-convergence check against a computed value, not agreement with an experimentally measured quantity. Treat the stored reference as a computed baseline, not as ground truth.
Every page published on this site, grouped by what it is for. The integration pages each document one upstream project and the schemas that mirror it.
An adapter is a plain class. Two rules keep the ecosystem healthy: SDK imports live
inside methods, so importing your adapter never requires the SDK; and recoverable
failures return status=FAILED rather than raising, so one bad point does not
kill a sweep.
class MySimulatorAdapter:
@property
def spec(self) -> BackendSpec:
return BackendSpec(name="my_sim", provider="me", simulator=True)
def validate(self, circuit: CircuitSpec) -> list[str]:
return [] if circuit.num_qubits <= 30 else ["max 30 qubits"]
def run(self, circuit: CircuitSpec, options: ExecutionOptions) -> QuantumResult:
import my_sdk # SDK imports stay inside methods
counts = my_sdk.sample(circuit.serialized, shots=options.shots)
return QuantumResult(
computing_model=circuit.computing_model,
shots=ShotResult(num_qubits=circuit.num_qubits,
num_shots=options.shots, counts=counts),
status=JobStatus.SUCCEEDED,
)
Templates for both protocols live in integrations/template/, and Adapters & Backends walks through testing an adapter with the SDK mocked out.