Skip to content

Transpile and run with Qiskit

examples/qiskit_transpile_and_run.py

Run existing Qiskit code — provider, transpiler, primitives. The adapter puts the platform behind the interfaces Qiskit code already speaks: QubitraProvider lists the catalogue as BackendV2 instances, and the V2 primitives run PUBs. The example builds a GHZ state, transpiles it against the backend’s reported capability, samples it, and reads ⟨ZZ⟩ from it.

Needs the extra: pip install 'qubitra-sdk[qiskit]'.

from qiskit import QuantumCircuit, transpile
from qiskit.quantum_info import SparsePauliOp
from qubitra.qiskit import QubitraEstimatorV2, QubitraProvider, QubitraSamplerV2
provider = QubitraProvider() # QUBITRA_API_KEY / QUBITRA_API_URL from the environment
backend = provider.backend(backend_id)
print(f"backend {backend.name}: {backend.num_qubits} qubits")

QubitraProvider builds its client from the environment, and provider.backend() returns a BackendV2 whose Target is built from the catalogue’s capability metadata — basis gates, connectivity, qubit count.

# A GHZ state on three qubits: H then a CX ladder, measured into "meas".
ghz = QuantumCircuit(3, name="ghz")
ghz.h(0)
ghz.cx(0, 1)
ghz.cx(1, 2)
ghz.measure_all()
# Transpiling against the backend rewrites the circuit for its Target. Against a
# backend with no reported capability this is a pass-through — the Target carries
# no constraints to satisfy.
transpiled = transpile(ghz, backend=backend)

transpile(circuit, backend=backend) aims at what the backend reported: the Target’s instruction set and coupling map come from the catalogue. A backend whose capability metadata has yet to be populated yields a permissive Target, and the transpile passes the circuit through.

sampler = QubitraSamplerV2(backend)
result = sampler.run([transpiled], shots=SHOTS).result()
counts = result[0].data.meas.get_counts()
print()
print(f"counts ({SHOTS} shots):")
for bitstring, count in sorted(counts.items()):
print(f" {bitstring} {count:5d} {'█' * round(40 * count / SHOTS)}")

QubitraSamplerV2.run takes a list and submits it as one job — a platform round trip of seconds (submit, poll, read). Results carry one BitArray per classical register; measure_all named this one meas.

# The estimator reads an operator's average instead of sampling bitstrings, so its
# circuit carries no measurements. <ZZI> on a GHZ state is +1: qubits 0 and 1 are
# perfectly correlated. Labels are Qiskit-dense — "IZZ" is Z on qubits 0 and 1.
bare = QuantumCircuit(3)
bare.h(0)
bare.cx(0, 1)
bare.cx(1, 2)
zz = SparsePauliOp.from_list([("IZZ", 1.0)])
estimator = QubitraEstimatorV2(backend)
estimation = estimator.run([(bare, zz)]).result()
print()
print(f"<ZZ> on qubits 0,1 = {float(estimation[0].data.evs):+.4f} (GHZ gives +1)")
return 0

The estimator’s circuit is the same GHZ preparation without the measurements, and the observable arrives as a SparsePauliOp. Qiskit’s dense labels read right to left: "IZZ" puts Z on qubits 0 and 1. The adapter converts it to the platform’s sparse Pauli form on the way through.

Terminal window
QUBITRA_API_KEY=qpk_... python examples/qiskit_transpile_and_run.py

Output from a live run, trimmed to the results:

counts (1024 shots):
000 503 ████████████████████
111 521 ████████████████████
<ZZ> on qubits 0,1 = +1.0000 (GHZ gives +1)

The counts split between 000 and 111 — every qubit agrees, which is the GHZ state — and the estimator reads the qubit-0/qubit-1 correlation as exactly +1.

  • Qiskit — the full adapter: format selection, sweeps, precision, and per-call latency.
  • Primitives and PUBs — what the primitives submit underneath.