Estimate an expectation value
examples/estimate_expectation.py
The problem
Section titled “The problem”Measure an observable’s expectation value. A sampler answers with counts; an estimator answers with the average of an operator on the state a circuit prepares — a single number per observable. Variational algorithms such as VQE and QAOA are optimisers wrapped around that number, so this is the shape those workloads submit in.
The walkthrough
Section titled “The walkthrough”The circuit
Section titled “The circuit”from qubitra import Observable, PauliTerm, QubitraClient
# No `bit` declaration and no `measure`: the observables are what gets read.BELL = """OPENQASM 3.0;include "stdgates.inc";qubit[2] q;h q[0];cx q[0], q[1];"""An estimator PUB’s circuit carries no measurement instructions, because measuring collapses the state whose expectation value is being read. The observables do the reading.
The observables, in sparse Pauli form
Section titled “The observables, in sparse Pauli form”# A sparse Pauli representation: a term names the qubits it acts on rather than padding to# the circuit's width, so a two-qubit term on a 26-qubit circuit is still two characters.ZZ = Observable(num_qubits=2, terms=(PauliTerm(pauli="ZZ", qubits=(0, 1), coefficient=1.0),))ZI = Observable(num_qubits=2, terms=(PauliTerm(pauli="Z", qubits=(0,), coefficient=1.0),))Each PauliTerm names the qubits it acts on: ZI is written as a single-character
"Z" on qubit 0, and the identity on qubit 1 is implied. The form stays two characters
however wide the circuit is.
The two answers say something checkable about entanglement. Z⊗Z is +1 on 00 and
11 and −1 on 01 and 10; a Bell pair is only ever found in the correlated
outcomes, so ⟨ZZ⟩ = +1. Z⊗I reads the first qubit alone, which on its own is an even
coin, so ⟨ZI⟩ = 0.
One PUB, two observables
Section titled “One PUB, two observables” # One circuit, two observables — a single PUB. `values` comes back index-aligned # with the list, so values[0] is ZZ and values[1] is ZI. job = client.jobs.submit( backend_id=backend_id, circuit=BELL, observables=[ZZ, ZI], shots=SHOTS, name="bell-pair expectation", )Passing observables makes the PUB an estimator. Both observables read the same
prepared state, in one job.
Read the values
Section titled “Read the values” result = client.jobs.result(job.id) # `values` reads the first PUB, which is the only one here. A job of several PUBs # reads `result.pubs[i]` instead. values = result.values or () if len(values) != len(EXPECTED): print(f"expected {len(EXPECTED)} values, got {len(values)}", file=sys.stderr) return 1
print() for (label, expected), value in zip(EXPECTED.items(), values, strict=True): # An exact simulator computes the expectation from the state vector rather than # sampling, so it lands on the analytic value; a noisy backend will not. print(f" <{label}> {value:+.4f} (expected {expected:+.1f})") return 0values is index-aligned with the observables list: values[0] is ⟨ZZ⟩ and
values[1] is ⟨ZI⟩.
Running it
Section titled “Running it”QUBITRA_API_KEY=qpk_... python examples/estimate_expectation.pyOutput from a live run, trimmed to the values:
<ZZ> +1.0000 (expected +1.0) <ZI> +0.0000 (expected +0.0)Perfect correlation with no local bias — the two numbers that define a Bell pair. The default backend computes expectations from the state vector, so the values land exactly on the analytic answers.
Where to go next
Section titled “Where to go next”- Primitives and PUBs — the estimator, observables, and how results align with what you submitted.