Skip to content

Submit a Bell pair

examples/submit_bell_pair.py

Submit a first circuit and read counts back. The example runs the whole execution path — SDK to the Qubitra API to a Temporal workflow to whichever backend the identifier resolves to — so a failure here says something real about the stack. It also shows format negotiation: the circuit ships in whichever OpenQASM version the backend accepts.

from qubitra import CircuitFormat, QubitraClient
# A Bell pair: entangle two qubits, measure both. On a simulator the counts land almost
# entirely on "00" and "11", so a result that is spread across all four outcomes is a
# visible sign something ran the wrong circuit.
BELL = {
# `include "stdgates.inc";` is not decoration: OpenQASM 3 defines no gates of its own,
# so `h` and `cx` are undefined identifiers without it and a conformant importer
# rejects the program. Every OpenQASM 3 circuit in this SDK's documentation carries it.
CircuitFormat.OPENQASM_3: """
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c = measure q;
""",
# Backends differ in what they accept, and not every one has caught up to OpenQASM 3 —
# `client.backends.list()` reports each one's `supported_formats`.
CircuitFormat.OPENQASM_2: """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0],q[1];
measure q -> c;
""",
}

The same Bell pair is written twice, keyed by CircuitFormat, because the format a job ships in is negotiated per backend. A circuit is a string in every format; the CircuitFormat value beside it tells the platform how to parse it.

DEFAULT_BACKEND = "sim-statevector-26q"
SHOTS = 1024
# Preferred first: OpenQASM 3 where a backend takes it, OpenQASM 2 otherwise.
PREFERENCE = (CircuitFormat.OPENQASM_3, CircuitFormat.OPENQASM_2)
def pick_format(client: QubitraClient, backend_id: str) -> CircuitFormat:
"""The best format this backend accepts, out of the ones this circuit is written in.
A backend reports what it accepts, and a backend that reports nothing places no
constraint of its own — so an empty list means the newest format is worth trying rather
than that nothing is.
"""
accepted = set(client.backends.get(backend_id).supported_formats)
if not accepted:
return PREFERENCE[0]
for candidate in PREFERENCE:
if candidate in accepted:
return candidate
raise SystemExit(
f"{backend_id} accepts {sorted(accepted)}, and this example writes only "
f"{[f.value for f in PREFERENCE]}"
)

client.backends.get(backend_id).supported_formats is the backend’s own report of what it takes, and pick_format walks the preference order against it. An empty report places no constraint, so the newest format is worth trying.

job = client.jobs.submit(
backend_id=backend_id,
circuit=BELL[circuit_format],
circuit_format=circuit_format,
shots=SHOTS,
name="bell-pair example",
)
# submit returns once the platform has accepted the job, not once it has run, so the
# first status is PENDING and the id is what everything else is addressed by.
print(f"submitted {job.id} to {backend_id} — {job.status}")
job = client.jobs.wait(job.id, timeout=120, poll_interval=2)
print(f"finished {job.id} — {job.status}")
if job.status.value != "COMPLETED":
# A failed job is a result too, and its error is the useful part.
print(f"job did not complete: {job.error or '(no error recorded)'}", file=sys.stderr)
return 1

submit returns as soon as the platform has accepted the job; wait polls until it reaches a terminal status. The status check matters because wait returning says the job finished, and a failed job finishes too — its error field carries the reason.

result = client.jobs.result(job.id)
counts = result.counts or {}
total = sum(counts.values()) or 1
print()
for outcome, count in sorted(counts.items(), key=lambda kv: -kv[1]):
share = 100 * count / total
print(f" {outcome} {count:6} {share:5.1f}% {'█' * round(share / 2)}")
return 0

result.counts maps measured bitstrings to how often each appeared. A Bell pair’s counts land almost entirely on 00 and 11, so the printed histogram is checkable at a glance.

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

Output from a live run at 1024 shots, trimmed to the counts:

11 521 50.9% █████████████████████████
00 503 49.1% █████████████████████████

The counts concentrate on the two correlated outcomes in a near-even split — the Bell pair’s signature. A backend id as the first argument selects a different backend, and a CircuitFormat value as the second forces a format past the negotiation.