Primitives and PUBs
A Job carries an ordered list of PUBs — Primitive Unit Blocs. A PUB is one circuit plus everything needed to run it, and its contents determine what kind of result it returns.
| A PUB holding | is a | and returns |
|---|---|---|
| a circuit | sampler | CountsResult — measured bitstrings |
| a circuit and observables | estimator | ExpectationValuesResult — a number per observable |
| either, plus parameter rows | a sweep | one value per row |
client.jobs.submit(circuit=…) builds a Job of exactly one PUB. That is the common case,
and it stays a one-line call for all three shapes.
Counts: the sampler
Section titled “Counts: the sampler”A PUB whose circuit measures returns counts.
BELL = """OPENQASM 3.0;include "stdgates.inc";qubit[2] q;bit[2] c;h q[0];cx q[0], q[1];c = measure q;"""
job = client.jobs.submit(backend_id="sim-statevector-26q", circuit=BELL, shots=1024)client.jobs.wait(job.id)print(client.jobs.result(job.id).counts) # {"00": 512, "11": 512}Expectation values: the estimator
Section titled “Expectation values: the estimator”An estimator returns the expectation value of an operator on the state a circuit prepares — a single number per observable instead of a histogram. Variational algorithms such as VQE and QAOA are optimisers wrapped around that number, which is why it is provided as a primitive.
An observable is a weighted sum of Pauli strings. The representation is sparse: a term names the qubits it acts on, so a two-qubit term on a 26-qubit circuit stays two characters.
from qubitra import Observable, PauliTerm
# <ZZ> on a Bell pair: +1 on "00" and "11", -1 on "01" and "10", so a perfectly# correlated state gives exactly +1.ZZ = Observable(num_qubits=2, terms=(PauliTerm(pauli="ZZ", qubits=(0, 1), coefficient=1.0),))
job = client.jobs.submit( backend_id="sim-statevector-26q", circuit=BELL_UNMEASURED, observables=[ZZ], shots=4096,)client.jobs.wait(job.id)print(client.jobs.result(job.id).values) # (1.0,)values is index-aligned with observables: submitting three observables against one
circuit returns three numbers in that order. ExpectationValuesResult.stds carries the
standard error of each where the backend reports one; an exact simulator computes the
expectation from the state vector rather than sampling, so it may report none.
Parameter sweeps
Section titled “Parameter sweeps”A variational loop evaluates the same circuit at many parameter values. Submitting one job per value is slow — each queues, schedules and is accounted separately — so a PUB can carry the whole sweep.
The binding lives in the circuit. An OpenQASM 3 input declaration names a value the
program does not fix; parameter_values then supplies one row per run, each row holding
one value per input in declaration order.
import math
from qubitra import Observable, PauliTerm
ROTATION = """OPENQASM 3.0;include "stdgates.inc";input float[64] theta;qubit[1] q;ry(theta) q[0];"""
Z = Observable(num_qubits=1, terms=(PauliTerm(pauli="Z", qubits=(0,), coefficient=1.0),))
job = client.jobs.submit( backend_id="sim-statevector-26q", circuit=ROTATION, observables=[Z], parameter_values=[[0.0], [math.pi / 2], [math.pi]], shots=4096,)client.jobs.wait(job.id)print(client.jobs.result(job.id).values) # approximately (1.0, 0.0, -1.0)Rotating by theta about Y walks the state from |0> to |1>, so <Z> traces a
cosine: +1, 0, -1. The whole sweep is expressed in the circuit text and the
parameter rows; no other mechanism is involved.
Several PUBs in one Job
Section titled “Several PUBs in one Job”client.jobs.run submits a list. Use it when the circuits belong to one logical
measurement — a set of observables that must be read against the same run, or a batch you
want reported and charged as one unit.
from qubitra import Pub
job = client.jobs.run( backend_id="sim-statevector-26q", pubs=[ Pub(circuit=BELL), Pub(circuit=GHZ, shots=8192), # its own shot count Pub(circuit=ROTATION, parameter_values=[[0.0], [3.14159]]), ], shots=1024, # the job's default name="comparison batch",)shots on the job is the default; a PUB carrying its own shot count overrides it for
that PUB alone.
Reading results
Section titled “Reading results”JobResult.pubs is index-aligned with what you submitted: entry i belongs to pubs[i],
regardless of what happened to the others.
from qubitra import CountsResult, ErrorResult, ExpectationValuesResult
for index, pub_result in enumerate(client.jobs.result(job.id).pubs): match pub_result: case CountsResult(counts=counts): print(index, "counts", counts) case ExpectationValuesResult(values=values): print(index, "values", values) case ErrorResult(detail=detail): print(index, "failed:", detail)A PUB that failed occupies its own slot as an ErrorResult; the entries that succeeded
are still present.
For a single-circuit submission, the convenience properties read the first entry and
return None when it is not that shape:
result = client.jobs.result(job.id)result.kind # "counts" | "probabilities" | "expectation_values" | "error"result.counts # dict, or Noneresult.values # tuple of floats, or Noneresult.probabilities # dict, or Noneresult.shots # int, or None