SDK: metering and cost
Reference for qsolve.metering and qsolve.sim: how the SDK records what crosses the backend, how fidelity and circuit size are measured, and how Estimate and run-time prediction feed the price quote.
Updated 13 November 2026 View as markdown
On this page
Qsolve does not take your word for how big or how faithful your quantum run was. Everything your quantum method sends to a backend passes through a recorder, and the numbers are computed from that record. This page documents the recorder, the measurement functions, the pre-run estimate and the run-time model. Back to the SDK reference.
Most of this is harness machinery: as a workflow author you call ctx.report_mitigation and return an Estimate; the rest you read to understand what the platform measures. For the concepts see Error mitigation and fidelity and Benchmarks.
qsolve.metering
from qsolve import metering
Constants
| Name | Value | Meaning |
|---|---|---|
IDEAL_MAX_QUBITS |
20 |
Largest circuit whose ideal output is simulated (exactly, noise-free). Bigger circuits cannot be scored. |
MAX_IDEAL_CIRCUITS |
200 |
Distinct unreported circuits simulated for fidelity, spread evenly over the run. |
MAX_STATS_CIRCUITS |
40 |
Distinct circuits transpiled for depth and gate counts: the 20 largest, then an even spread of the rest, so a workflow cannot hide its deepest circuit among many small ones. |
BASIS |
["cx", "rz", "sx", "x"] |
The gate set circuits are transpiled to for depth and two-qubit counts. |
Dispatch
@dataclass
class Dispatch:
index: int
method: str
circuit: QuantumCircuit
shots: int
counts: dict[str, int]
metadata: dict[str, Any]
seconds: float
One circuit that crossed the backend boundary. index is its position in the run's record (the number you cite in report_mitigation), method the id of the manifest method that sent it, circuit the copy that was sent, counts and metadata what came back, seconds the backend time.
MitigationReport
@dataclass
class MitigationReport:
circuits: list[int]
distribution: dict[str, float]
methods: list[str]
kept: float | None
steps: list[dict[str, Any]]
What a workflow declared through ctx.report_mitigation, after validation.
Recorder
@dataclass
class Recorder:
dispatches: list[Dispatch]
reports: list[MitigationReport]
method: str = "quantum"
lock: threading.Lock
Everything that crossed the backend boundary during one run. One is created per run_workflow.
| Member | Meaning |
|---|---|
add(circuit, shots, counts, metadata, seconds) -> int |
Appends a Dispatch tagged with the current method and returns its index. Thread-safe. Called by QsolveBackend.dispatch. |
of(method) -> list[Dispatch] |
The dispatches sent by one method id. |
report(circuits, distribution, methods, kept, steps) -> None |
Stores a MitigationReport. Called by ctx.report_mitigation after it has validated the arguments. |
method |
The method currently running; the comparison sets it before each method, which is how a dispatch is attributed. |
rec = metering.Recorder()
backend = QsolveBackend(AerBackend(), rec) # see the bridge page
backend.run(qc, shots=100)
print(len(rec.dispatches), rec.of("quantum")[0].counts)
fidelity and shot_noise_floor
fidelity(p: dict[str, float], q: dict[str, float]) -> float
shot_noise_floor(ideal: dict[str, float], shots: int, seed: int) -> float
fidelity is the classical (Bhattacharyya) fidelity F = (sum_i sqrt(p_i q_i))^2 of two distributions over bit strings. Both are normalised inside the function; spaces in bit-string keys are ignored; non-positive weights are dropped; it returns 0.0 if either side has no weight. 1 means identical, 0 disjoint.
shot_noise_floor draws shots noise-free samples from ideal (multinomial, seeded) and returns their fidelity with ideal. With finite shots even a perfect device scores below 1; this is that score, reported as fidelity_floor next to every figure.
circuit_key
circuit_key(qc: QuantumCircuit) -> tuple[Any, ...]
A hashable identity for a bound circuit: width, and per operation its name, qubit and clbit positions, parameter values (rounded to 12 places; arrays by shape and bytes) and, for non-standard operations such as Pauli-evolution gates, the identity of their decomposition. Two dispatches with equal keys are the same circuit; their counts are pooled before scoring.
ideal_distribution
ideal_distribution(qc: QuantumCircuit) -> dict[str, float] | None
The noise-free output distribution over the circuit's classical bits (bit string with classical bit 0 on the right; probabilities above 1e-14 only), or None when it cannot be simulated exactly: more than 20 qubits, no qubits, mid-circuit measurement (a gate after a measurement), reset, delay, if_else, while_loop, for_loop, switch_case, a classical condition, or classical bits that are never measured. Barriers are ignored. Uses a statevector simulation.
active_qubits
active_qubits(qc: QuantumCircuit) -> int
The number of distinct qubits the circuit's operations touch, barriers excluded. (Different from qsolve.circuit_record.active_qubits, which returns the sorted list of qubit indices.)
circuit_stats
circuit_stats(dispatches: list[Dispatch]) -> CircuitStats | None
The measured size of what was sent: qubits (the widest circuit by active qubits), depth and two_qubit_gates (the maxima over a sample of at most 40 distinct circuits transpiled to BASIS at optimisation level 1, the cx count being the two-qubit count), circuits (the number of dispatches) and shots (the sum of shots over all of them). None when there are no dispatches. The platform's max_two_qubit_gates bound in a benchmark case is checked against this number.
mitigation
mitigation(rec: Recorder, method: str, source: str) -> Mitigation | None
Raw fidelity from the recorded circuits, mitigated fidelity from the workflow's reports. Returns None when the method sent nothing or none of its circuits can be scored.
Which circuits count:
- Circuits a valid report covers, and every other circuit whose ideal output is not a single bit string. A circuit whose output is a single bit string (a mirror or calibration circuit, an idle register) computes nothing the answer depends on and is left out; if every circuit is like that, they are scored anyway. A circuit left unreported that does compute something counts at its raw fidelity, so reporting a flattering subset changes nothing.
- Of those, only circuits with at least half the two-qubit operations (after opening composite gates) of the largest. A trivial circuit sent alongside carries no weight.
- A report is used only when all the circuits it cites share one ideal output and were sent by this method.
- Circuits that cannot be scored are left out;
coverageis the share of the method's shots that could be scored.
The figures are shot-weighted averages. With no usable report, fidelity_mitigated equals fidelity_raw, postselection_rate is 1.0, methods is empty and reported is false. With reports, fidelity_mitigated is the fidelity of your reported distribution against the ideal output (circuits without a report contribute their raw fidelity), postselection_rate is the mean of your kept values (1 where omitted), methods the union of your names, reported is true and details["steps"] your ladder.
source is stored as Mitigation.source: hardware, noise_model or ideal_simulator. run_workflow derives it from the backend: any non-qpu target is ideal_simulator; a qpu whose device name starts with fake is noise_model; otherwise hardware.
quality
quality(rec: Recorder, method: str, source: str) -> Quality
Quality(mitigation=mitigation(...), circuit=circuit_stats(rec.of(method))). Each part is None when nothing was sent or scorable. This is what run_workflow stores in result.quality, for the quantum method.
Estimate
Estimate is defined in qsolve.results and exported from qsolve; it is the one metering type you construct. Your Workflow.estimate(target) returns it.
class Estimate(BaseModel):
qubits: int
circuits_on_target: int
shots_on_target: int
classical_cost_class: str = "low"
runtime_s: float | None = None # at least 0
notes: list[str] = []
by_default: bool = False
| Field | Meaning |
|---|---|
qubits |
Qubits the run uses. |
circuits_on_target |
Circuits submitted to the chosen backend over the whole run. |
shots_on_target |
Total shots across those circuits. |
classical_cost_class |
"low", "medium" or "high": local CPU work in the sandbox. Not validated by the model; use those three values. |
runtime_s |
Predicted seconds for the whole run on one reference core per simulator thread, excluding queue time at a hardware provider. None means not predicted. Must be at least 0. The runner adds RUN_OVERHEAD_S (20 s). The platform refuses a run whose prediction exceeds resources.timeout_s (a platform rule; the SDK itself does not check it). |
notes |
Free text shown with the quote. |
by_default |
Set by the SDK, never by you: true when the workflow gave no estimate and the SDK used one circuit and params["shots"] shots. |
What the estimate is used for:
- The price quote and credit hold. The platform turns the numbers into credits (Credits and pricing).
- The
estimate_honestgate.run_benchmarkscompares your estimate with what the first benchmark case really sent. Circuits and shots must each be within a factor of two (0.5 <= estimated / actual <= 2). With no estimate it passes with a note; if no circuits were sent it passes.
def estimate(self, target):
return Estimate(qubits=self.n, circuits_on_target=1,
shots_on_target=int(self.params["shots"]), classical_cost_class="low")
Count every circuit your quantum method sends, including one per measurement basis and per parameter set; the Estimator primitive sends one circuit per basis group per value.
qsolve.sim
from qsolve.sim import (RUN_OVERHEAD_S, S_PER_CIRCUIT, S_PER_GATE_AMPLITUDE, S_PER_SHOT,
S_PER_TRANSPILE, sim_threads, simulator_threads, statevector_seconds)
Simulator threading and run-time prediction. Aer updates each amplitude of a statevector independently, so a simulation gives bit-identical results at any thread count; simulations may therefore use every core the run was given, while BLAS and other OpenMP code stay on one thread, where summation order could change the result.
Constants (seconds, for one core of a current server CPU)
| Name | Value | Meaning |
|---|---|---|
S_PER_GATE_AMPLITUDE |
1.8e-9 |
One gate applied to one amplitude. |
S_PER_CIRCUIT |
0.012 |
Aer call, result decoding and Python around one circuit. |
S_PER_SHOT |
2e-6 |
Drawing one shot and decoding it into counts. |
S_PER_TRANSPILE |
0.15 |
Transpiling a circuit the simulator cannot run as written. |
RUN_OVERHEAD_S |
20.0 |
Sandbox start, imports, classical baseline and interpretation. |
The platform scales the prediction to its own machines. The private constants _PARALLEL_FROM_QUBITS = 15 (Aer parallelises statevector updates from 15 qubits up) and _THREAD_EFFICIENCY = 0.7 (measured: 2 threads run 20 to 24 qubit circuits 1.6 to 1.7 times faster) are used by statevector_seconds.
sim_threads
sim_threads() -> int
Threads a simulation may use: the positive integer in QSOLVE_SIM_THREADS (set by the platform to the sandbox's CPUs) if there is one, otherwise the number of cores this process may run on (os.sched_getaffinity, else os.cpu_count()), at least 1.
simulator_threads
@contextmanager
simulator_threads() -> Iterator[None]
A context manager that lets OpenMP (Aer) use sim_threads() threads for the duration of the block, via threadpoolctl. Used by AerBackend.sample. If you run your own AerSimulator calls inside a method, wrap them in it to use the cores the run was given:
from qsolve.sim import simulator_threads
with simulator_threads():
counts = sim.run(qc, shots=1000).result().get_counts()
statevector_seconds
statevector_seconds(qubits: int, gates: int, circuits: int = 1, shots: int = 0,
threads: int | None = None) -> float
Predicted seconds to simulate circuits circuits of gates gates on qubits qubits, drawing shots shots from each.
| Parameter | Meaning |
|---|---|
qubits |
Circuit width. |
gates |
Gates per circuit. |
circuits |
Number of circuits. Default 1. |
shots |
Shots drawn from each circuit. Default 0. |
threads |
Threads to assume; default sim_threads(). |
The model: work = S_PER_GATE_AMPLITUDE * gates * 2**qubits; for 15 or more qubits and more than one thread, work /= 1 + 0.7 * (threads - 1); the result is circuits * (work + S_PER_CIRCUIT + S_PER_SHOT * shots). It is an estimate for use in Estimate.runtime_s, not a measurement. Add S_PER_TRANSPILE per circuit the simulator cannot run as written, and the time of any classical baseline you run.
Estimate(qubits=n, circuits_on_target=1, shots_on_target=shots,
runtime_s=statevector_seconds(n, gates=400, circuits=1, shots=shots))