Skip to content

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.

Terminal window
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.

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 instances
backend = 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.

from qiskit import QuantumCircuit, transpile
from 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 Parameter
import 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 binding

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 SparsePauliOp
from 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 broadcast
result[0].data.stds # 0.0 where the backend computed exactly and sampled nothing

A 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.

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.