Skip to content

Choosing a backend

backend_id is the one decision a submission requires. The catalogue tells you enough to make it.

from qubitra import QubitraClient
with QubitraClient() as client:
for backend in client.backends.list():
print(
backend.id,
backend.type,
f"{backend.qubit_count}q",
f"shots {backend.shots_min}-{backend.shots_max}",
[fmt.value for fmt in backend.supported_formats],
)

The list holds only Backends you can submit against. client.backends.get(slug) reads one, and raises NotFoundError for a slug that is not public.

The catalogue changes as Backends are added and retired, so read it rather than hard-coding an identifier. Three entries of the kind you will find there:

Backend.id type Qubits Shots Formats
sim-statevector-26q SIMULATOR 26 1 – 100 000 OPENQASM_2, OPENQASM_3, QPY
sim-noisy-12q SIMULATOR 12 1 – 100 000 OPENQASM_2, OPENQASM_3, QPY
qpu-superconducting-127q QPU 127 1 – 20 000 OPENQASM_3

Develop against a simulator. It usually accepts more circuit formats and a wider shot range, and its results are deterministic, which makes them suitable for test assertions. Move to a QPU when the hardware itself is what you are measuring.

The two simulators answer different questions. sim-statevector-26q computes exactly, so its answer is what the circuit means with no hardware in the way — use it to check that a circuit does what you intended. sim-noisy-12q runs the same circuit through a real device’s measured error rates, so its answer is what that class of hardware would do to your circuit. A result that is clean on the first and unusable on the second indicates the circuit is too deep for current hardware — useful to establish before paying for a QPU run.

Shots cost differently on each: on an exact state vector, one final state is sampled repeatedly, so shots are nearly free; the noisy simulator runs one trajectory per shot, so its cost is linear in the shot count.

Three properties are checked at submit, before anything is charged and before any execution starts. A mismatch is rejected with InvalidRequestError.

Qubit count. qubit_count is the width the Backend offers. The SDK does not parse your program, so compare the circuit’s width yourself:

backend = client.backends.get("sim-statevector-26q")
if qubits_needed > (backend.qubit_count or 0):
raise SystemExit(f"{backend.id} offers {backend.qubit_count} qubits, need {qubits_needed}")

Circuit format. supported_formats is exhaustive: submitting a format a Backend does not accept is rejected with an error naming what it does accept. qpu-superconducting-127q takes OPENQASM_3 only, so an OpenQASM 2 program has to go to a simulator or be translated first.

from qubitra import CircuitFormat
backend = client.backends.get("qpu-superconducting-127q")
assert CircuitFormat.OPENQASM_2 not in backend.supported_formats

A format this SDK version does not recognise is dropped from supported_formats rather than failing the read, so the tuple only ever contains formats both you and the platform understand.

Shot window. shots_min and shots_max bound what the Backend will take. The default is 1024, which is inside the window of most Backends but not all of them.

def clamp_shots(backend, wanted: int) -> int:
"""Keep a shot count inside the backend's stated window."""
return max(backend.shots_min or 1, min(wanted, backend.shots_max or wanted))

Filter the catalogue by what you need rather than hard-coding a slug — a slug can be retired, and code that selects by capability keeps working when the catalogue changes.

from qubitra import BackendType, CircuitFormat
def pick_backend(client, *, qubits: int, shots: int, fmt: CircuitFormat, prefer_qpu: bool):
"""The narrowest Backend that can run this circuit, or None."""
candidates = [
backend
for backend in client.backends.list()
if (backend.qubit_count or 0) >= qubits
and fmt in backend.supported_formats
and (backend.shots_min or 1) <= shots <= (backend.shots_max or shots)
]
wanted = BackendType.QPU if prefer_qpu else BackendType.SIMULATOR
preferred = [backend for backend in candidates if backend.type is wanted]
pool = preferred or candidates
# Smallest sufficient machine first: leave the wide ones for circuits that need them.
return min(pool, key=lambda backend: backend.qubit_count or 0, default=None)

basis_gates, operations, connectivity and calibration describe what a Backend can execute natively: the gate set it implements, the non-gate operations it supports, its coupling map as qubit pairs, and its most recent calibration data.

backend = client.backends.get("sim-noisy-12q")
if backend.basis_gates is not None:
print("native gates:", ", ".join(backend.basis_gates))

An empty tuple and None mean different things: () is a Backend that reported an empty list, None is a Backend that reported nothing. A partially-readable coupling map keeps the pairs it can read rather than failing the whole catalogue read.

Concretely:

  • queue_size is the queue depth reported at the last catalogue refresh. It is not a queue position, not a wait estimate, and not current. Do not route on it.
  • available is whether the Backend was still listed at that same refresh. The catalogue already filters on it, so every Backend list() returns reads True. A Backend that has since been retired disappears from the list, and its get raises NotFoundError; it does not come back as available=False.

There is no live availability signal in the SDK. If a Backend is gone, you find out at submit via NotFoundError, and nothing is charged.

from qubitra import NotFoundError
try:
job = client.jobs.submit(backend_id=chosen.id, circuit=BELL, shots=1024)
except NotFoundError:
# The Backend is no longer available. Re-read the catalogue and pick again.
chosen = pick_backend(client, qubits=2, shots=1024, fmt=CircuitFormat.OPENQASM_3, prefer_qpu=False)

A slug like qpu-superconducting-127q describes capability: what kind of machine, what technology, how wide. Neither the slug nor any field on the model names whoever runs the hardware — there is no provider field, hardware owner, or access tier. The hardware behind a Backend can change without your code changing, so select by capability rather than by inferring a vendor from a slug.