Starting point: Earlier programming chapters; relevant constructions from Quantum Computing and Quantum Algorithms.
Anchor example: Package a complete algorithm implementation with tests, execution records, and analysis.
This lesson includes explanations, worked examples, and problems with solutions. Advanced results whose proofs are outside the chapter are identified explicitly.
Each chapter has ended with a program that passed its checks once, on one machine, with one seed. Along the way we met most of the ways that such a result can fail to transfer: versions that differ between machines, notebooks with hidden state, bit orders, layouts, seeds, noise conventions, and hardware runs without records.
What makes a quantum programming result independently reproducible? We answer by building a small but complete project, the capstone: an implementation of the Bernstein–Vazirani algorithm that recovers a hidden bit string \(s\) with one oracle query. Its code needs a structure that separates construction, experiment and tests, with pinned dependencies and a version history. Its runs need saved circuits and records complete enough that a reader can rerun and check them. The capstone itself then needs a specification, an implementation, and an honest statement of what its results show.
The capstone is small enough to verify completely, which lets every reproducibility habit be checked rather than only described.
16.1 Project Structure
A project is reproducible when another reader can reconstruct its task, its circuit, its assumptions and its evidence. A plot without the program that made it, and the record of how it ran, cannot be reconstructed. So what must the code itself look like? We decide how to divide it into files first, then how to pin and track the software it depends on.
16.1.1 Modules, notebooks, and tests
Suppose all of the capstone lived in one notebook: building the oracle, running it, and checking it. Could a reader reuse the oracle in a test without rerunning the experiment? Not easily. So we split the code by purpose. Circuit factories, such as one that builds the oracle for a given \(s\), go in a module, a Python file that other files import. An experiment driver, a script, runs the experiment from a fresh process. Tests import the same factories and check them. A notebook may still explain the experiment, but the script is what we trust.
What should the tests contain? The checks that caught real errors earlier in the course: a small case solvable by hand, an asymmetric input that exposes bit order, and a superposition input that exposes phases. And, as with the hardware program, importing a module must never submit a job.
16.1.2 Dependencies and version control
Our coin program showed that the same text can behave differently with different packages underneath it. So we pin the tested versions and record the Python version. The requirements file captures the packages the project uses directly. A full lock file and a note on the platform make an exact reconstruction possible. Updating any package means rerunning the examples, not assuming that nothing changed.
The code itself changes too. How does a result point to the code that produced it? A version-control system, such as Git, records every change to the source with an identifier, so a result can name the exact version it came from. Credentials stay out of it, as they stay out of every source file.
16.2 Experiment Records
Well-organized code still runs differently with different settings. A reader rerunning the capstone needs to know exactly what ran, and with what result. What should we save? We consider the circuits and settings first, then the results and what they cost.
16.2.1 Saved circuits and configurations
Is a drawing of the circuit enough to rerun it? No. A drawing can omit the global phase, the values bound to parameters, and other details of the object. So we save the circuit itself, in a file format that a program can load. Qiskit’s format for this is QPY, a binary file that preserves the circuit’s full structure, for loading by compatible Qiskit versions.
The companion checks a save and load in memory, without writing a file. io.BytesIO() creates an in-memory file, qpy.dump writes the circuit to it, and buffer.seek(0) moves back to its start. qpy.load then reads back a list of circuits, of which we take the first. The test requires that the restored circuit has an equivalent operator. When compilation is involved, we save the logical circuit, the compiled circuit, its layout, the parameter values and the seeds, because each of them changed results in earlier chapters.
16.2.2 Results, metadata, and execution costs
What would a reader need in order to rerun the capstone and check our claim? Each item answers a failure we have already met. The problem and its input size say what was attempted. The access assumption says what the program may do with the oracle, since the claim is about queries. The success criterion says what counts as a correct answer. The shots and the seed determine the sampled counts. The number of qubits and the logical and compiled gate counts give the cost, since compilation can multiply it. The package versions explain differences between machines.
The kind of execution matters too: exact local simulation, noisy simulation and hardware make different claims, so each result is labelled with its kind. Finally, the measured outputs belong in the record, with their confidence intervals and error budgets stated beside the conclusions rather than left in raw logs. The companion assembles such a record as a Python dictionary and prints it with json.dumps in JSON, a plain-text format that other programs can read.
16.3 Capstone Project
We now apply all of this to the capstone. The Bernstein–Vazirani problem gives us an oracle for \(f(x)=s\cdot x \bmod 2\) and asks for \(s\). Classically each query reveals one bit of \(s\), while the quantum algorithm needs one query. What exactly does our program implement, and what does a successful run establish? We write the specification and the implementation first, then evaluate them and state their limits.
16.3.1 Specification and implementation
How does one query reveal all of \(s\)? With the target in \(\ket-\), the oracle is a phase oracle, multiplying \(\ket x\) by \((-1)^{s\cdot x}\). Starting from the uniform superposition, \[H^{\otimes n}\,\frac1{\sqrt{2^n}}\sum_x(-1)^{s\cdot x}\ket x=\ket s,\] because \(H^{\otimes n}\ket s\) is exactly that superposition and \(H^{\otimes n}\) is its own inverse. So measuring the input register gives \(s\) with certainty.
How is the oracle built? Since \(s\cdot x=\bigoplus_{j:\,s_j=1}x_j\), it needs one CNOT from input qubit \(j\) to the target for each bit \(s_j=1\), exactly as we built an XOR term in our first oracle. The secret "1011" is written high bit first, like Qiskit’s strings. So the code walks through reversed(secret), and enumerate pairs each character with its qubit index, starting from qubit 0. Passing range(n+1) to h applies H to all those qubits at once.
Is the program discovering a secret? The program knows \(s\), because it built the oracle from it. The algorithm’s claim concerns queries: given only access to the oracle, one query suffices. Knowing \(s\) is what lets us verify the construction.
16.3.2 Evaluation, limitations, and documentation
What should the output look like? The companion uses \(s=1011\) and also measures the target. The target, qubit \(n\), is the leftmost character of each string. It stays in \(\ket-\), so it reads 0 or 1 at random. With seed 41 the counts were 540 for 11011 and 484 for 01011. The expression bits[1:] drops the first character, and the check requires that the remaining four bits equal the secret in every observed string.
"""Runnable companion for Quantum Programming, Chapter 16."""
import io, json, sys
from importlib.metadata import version
from qiskit import QuantumCircuit, qpy
from qiskit.primitives import StatevectorSampler
from qiskit.quantum_info import Operator
secret = "1011" # Written high bit first.
n = len(secret)
qc = QuantumCircuit(n+1)
qc.x(n); qc.h(range(n+1))
for qubit, bit in enumerate(reversed(secret)):
if bit == "1":
qc.cx(qubit, n)
qc.h(range(n))
# Save/load the coherent circuit before appending measurements.
buffer = io.BytesIO()
qpy.dump(qc, buffer); buffer.seek(0)
restored = qpy.load(buffer)[0]
assert Operator(qc).equiv(Operator(restored))
qc.measure_all()
counts = StatevectorSampler(seed=41).run([qc], shots=1024).result()[0].data.meas.get_counts()
assert all(bits[1:] == secret for bits in counts)
record = {"problem": "Bernstein-Vazirani", "secret": secret,
"python": sys.version.split()[0], "qiskit": version("qiskit"),
"seed": 41, "shots": 1024, "qubits": qc.num_qubits,
"gate_counts": dict(qc.count_ops()), "counts": counts,
"execution": "local ideal StatevectorSampler", "oracle_queries_per_shot": 1}
print(json.dumps(record, indent=2))
Problem 16.1. Why does the target bit vary in the capstone output?
Worked solution
It remains in minus, which gives a fair computational-basis result. The useful secret is in the other bits, so the decoder excludes the target bit.
Problem 16.2. What changes if the secret is all zeros?
Worked solution
The oracle has no CNOTs and the input Hadamards cancel, so the output secret is all zeros. This is an important edge case.
What does the run establish? That this implementation is correct for one secret of length 4, in ideal local simulation. To make it a study, we extend it across lengths, and test the edge cases \(s=0\cdots0\) and \(s=1\cdots1\). We report how the oracle’s gate count grows with \(n\), and compare with the classical baseline of \(n\) queries under the same oracle contract. The result is a reproducible implementation study. It is not a claim of speedup on hardware.
Problem 16.3. What evidence would be needed for a runtime advantage claim?
Worked solution
A scalable task and access model, matched classical baseline, complete preparation and oracle costs, execution and readout costs, comparable success and precision, and measurements or justified resource analysis over growing sizes.
Phase-oracle readout. U_f is the reversible bit oracle on the input register and the minus target, not an elementary gate. The final input measurement reads the encoded pattern.
The companion programs of this course form a locally tested starting point for such projects. Each one states its prediction, checks it, and keeps any hardware submission explicit.
An independently reproducible quantum project packages source code, environment lockfiles, random seeds, transpiled circuits, raw shot counts, and analysis scripts in a self-contained repository.
Every reported plot or table should be generated deterministically from archived raw execution records with explicit error bars and stated assumptions.
Clear separation between exact small-instance verification, noisy simulation, and hardware execution prevents overclaiming algorithmic performance.
Questions and worked problems
These exercises draw on the whole chapter. Attempt each question before opening a hint, explain your reasoning, then compare it with the worked solution.
For the capstone’s Bernstein–Vazirani construction, which input qubits receive CNOTs to the target for the secret 0110? What should the input register read?
Hint 1
The secret is written high bit first and reversed when assigning qubits.
Hint 2
Qubit \(k\) gets a CNOT if bit \(k\) (from the right) is \(1\).
Worked solution
Reading 0110 from the right, bits \(1\) and \(2\) are \(1\), so qubits 1 and 2 control CNOTs onto the target. The input register then reads 0110 with certainty (in Qiskit’s order, qubit 0 rightmost), and the full measured string has the target bit on the left.
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
def bernstein_vazirani(secret):
n = len(secret)
circuit = QuantumCircuit(n + 1)
circuit.x(n)
circuit.h(range(n + 1))
for qubit, bit in enumerate(reversed(secret)):
if bit == "1":
circuit.cx(qubit, n)
circuit.h(range(n))
return circuit
circuit = bernstein_vazirani("0110")
controls = [circuit.find_bit(i.qubits[0]).index for i in circuit.data if i.operation.name == "cx"]
assert controls == [1, 2]
circuit.measure_all()
counts = StatevectorSampler(seed=1).run([circuit], shots=256).result()[0].data.meas.get_counts()
assert all(bits[1:] == "0110" for bits in counts)
For an \(n\)-bit secret with \(k\) ones, count the gates of the capstone circuit (before measurement). Check the counts and the depth for the all-ones secret with \(n=10\).
Hint 1
One X, \(n+1\) Hadamards before, \(n\) after, and \(k\) CNOTs.
Hint 2
All CNOTs share the target, so they run sequentially.
Worked solution
The circuit has \(1\) X, \(2n+1\) Hadamards, and \(k\) CNOTs. For \(n=10\), \(k=10\): \(21\) H, \(10\) CNOTs, depth \(13\), since the CNOTs all target the same qubit and run one after another. The query count is one, but the oracle’s gate count and depth grow with the secret, which a resource report must include.
from qiskit import QuantumCircuit
def bernstein_vazirani(secret):
n = len(secret)
circuit = QuantumCircuit(n + 1)
circuit.x(n)
circuit.h(range(n + 1))
for qubit, bit in enumerate(reversed(secret)):
if bit == "1":
circuit.cx(qubit, n)
circuit.h(range(n))
return circuit
circuit = bernstein_vazirani("1" * 10)
assert dict(circuit.count_ops()) == {"h": 21, "cx": 10, "x": 1} and circuit.depth() == 13
A project records the Qiskit version but not the versions of Aer, NumPy, or the Python interpreter. Which results could fail to reproduce, and what file in this course records the complete environment?
Hint 1
Simulators, random-number streams, and numerical libraries all have versions.
Hint 2
The course keeps two dependency files.
Worked solution
Noisy-simulation results depend on the Aer version; numerical tolerances and random streams can change with NumPy and Python; even ideal sampling with a fixed seed can differ between versions of the primitives. requirements.txt pins the direct dependencies, and requirements-lock.txt records the full resolved environment, while TESTING.md notes the platform. A record should cite all of them.
Turn the capstone into a tested function. (a) Write bernstein_vazirani(secret) and a check that samples the circuit and verifies the input register for every shot. (b) Apply it to 0000, 1111, and three random six-bit secrets from a seeded generator. (c) Why include the all-zero and all-one secrets?
Hint 1
Use bits[1:] to drop the target bit.
Hint 2
Edge cases test the empty oracle and the busiest oracle.
Worked solution
The function passes for all five secrets. The all-zero secret has no CNOTs at all, so it checks that the Hadamard layers alone return \(\ket{0^n}\) and that an empty oracle does not break the construction; the all-one secret exercises every CNOT and the longest sequential chain on the target. Random secrets cover typical cases. Edge cases and random cases together make a stronger test than the single secret in the companion.
import random
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
def bernstein_vazirani(secret):
n = len(secret)
circuit = QuantumCircuit(n + 1)
circuit.x(n)
circuit.h(range(n + 1))
for qubit, bit in enumerate(reversed(secret)):
if bit == "1":
circuit.cx(qubit, n)
circuit.h(range(n))
circuit.measure_all()
return circuit
def recovers(secret, seed=41):
counts = StatevectorSampler(seed=seed).run([bernstein_vazirani(secret)], shots=256).result()[0].data.meas.get_counts()
return all(bits[1:] == secret for bits in counts)
rng = random.Random(7)
secrets = ["0000", "1111"] + ["".join(rng.choice("01") for _ in range(6)) for _ in range(3)]
assert all(recovers(secret) for secret in secrets)
Verify the oracle’s phase contract directly: after the oracle acts on the uniform input with the target in \(\ket-\), the input amplitudes should be \((-1)^{s\cdot x}/2^{n/2}\). Implement the check for the secret 1011 by returning the target to \(\ket0\) and comparing the statevector with the predicted sign pattern.
Hint 1
After the oracle, H and then X on the target return \(\ket-\) to \(\ket0\).
Hint 2
\(s\cdot x\) is the parity of the bitwise AND of the integers.
Worked solution
Undoing the target preparation leaves the state \(\sum_x(-1)^{s\cdot x}\ket x/4\otimes\ket0\). Comparing the first \(16\) amplitudes (target \(0\)) with \((-1)^{\mathrm{popcount}(s\,\&\,x)}/4\) confirms the phase oracle exactly. This tests the oracle’s promised behavior independently of the final Hadamards, so a failure would point to the oracle rather than to the readout.
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
secret = "1011"
n = len(secret)
s = int(secret, 2)
circuit = QuantumCircuit(n + 1)
circuit.x(n)
circuit.h(range(n + 1))
for qubit, bit in enumerate(reversed(secret)):
if bit == "1":
circuit.cx(qubit, n)
circuit.h(n)
circuit.x(n)
amplitudes = Statevector.from_instruction(circuit).data[: 2 ** n]
expected = np.array([(-1) ** bin(s & x).count("1") for x in range(2 ** n)]) / 2 ** (n / 2)
assert np.allclose(amplitudes, expected)
Build an experiment record for the capstone that a reader can verify. Include the problem, secret, shots, seed, and package versions for qiskit, qiskit-aer, and numpy, and add a SHA-256 fingerprint of the QPY-serialized coherent circuit. Why is the fingerprint useful?
The record below stores the descriptive fields, the versions, and a hash of the serialized circuit. The fingerprint identifies exactly which circuit produced the results: if someone later regenerates the circuit and the hash differs, something in the construction or the software changed. Storing the QPY file itself lets them load and rerun that exact circuit. (QPY files can depend on the Qiskit version, so the version belongs next to the hash.)
import hashlib, io, json
from importlib.metadata import version
from qiskit import QuantumCircuit, qpy
secret = "1011"
n = len(secret)
circuit = QuantumCircuit(n + 1)
circuit.x(n)
circuit.h(range(n + 1))
for qubit, bit in enumerate(reversed(secret)):
if bit == "1":
circuit.cx(qubit, n)
circuit.h(range(n))
buffer = io.BytesIO()
qpy.dump(circuit, buffer)
record = {
"problem": "Bernstein-Vazirani", "secret": secret, "shots": 1024, "seed": 41,
"versions": {name: version(name) for name in ("qiskit", "qiskit-aer", "numpy")},
"circuit_sha256": hashlib.sha256(buffer.getvalue()).hexdigest(),
"execution": "local ideal StatevectorSampler",
}
assert len(record["circuit_sha256"]) == 64
print(json.dumps(record, indent=2))
Run the capstone (secret 1011) on a noisy simulator in which every CNOT suffers two-qubit depolarizing error \(0.02\). (a) Estimate the fraction of shots whose input register equals the secret. (b) Why is it below \(1\), and how would it change for a longer secret with more ones? (c) How should the result be labelled?
Hint 1
Three CNOTs each carry error \(0.02\).
Hint 2
Errors grow with the number of ones in the secret.
Worked solution
(a) About \(0.96\) with \(8000\) shots. (b) Each of the three CNOTs fails with probability about \(0.02\), and many failures corrupt the readout, so success is roughly \(1-3\times0.02\times(\text{fraction harmful})\). A secret with \(k\) ones has \(k\) noisy CNOTs, so success falls roughly linearly in \(k\) for small errors. (c) As a noisy simulation with a stated, hypothetical noise model, distinct from both ideal simulation and hardware, with the model parameters, seed, and shots recorded.
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel, depolarizing_error
secret = "1011"
n = len(secret)
circuit = QuantumCircuit(n + 1)
circuit.x(n)
circuit.h(range(n + 1))
for qubit, bit in enumerate(reversed(secret)):
if bit == "1":
circuit.cx(qubit, n)
circuit.h(range(n))
circuit.measure_all()
noise = NoiseModel()
noise.add_all_qubit_quantum_error(depolarizing_error(0.02, 2), ["cx"])
sim = AerSimulator(noise_model=noise, seed_simulator=3, max_parallel_threads=1)
shots = 8000
counts = sim.run(transpile(circuit, sim, optimization_level=0), shots=shots).result().get_counts()
success = sum(count for bits, count in counts.items() if bits[1:] == secret) / shots
assert 0.9 < success < 0.99
A student argues: “The capstone finds a \(50\)-bit secret with one query, while a classical algorithm needs \(50\), so this project demonstrates a quantum speedup.” List what the project actually demonstrates and what a speedup claim would require.
Hint 1
The oracle was built from the known secret.
Hint 2
Compare end-to-end costs under the same access model.
Worked solution
The project demonstrates a correct, reproducible implementation of the algorithm in the query model, where the oracle is a supplied interface. Here the oracle was constructed from the known secret, with one CNOT per one-bit, so reading the circuit reveals the secret and building it costs as many gates as the classical queries it replaces. A speedup claim would need a setting where the oracle is genuinely given as a black box, matched classical and quantum access, end-to-end costs including oracle construction and noise, and measured performance as \(n\) grows, not a single success.
Extend the capstone study to \(n=50\). (a) Why is a statevector simulation impossible, and why can Aer’s stabilizer method still run the circuit? (b) Run it for the secret “10” repeated \(25\) times and check the result. (c) What does this say about choosing a simulation method in a scaling study?
Hint 1
The circuit uses only X, H, and CNOT.
Hint 2
A statevector of \(51\) qubits needs \(2^{51}\) amplitudes.
Worked solution
(a) \(51\) qubits need \(2^{51}\approx2\times10^{15}\) amplitudes, about \(36\) petabytes. The circuit uses only Clifford gates (X, H, CNOT), which the stabilizer method simulates with polynomial resources. (b) Every shot returns the secret in the input register. (c) The simulator must match the circuit class: stabilizer methods scale for Clifford circuits, tensor networks for low-entanglement ones, statevectors for small general circuits. A scaling study should state which was used, since simulation cost is a property of the method, not evidence about quantum hardware.
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
secret = "10" * 25
n = len(secret)
circuit = QuantumCircuit(n + 1)
circuit.x(n)
circuit.h(range(n + 1))
for qubit, bit in enumerate(reversed(secret)):
if bit == "1":
circuit.cx(qubit, n)
circuit.h(range(n))
circuit.measure_all()
counts = AerSimulator(method="stabilizer").run(circuit, shots=20, seed_simulator=1).result().get_counts()
assert {bits[1:] for bits in counts} == {secret}
Design the directory layout and test plan for turning the capstone into a reusable project. Name the modules, what each test checks, and how hardware submission is kept from happening by accident.
Hint 1
Separate circuit factories, experiment drivers, and tests.
Hint 2
Keep submission behind an explicit flag.
Worked solution
A reasonable layout: a package module with the circuit factory (bernstein_vazirani) and the oracle; an experiment driver script that builds, compiles, runs, and writes a JSON record and QPY file; a tests directory with exact tests (phase contract, statevector output for small \(n\), edge-case secrets), seeded sampling tests, a noisy-simulation smoke test, and a serialization round-trip test; and requirements and lock files with a README describing the environment. The driver submits to hardware only with an explicit option and backend name, and importing any module never submits. Continuous testing reruns the suite whenever dependencies change.