Part V. Practical Software · Chapter 38
Qiskit, Primitives, and Circuit Execution
An SDK tutorial can put a Bell circuit on screen in ten minutes; knowing whether the output means anything takes a bit more structure. This chapter separates the stable execution workflow — build, choose a backend, set shots, record, interpret — from the API surface that keeps changing underneath it.
In this chapter 11 sections
Define the circuit and observable independently of the backend, use the appropriate primitive for expectation estimation or sampled outcomes, pin the Qiskit version and execution options, and store the transpiled circuit, shots or precision target, backend identity, and result schema.
The useful distinction is semantic, not syntactic: an estimator answers a question about an observable; a sampler answers a question about a distribution. A versioned adapter makes those contracts explicit without promoting today’s class names into timeless pedagogy.
Sampler and estimator answer different questions
For a state and observable O, an estimator targets . A sampler approximates through finite draws. Qiskit documents these as distinct primitive interfaces [Qiskit primitives documentation]. The estimator needs an observable and precision contract; the sampler needs measurement interpretation and a shot contract.
Keep a framework-neutral fixture beside the adapter: circuit operations in the book’s q0-most-significant convention, the analytic expectation or exact support, tolerances, and an acceptance rule. OpenQASM 3 can record circuit and control semantics, while QIR defines an intermediate-representation contract; neither makes interchange automatic [OpenQASM 3 specification] [QIR Alliance specification].
The pinned Qiskit environment
The lab records Python, exact Qiskit distribution versions, a lock hash, imports, primitive implementation, and review date. It does not install Qiskit in the core lab environment. Optional dependency churn should break an optional adapter job, not the reference simulator. Store the output schema version too: a numerical result can remain correct while a container or metadata field changes.
| Interpreter | exact major/minor; supported platform |
|---|---|
| Resolver evidence | lock file and SHA-256 |
| Primitive | fully qualified implementation and options |
| Target | ideal reference or named backend identity |
| Review | date plus migration owner |
Estimate Z after a Hadamard
Prepare by applying H to and ask for Pauli Z. Analytically, the +1 and −1 eigenvalues have equal weight, so E[Z]=0. This is an expectation check, not a histogram check. Store circuit and observable hashes, estimator options, returned value, target precision, and metadata. On an ideal reference primitive, use a tight numerical tolerance. On shot-based or hardware execution, attach an uncertainty method and backend evidence.
The derivation is the durable oracle. The Qiskit call is a dated adapter. That separation allows an API migration to alter imports or result extraction without altering the expected value.
The zero expectation alone is a weak fixture: the states and could be accidentally averaged by a broken batch adapter and also yield zero. Add anchor cases. For , ; for , ; for , and . The set exercises state preparation, observable ordering, sign, and batch alignment. Store results keyed by stable case IDs rather than assuming the returned list is in submission order.
For a Pauli observable with eigenvalues ±1 estimated from independent shots, a sample mean has estimated standard error under the simple binomial model. An ideal state-vector estimator has numerical error instead, so reporting “shots” for it is a category mistake. The record schema should make the uncertainty source explicit: analytic, finite-shot, mitigation-derived, or backend-reported.
Sample a Bell circuit without losing bit order
The paired sampler fixture prepares the Bell state and measures both qubits. Exact support is {00,11}; expected probability is one half for each. Finite shots fluctuate. With n shots and ideal p=0.5, a simple teaching band can use . This is an explicitly declared normal-approximation fixture, not a universal test.
Finite-shot output cannot be interpreted like a state vector [Nielsen and Chuang] [benchmarking review]. Normalize the SDK’s native classical-bit display into the book convention before comparing support, and preserve the native record so the translation remains auditable.
Bell support is deliberately paired with asymmetric calibration fixtures. Prepare 01 and 10, measure them, and verify the adapter maps each native key to the intended book string. Without those rows, reversing both classical columns leaves 00 and 11 unchanged and the headline test passes. The normalization function should receive the explicit logical-to-classical map emitted by compilation; it should never infer order from dictionary iteration or a formatted histogram label.
Quasi-distributions need another boundary. Error mitigation can produce small negative weights or values above one, which are not raw counts and must not be rounded into a histogram. Retain the native numeric type, normalization convention, and mitigation metadata. A sampler acceptance test may compare an explicitly normalized probability estimate, but its record must distinguish that estimate from observed integer frequencies.
Pinned Qiskit sampler/estimator lab
| case | oracle | evidence |
|---|---|---|
| H then Z estimator | E[Z]=0 | value, precision, observable/circuit hashes |
| Bell sampler | support 00/11, p=.5/.5 | shots, seed, native and normalized bit maps |
| migration | same semantic oracle | old/new schema diff |
from math import sqrt
def band(shots, sigma=3):
if type(shots) is not int or shots <= 0:
raise ValueError("shots must be a positive integer")
half_width = sigma / (2 * sqrt(shots))
return 0.5 - half_width, 0.5 + half_width
records = [
{"shots": 1_000, "counts": {"00": 508, "11": 492}},
{"shots": 10_000, "counts": {"00": 4_963, "11": 5_037}},
]
thousand_band = band(1_000)
ten_thousand_band = band(10_000)
assert abs(0.0) <= 1e-12 # analytic H-Z estimator oracle
for record in records:
assert set(record["counts"]) == {"00", "11"}
assert sum(record["counts"].values()) == record["shots"]
lo, hi = band(record["shots"])
assert lo <= record["counts"]["00"] / record["shots"] <= hi
invalid_rejected = False
try:
band(0)
except ValueError:
invalid_rejected = True
assert ten_thousand_band[1] - ten_thousand_band[0] < thousand_band[1] - thousand_band[0]
assert invalid_rejected
print(f"PASS: 38 primitives band1000={thousand_band} band10000={ten_thousand_band} invalid_shots={invalid_rejected}")
Transpiled evidence belongs in the record
Backend compilation and measurement conventions are evidence, not plumbing [benchmarking review]. Record target identity, native instruction set, initial/final layout, optimization level, seed, native gate counts, depth in layers, measurement map, and transpiled-circuit hash. A result from an ideal primitive and one from a named processor may share an API while supporting different claims.
Parameter binding is part of this evidence. Hash the symbolic circuit, the ordered parameter names, and the bound numeric vector separately. A batch in which angles are transposed can return correctly shaped data for the wrong experiments. Before interpreting values, assert that every submitted case ID, observable ID, and parameter-set ID appears exactly once in the result record.
Migration test for the next Qiskit release
On a schedule, create a fresh environment with the candidate version, run schema tests before numerical tests, and classify failure as import/API, metadata/schema, circuit translation, or changed numerical semantics. Never update expected output merely because the new adapter emits it. Re-derive the oracle, document the migration, and retain the last passing record.
A schema migration should be narrow enough to review. Capture the old native result and the new native result, normalize both through version-specific readers, and compare the small semantic record. If the semantic record agrees, the change is an adapter migration. If it differs, stop and inspect transpilation, parameter association, observable convention, and bit order before changing a tolerance. Wider tolerances are not a remedy for a permuted experiment batch.
The deterministic and stochastic checks deliberately fail differently. A state-vector or exact reference expectation outside 10−12 is a semantic or numerical defect; rerunning it with more shots has no meaning. A finite-shot Bell frequency outside its predeclared interval is a statistical alert whose record includes n and the test method. Repeated alerts across independent seeds may motivate model investigation, but one alert is not automatically an adapter bug.
Store the two output schemas separately. The estimator row contains observable ID, value, uncertainty source, target precision, and backend metadata. The sampler row contains native keys, logical keys, integer shots when applicable, probabilities or quasi-weights with their numeric kind, and measurement mapping. A field named result that alternates between float, array, and distribution defeats schema validation.
The optional Qiskit job must be allowed to report unavailable. That state records the pinned environment requested, platform, resolver or import failure, and last known passing version. It does not convert the framework-neutral analytic fixtures into failures, and it does not disappear from the release record. This boundary keeps a software migration from rewriting the physics oracle.
Case identifiers must survive batching, transpilation, and result extraction. Assert their cardinality and order before attaching values to observables; otherwise a perfectly shaped array can associate the Bell sample with the Hadamard expectation. Shape validation is necessary, but semantic keys make the result reviewable.
Two-shot sampler audit
Prompt: Run the same Bell circuit through a Qiskit sampler at two shot counts, retain the transpiled circuit and metadata, and explain whether the frequency difference is statistically ordinary.
Deliverable: Pinned environment file, source module, two JSON result records, and a calculation of the acceptance intervals.
Pass condition: Both records identify version/backend/layout/shots; no impossible ideal outcomes occur; observed 00 and 11 frequencies fall inside the predeclared interval.
Reference package
Format: Version-specific reference module, stored records, and analytic binomial calculation.
Verification: CI recreates the optional environment and checks schema, metadata, circuit hash, total shots, support, and interval membership.
Both records preserve only 00 and 11, their counts sum to their declared shots, and each observed 00 fraction falls within the predeclared shot-dependent interval. A difference between the two fractions is therefore ordinary sampling evidence, not a changed state, provided the transpiled circuit and bit map also match.
Companion work
Artifacts for this chapter
These entries resolve to checked-in local source. Commands are reproduced exactly from the chapter manifest, and source-embedded fixtures are exported as direct downloads.
Reproduce or test
python3 tools/validate_briefs.py --briefs data/editorial_briefs_36_63.json --from 36 --through 63 --check-rewritten-sources --execute-artifacts
Provenance
Sources and review
- IBM Quantum. Qiskit documentation. IBM. 2026official documentation
- OpenQASM Technical Steering Committee. OpenQASM 3 specification. Linux Foundation Joint Development Foundation. 2026official technical specification
- QIR Alliance. Quantum Intermediate Representation specification and projects. Linux Foundation Joint Development Foundation. 2026official technical specification
- Michael A. Nielsen and Isaac L. Chuang. Quantum Computation and Quantum Information. Cambridge University Press. 2010textbook
- Timothy Proctor et al.. Benchmarking quantum computers. Nature Reviews Physics. 2025peer-reviewed perspective
The load-bearing claims in the chapter are mapped inline to this registered source set. A citation supports only the bounded claim beside it.