QPUBench

IBM resource + cost estimator

Estimate what a circuit, or a whole benchmark study, will cost on real IBM Quantum hardware, before submitting anything and without needing an IBM Quantum account. Two modules:


Installation

pip install 'qpubench[qiskit]'

Resource estimation: real, verified

from qpubench.backends.ibm_cost_estimator import estimate_circuit_resources

est = estimate_circuit_resources(
    my_circuit_spec, backend_name="ibm_brisbane", shots=4096,
)
print(est.depth, est.two_qubit_gate_count, est.estimated_qpu_time_s)

By default this resolves "ibm_brisbane" to the real qiskit_ibm_runtime.fake_provider.FakeBrisbane calibration snapshot; no credentials, no network. Pass a live backend (e.g. IBMAdapter(...).get_live_backend()) via backend= to estimate against current calibration data instead of a static snapshot.

What’s real here, not guessed: depth/gate_counts come from actually running Qiskit’s preset pass manager against the target topology/basis gates (ecr/rz/sx/x on Eagle-class chips like Brisbane). Circuit duration comes from QuantumCircuit.estimate_duration(backend.target) after scheduling_method="alap", exactly the method IBM’s own docs recommend for local usage estimation (quantum.cloud.ibm.com/docs/en/ guides/estimate-job-run-time, checked 2026-07-09). estimated_qpu_time_s applies that same page’s documented formula:

usage_seconds = per_sub_job_overhead + (rep_delay + circuit_duration) * shots

with IBM’s own stated defaults (per_sub_job_overhead ~= 2s, rep_delay = 250 microseconds). IBM’s doc notes the experimental scheduler_timing return value “is NOT the time used for billing”; this is IBM’s own recommended proxy for it, not a guarantee of exact billed- second parity.

Verified in this session: a 4-qubit test circuit transpiled against FakeBrisbane gives depth=14, 3 real ecr (2-qubit) gates, a 3.52-microsecond circuit duration, and (at 4096 shots) an estimated 3.04s of QPU time; see tests/test_ibm_cost_estimator.py.

Cost breakdown: sourced, but verify before budgeting

from qpubench.schemas.mirrors.ibm_cost_estimator import estimate_all_plans

for plan, breakdown in estimate_all_plans(total_qpu_seconds=180.0).items():
    print(plan.value, breakdown.cost_usd, breakdown.notes)

Or aggregate a whole study’s worth of CircuitResourceEstimates at once:

from qpubench.schemas.mirrors.ibm_cost_estimator import aggregate_benchmark_cost

agg = aggregate_benchmark_cost(list_of_circuit_resource_estimates)
print(agg.total_qpu_seconds, agg.plan_breakdowns)

The four plans

Plan Model Rate Minimum Confirmed against
Open Free quota n/a n/a IBM’s own docs: “up to 10 minutes per 28-day rolling window”
Pay-As-You-Go Billed by the second $96/min none Rate: analyst/press sources (below). Billing unit (seconds): IBM’s own docs
Flex Prepaid lump sum $72/min $30,000 (~417 min), valid 1 year Minimum minutes (“at least 400”) and validity (“within one year”): IBM’s own docs. Rate/minimum $: analyst/press sources
Premium Annual subscription $48/min 5,200 min/year (~$249,600/year) Analyst/press sources only

Important: verify before making a real budget decision. ibm.com/quantum/products (the pricing page) returned HTTP 403 to automated fetches in this session (bot protection), so the exact $/minute figures above could not be confirmed directly against IBM’s own pricing page. What backs them:

Every rate is a field on IBMPricingRates, not a hardcoded constant; override with your own confirmed numbers:

from qpubench.schemas.mirrors.ibm_cost_estimator import IBMPricingRates, estimate_all_plans

my_rates = IBMPricingRates(pay_as_you_go_usd_per_minute=100.0)   # if IBM's rate changed
estimate_all_plans(total_qpu_seconds=180.0, rates=my_rates)

How each plan is evaluated


End-to-end example: costing the VQE benchmark CSV

utils/estimate_ibm_cost.py runs this against a campaign’s matrix end to end. The minimal case (H2/sto-3g, 4 qubits, 1 circuit) fits comfortably in the Open Plan’s free quota (~3 s against 600 s free).

One transpiled circuit is not one evaluation. Evaluating <H> submits one job holding one circuit per measurement basis, each at the row’s full shot count, so the script prices each job as

job overhead + circuits x shots x (rep_delay + circuit duration)

The circuit count follows from the Hamiltonian the row measures: for grouped rows it is tn-vqe’s own count of XOR patterns in the operator’s matrix, and for pauli rows it is the qubit-wise-commuting bases in Num_ExpVals_Per_Iter. A TN-VQE row measures the network-transformed operator instead, which has more terms, so its count is read from the run’s collected record where one exists. Seven completed Estimator jobs on the device, with known billed quantum_seconds, fit

billed seconds per evaluation = 11.0 + 1.125 x measurement bases

to within 4% from 2 bases to 81. The fixed 11 s is the readout calibration the Estimator’s default options request once per job, and the slope matches 4,096 shots at rep_delay plus a shallow circuit. The script uses that 11 s for pauli rows and IBM’s stated ~2 s for the Sampler behind grouped. Those jobs ran on a Heron device at its 250 µs rep_delay; the script reads each device’s own default instead, which on the Nighthawk ibm_berlin is 4 ms, sixteen times the cost per shot. Deep circuits break the fit: three depth-2389 jobs each billed fourteen times what the line predicts.

Jobs come from the run where there is one. Every call that measures anything is one job. tn-vqe logs per call how many circuits it measured (n_measured on TN-VQE, which leaves out what its phi cache served; n_circuits on VQE), and the runner keeps that as the record’s circuits_measured; utils/backfill_circuits_measured.py re-fetches it from Cebule for records collected before. A row with a collected run is priced on that run’s jobs, capped at its current Quantum_Eval_Budget; a row without one is priced at the full budget. On the pauli path the logged count is of Pauli observables rather than bases, so those jobs are priced at the operator’s qubit-wise-commuting bases. Every measuring row is priced as a hardware run whatever its Backend_Platform, and --platform / --ansatz narrow the table to the rows being planned.

Turning this into a real campaign plan

utils/estimate_ibm_cost.py --campaign NAME prices any campaign’s matrix this way; see campaigns/ibm_tn-vqe_qesem/README.md for how that campaign submits and collects targeted_screen.csv.