Qiskit
The Qiskit adapter presents the platform through the interfaces Qiskit code already uses:
a provider whose backends are BackendV2 instances, and SamplerV2/EstimatorV2
primitives that run PUBs. Existing Qiskit code changes its constructor lines, not its
algorithm.
pip install 'qubitra-sdk[qiskit]'The base install carries no framework; the extra brings Qiskit in. Importing
qubitra.qiskit without it raises an error naming this install line.
Provider and backends
Section titled “Provider and backends”QubitraProvider wraps a QubitraClient, or builds one from the environment
(QUBITRA_API_KEY, QUBITRA_API_URL) like the client itself.
from qubitra.qiskit import QubitraProvider
provider = QubitraProvider() # env-driven, or QubitraProvider(client)backends = provider.backends() # the whole catalogue, as BackendV2 instancesbackend = provider.backend("sim-statevector-26q")Each backend’s Target is built from the catalogue’s capability metadata: basis_gates
and operations become its instruction set, connectivity its coupling map,
qubit_count its width. transpile(circuit, backend=backend) then aims at what the
backend reported.
backend.run(circuit, shots=...) takes bound circuits, waits out the platform round
trip, and returns a job whose result() is a qiskit.result.Result with counts. For
parameters, observables, and batching, use the V2 primitives.
Sampler
Section titled “Sampler”from qiskit import QuantumCircuit, transpilefrom qubitra.qiskit import QubitraSamplerV2
bell = QuantumCircuit(2)bell.h(0)bell.cx(0, 1)bell.measure_all()
sampler = QubitraSamplerV2(backend)result = sampler.run([transpile(bell, backend=backend)], shots=1024).result()print(result[0].data.meas.get_counts()) # {"00": 512, "11": 512}Results carry one BitArray per classical register, in Qiskit’s own bit order — the
platform’s counts keys put clbit 0 rightmost, so nothing is reversed on the way through.
Circuits leave in the richest format the backend reports — QPY, then OpenQASM 3, then
OpenQASM 2 — so a backend whose supported_formats lists only OpenQASM 2 runs the same
code. Two things change on such a backend, both handled by the adapter: sweeps ship as
pre-bound circuit text (OpenQASM 2 has no parameter mechanism), and the estimator
refuses the backend up front, because no OpenQASM-2-only backend accepts observables —
expectation values need a backend such as sim-statevector-26q.
A parametric circuit otherwise travels unbound: as OpenQASM 3, its free parameters
export as input declarations — the platform’s binding mechanism — and the sweep’s
values travel as parameter rows beside the circuit, not as N pre-bound copies of it.
from qiskit.circuit import Parameterimport numpy as np
theta = Parameter("theta")rotation = QuantumCircuit(1)rotation.ry(theta, 0)rotation.measure_all()
result = sampler.run([(rotation, np.linspace(0, np.pi, 5).reshape(5, 1))]).result()result[0].data.meas.shape # (5,) — one counts set per bindingEstimator
Section titled “Estimator”The estimator takes circuits without measurements and reads operators instead.
Observables arrive as anything Qiskit coerces — SparsePauliOp most commonly — and are
converted to the platform’s sparse Pauli form. Coefficients must be real: the platform
measures Hermitian observables, and a projector basis (0, 1, +, …) is rejected with
an error saying so.
from qiskit.quantum_info import SparsePauliOpfrom qubitra.qiskit import QubitraEstimatorV2
zz = SparsePauliOp.from_list([("ZZ", 1.0)])bell_bare = QuantumCircuit(2)bell_bare.h(0)bell_bare.cx(0, 1)
estimator = QubitraEstimatorV2(backend)result = estimator.run([(bell_bare, zz)]).result()result[0].data.evs # array(1.0) — shaped to the pub's broadcastresult[0].data.stds # 0.0 where the backend computed exactly and sampled nothingA sweep of one observable is submitted as a single multi-row PUB, and evs comes back
shaped to the pub’s parameter broadcast. precision maps to shots as ceil(1/precision²);
precision=0 requests the backend’s default.
Latency
Section titled “Latency”One primitive call is one platform job, whatever it carries: a parameter sweep, a circuit list, several pubs — all of it travels as a single multi-PUB submission. Each call still round-trips the platform in seconds (submit, poll, read), so the shape of your algorithm determines the total latency:
- A sweep (evaluate the same circuit at 40 points) is one call, one job, seconds.
- An adaptive loop (an optimiser choosing each point from the last result) pays one round trip per iteration, because each iteration is a new decision.
For adaptive runs, open a Session and pass its id —
QubitraSamplerV2(backend, session_id=...) — so the whole run is correlated and
budget-bounded rather than appearing as hundreds of unrelated jobs.