CHAPTER 23
Programming reproducible quantum experiments
Learning goals. Run current local Qiskit workflows, convert wire conventions, separate sampling from exact simulation, and prepare reproducible experiments.
23.1 A program is an executable specification
A circuit diagram leaves choices that software must make explicit: register ordering, preparation, parameter units, measurement placement, classical-bit mapping, shot count, random seed, noise model, and output interpretation. These choices should travel with an experiment.
The accompanying Python examples use Qiskit 2.5.2 and were executed during this edition’s validation. This is the tested version, not a promise that it will remain the latest release. The official documentation consulted at the research cutoff uses the modern V2 primitive interfaces [71, 72, 73].
Create an isolated environment and install the pinned dependency:
python -m venv .venv
# Activate the environment using your operating system's command.
python -m pip install qiskit==2.5.2
python qiskit_examples.pyThe web chapter links to the actual script and requirements file. All examples run locally without an account, hardware reservation, API key, or cloud charge.
The complete executable
examples and pinned
requirements accompany this chapter. Run
python qiskit_examples.py in the environment above; the
script asserts the basis, ordering, Bell-state, estimator, and channel
identities before printing results.
23.2 Exact state-vector calculations
import numpy as np
from qiskit.quantum_info import Statevector, Pauli
psi = Statevector(np.array([2, 3j]) / np.sqrt(13))
for axis in ("X", "Y", "Z"):
expectation = psi.expectation_value(Pauli(axis)).real
print(axis, expectation)Expected values are 0,12/13,−5/13. These are exact simulator expectations up to floating-point error, not estimates from finite measurements. State-vector simulation should omit measurement instructions when the desired object is a coherent state before readout.
A Bell circuit is equally direct:
from qiskit import QuantumCircuit
from qiskit.quantum_info import DensityMatrix, partial_trace
bell = QuantumCircuit(2)
bell.h(0)
bell.cx(0, 1)
state = Statevector.from_instruction(bell)
reduced = partial_trace(DensityMatrix(state), [1])
print(reduced.data)The reduction is I/2. In Qiskit, the argument lists subsystems to trace out, not retain. The Bell example is symmetric; use asymmetric examples to test ordering.
23.3 Convert wire conventions deliberately
This book displays with q0 most significant. Qiskit state-vector indices give its qubit 0 the least significant weight. To preserve the book’s displayed wire labels, map book wire j to Qiskit wire n−1−j.
For three qubits, applying X to book q0 is:
asymmetric = QuantumCircuit(3)
asymmetric.x(2) # book q0 maps to Qiskit wire 2
assert np.argmax(Statevector.from_instruction(asymmetric).probabilities()) == 4Index four is binary 100 in both displayed integer conventions after this mapping. A Bell state alone would not expose a reversed-order mistake.
Classical measurement registers also have display conventions. Name them, record the qubit-to-classical-bit mapping, and inspect an asymmetric known state before interpreting a histogram.
23.4 Finite shots with a V2 sampler
from qiskit.primitives import StatevectorSampler
measured = bell.copy()
measured.measure_all()
sampler = StatevectorSampler(seed=2026)
result = sampler.run([measured], shots=2048).result()
counts = result[0].data.meas.get_counts()
print(counts)Only 00 and 11 should occur in this ideal example, with frequencies near one half. The default classical register created by measure_all is named meas, which is why the result path uses that name. A circuit with a differently named register needs the corresponding data field.
StatevectorSampler implements a local pure-state sampler and does not support mid-circuit measurements. A dynamic circuit needs a simulator or backend supporting its actual control flow. Removing an intermediate measurement to make code run changes the experiment and can change its answer.
23.5 Estimators and observables
V2 estimator primitives accept a circuit and observable specification. A local StatevectorEstimator can compute expectations of Pauli-sum observables:
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp.from_list([("XX", 1.0), ("ZZ", 1.0)])
value = StatevectorEstimator().run([(bell, observable)]).result()[0].data.evs
print(value) # 2.0 for the ideal Bell stateA hardware estimator has sampling, compilation, and service-specific options. The local exact value is a validation reference, not evidence that a physical processor will return it without uncertainty.
For hardware work, map the logical circuit to the target’s instruction set and connectivity, transform observables consistently with any layout, inspect the compiled depth and operations, then execute with explicit shots or precision. The current provider documentation governs credentials, supported modes, and costs. No cloud submission is needed for this textbook’s reproducible examples.
23.6 Other tools in the ecosystem
| Tool / interface | Useful role | What to verify |
|---|---|---|
| Qiskit | Circuit construction, quantum information, compilation, primitives | Version, register order, target instructions, V2 result shapes |
| Cirq | Circuit and device modeling, state-vector and density simulation | Qubit order, supported operations and noise |
| PennyLane | Differentiable quantum circuits and hybrid workflows | Device, interface, gradient assumptions, shots |
| Stim | Large stabilizer circuits and detector models | Clifford restrictions and detector semantics |
| OpenQASM 3 | Circuit-level language with classical control constructs | Supported subset and backend execution semantics |
These descriptions follow official technical documentation [74, 75, 32, 76]. A language supporting a feature does not imply every backend implements it. Cross-platform conversion can lose timing, control-flow, or noise semantics.
23.7 A reproducibility record
Save the source circuit, dependency versions, backend or simulator identity, compilation options, layout, noise equations, shots, seeds, raw results, and the analysis code. Hardware calibrations change; a backend name alone is insufficient.
Validate small cases before scaling. Check normalization, known basis outputs, unitary inverses, analytical probabilities, and ordering conversions. Compare independent implementations where possible. A shared bug can pass a test that merely repeats the implementation’s formula, so use known identities and independent libraries.
A random seed makes a software experiment repeatable within its stated implementation; it does not make a physical device’s measurement outcomes deterministic. A change of library version can also change a pseudorandom stream or transpilation result while preserving correct distributions.
23.8 Exercises
23.1. Which Qiskit wire represents book q1 in a four-qubit register under the mapping?
Show solution / guidance
Wire .
23.2. Why should the exact Bell-state expectation of XX+ZZ be two?
Show solution / guidance
The Bell state is a +1 eigenstate of both XX and ZZ, so their expectations sum to two.
23.3. Can the local StatevectorSampler example execute teleportation with intermediate measurements and feed-forward?
Show solution / guidance
No. Use a compatible dynamic-circuit simulator or a branch-by-branch mathematical calculation. The browser teleportation lab computes all conditioned branches explicitly.
23.4. Why pin package versions if the formulas are timeless?
Show solution / guidance
APIs, result containers, supported operations, and compilation behavior change. Pinning separates mathematical reproducibility from software-interface drift.
23.5. Name a stronger ordering test than a Bell histogram.
Show solution / guidance
Prepare an asymmetric basis state such as book , map its wires explicitly, and verify its integer index and measurement-string interpretation.