Starting point: Earlier programming chapters; relevant constructions from Quantum Computing and Quantum Algorithms.
Anchor example: Construct and run a one-qubit circuit in a clean Python environment.
This lesson includes explanations, worked examples, and problems with solutions. Advanced results whose proofs are outside the chapter are identified explicitly.
We have read a first program: a function that builds the Bell circuit and a check that its amplitudes match our prediction. That check passed on one computer. A program is useful only if someone else can run it and get the same answer, and quantum programs add a difficulty of their own, because the outcomes of measurement are random.
So how do we make a quantum program reproducible and executable locally? We need a little more Python, so we learn it on the simplest experiment with a predictable result, a one-qubit coin. Running that program on another machine means installing the same software, so we fix the versions in an isolated environment. The software comes as several packages, and we ask which of them a local experiment needs. Finally we run the coin and decide which random outputs count as correct.
By the end we have a program whose output we can predict, reproduce and test.
2.1 Python and Environments
Our experiment starts from \(\ket0\), applies H, and measures, many times. Since \(H\ket0=(\ket0+\ket1)/\sqrt2\), each outcome has probability one half, so we expect a fair coin. Writing this program needs a few Python features beyond the Bell function, and running it elsewhere needs the same software. We take the language first and the software second.
2.1.1 Python through circuit examples
How do we tell the computer to build \(H\ket0\), measure it many times, and count the outcomes? The building part uses only what we already know. We name a one-qubit circuit and append gates with method calls:
The method measure_all() measures every qubit. It also adds a classical register, a row of classical bits that holds the results, and names it meas.
Running the circuit is the job of a sampler, an object that runs circuits and returns their measured outcomes. A sampler is built to run many circuits in one request, so it expects a list of circuits, even when we have only one. Square brackets make a list: [circuit] is a list with one item, and [a, b] has two. Inputs can also be given by name, as in shots=4096, which asks for 4096 repetitions.
What comes back is a tally: how many times each bit string appeared. Python stores such a tally as a dictionary, a table that maps each key to a value. A run might return {'1': 2030, '0': 2066}, where the keys are the bit strings and the values are their counts. We look up a value with counts.get("1", 0). The second argument is what we get if the key is missing, which happens when a string never occurred. The expression sum(counts.values()) adds all the counts.
Finally, we turn our prediction into checks with assert, as in the Bell program. The test == asks whether two values are equal, < asks whether one is smaller, and abs gives an absolute value.
2.1.2 Packages, versions, and isolated environments
Suppose the coin program runs on our computer but fails on a friend’s, with an error saying some name does not exist. How can the same text behave differently?
The program is not the only code involved. Its import lines pull in packages, bundles of code written by others and installed separately, such as qiskit. Each package exists in many versions, and between versions names change and behaviour can change. A package also relies on other packages, its dependencies, which have versions of their own. So the friend’s machine runs our text with different code underneath it.
The remedy has two parts. First we record the exact versions we tested. The file requirements.txt lists them as lines such as qiskit==2.6.0, which pins that package to one version. A second file, requirements-lock.txt, records every package that was installed, including the dependencies of dependencies.
Second, we install those versions in a virtual environment, a folder holding its own Python and its own packages, separate from the rest of the computer. Other projects then cannot change our versions, and we cannot change theirs. From the repository root, the first command below creates the environment, and the second switches the terminal to it. The third uses pip, Python’s package installer, to install the pinned versions. The fourth runs the companion program. The accompanying TESTING.md records the Python version and platform on which the companions were tested.
The requirements file installs three Qiskit packages, not one. Why three, and which do we need to flip a simulated coin? Once the software is installed, we must also decide where the program lives: in a script or in a notebook. The packages come first, because they decide what can run locally.
2.2.1 SDK, Aer, and Runtime
Which pieces do we need to run the coin on our own computer, and which only to reach real hardware? Each package answers a different need.
The package qiskit is the SDK itself. It builds circuits and computes quantum-information objects such as Statevector. It contains the compiler that rewrites circuits for a particular device. It also supplies reference primitives: standard interfaces for two basic requests, “sample these circuits” and “estimate these expectation values,” carried out by exact classical simulation. Our coin needs nothing more, because its sampler is one of these primitives.
Exact simulation of an ideal circuit is not always enough. When we later want faster simulators, or simulators that imitate a noisy device, we use the separate package qiskit-aer, called Aer. To send circuits to IBM’s remote devices we use qiskit-ibm-runtime, which needs an account. Importing that package contacts no one: only an explicit submission does. So none of the local examples needs an account or a token.
2.2.2 Notebooks and executable scripts
Suppose we develop the coin in a notebook, where code sits in cells that we run one at a time. We set shots = 100 in one cell and run it. Later we change that cell to shots = 4096, run a different cell, and delete the first one. Can a reader reproduce our result from the notebook they receive?
Not reliably. A notebook keeps every variable from the cells run so far, in whatever order we ran them. The result can depend on a cell that no longer exists. A script is a plain file that Python runs from top to bottom in a fresh process, so nothing hidden survives from before. If we use a notebook, we restart it and run all cells in order before trusting a result. The tested version of each experiment in this course is a script.
What else must we write down? The versions, the parameters, the number of shots, and the seed. A simulator does not use true randomness. It uses a pseudorandom number generator, a fixed algorithm whose outputs look random and are determined by a starting number called the seed. The same seed with the same implementation gives the same sequence of outcomes. A new version may consume the generator differently, so a seed reproduces a run only within one implementation.
Problem 2.1. What does changing the seed change?
Worked solution
It changes the pseudorandom shot sequence, not the ideal quantum probability. A changed probability indicates a different circuit, model, or execution setting.
2.3 A First Program
We now have the language, a pinned environment and the packages. We can run the coin. Two questions remain: what exactly does the program get back, and which outputs should we accept as correct? The first is about reading results, and the second about the randomness that every quantum program must allow for.
2.3.1 Build, run, and inspect
What happens between handing the sampler our circuit and holding a dictionary of counts? The program below writes each step on its own line, so that we can name it.
The first two imports let the program report the Python and Qiskit versions it ran with, the record we decided to keep. The next two bring in the circuit class and StatevectorSampler, a reference sampler.
After building the circuit, sampler = StatevectorSampler(seed=7) creates a sampler with a fixed seed. Its method run does not hand back counts directly. It returns a job, a handle to work that may still be in progress. This matters for remote devices, where a job can wait in a queue. Calling job.result() waits until the work is done and fetches the result.
The result holds one entry for each circuit in our list. Square brackets after it select an entry by position, starting from zero, so result[0] is the entry for our only circuit. Inside it, .data.meas is the record of the register meas, and get_counts() turns that record into the dictionary.
"""Runnable companion for Quantum Programming, Chapter 2."""
import sys
from importlib.metadata import version
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
circuit = QuantumCircuit(1)
circuit.h(0)
circuit.measure_all()
sampler = StatevectorSampler(seed=7)
job = sampler.run([circuit], shots=4096)
result = job.result()
counts = result[0].data.meas.get_counts()
assert sum(counts.values()) == 4096
assert abs(counts.get("1", 0) / 4096 - 0.5) < 0.05
print("Python", sys.version.split()[0], "Qiskit", version("qiskit"))
print(counts)
The sampler computed the amplitudes \((1/\sqrt2,1/\sqrt2)\) internally to generate its samples. What we received, though, is only a classical record of outcomes, the same kind of record a physical device would give.
Problem 2.2. Where are amplitudes measured in this example?
Worked solution
They are not physically measured. The reference simulator computes them classically to generate samples. Hardware can provide measurement outcomes, not its complete internal state vector.
2.3.2 Predictions and reproducible results
Which counts should our test accept? The obvious attempt demands exactly 2048 ones out of \(R=4096\) shots. Our run gave 2030, and the circuit is correct. So how far from 2048 may a correct run land?
Let \(b_k\) be 1 if shot \(k\) gives one and 0 otherwise, with probability \(p=\tfrac12\) of a one. A single \(b_k\) has mean \(p\). Its variance is \(\mathbb E[b_k^2]-p^2=p-p^2=p(1-p)\), because \(b_k^2=b_k\). The frequency is \(f=(b_1+\dots+b_R)/R\). For independent shots the variances add, and dividing by \(R\) divides the variance by \(R^2\), so \[\operatorname{Var} f=\frac{p(1-p)}{R},\qquad \text{standard deviation}=\sqrt{\frac{p(1-p)}{R}}=\frac1{2\sqrt R}=\frac1{128}.\]
The test accepts any frequency within 0.05 of one half. That is \(0.05\times128=6.4\) standard deviations. For so many shots the frequency is very nearly normally distributed, and a correct circuit lands outside that window with probability about \(2\times10^{-10}\). Our run lies \(18/4096\approx0.0044\) from one half, well inside. A second check confirms that all 4096 shots are present.
What does a pass establish? Only that the outcome distribution looks fair. The circuit that prepares \((\ket0-\ket1)/\sqrt2\) would pass too, and so would a classical coin, so a pass is not evidence that we applied H rather than something else. The fixed seed adds a different kind of value. With the same seed and versions, the program prints the same counts every time, so a change in the output tells us that something in the code or the environment changed. Re-running a fixed program to detect such changes is called regression testing.
Problem 2.3. Why should a fair coin not be tested by demanding exactly 2048 ones?
Worked solution
Even an ideal fair source fluctuates. Exact equality would reject many correct executions. Test a distribution with a justified tolerance and test the unitary separately.
Our coin had a single qubit, so each outcome was one character. With two qubits a sampler returns strings such as 01. Which character belongs to qubit 0?
Reproducible quantum software pins exact package versions, records random seeds, and separates circuit construction from backend execution.
In Qiskit, QuantumCircuit defines the logical program, quantum_info supplies exact linear-algebra inspection, and primitives (Sampler and Estimator) standardize execution interfaces.
Qiskit uses little-endian bit ordering (qubit 0 is the rightmost, least-significant bit in string labels and tensor products), a convention that must be tracked consistently across circuits and matrices.
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 \(1024\) fair-coin shots, find the expected count of 1 and the standard deviations of the count and of the frequency. Would \(540\) ones be surprising?
Hint 1
Count standard deviation \(\sqrt{Rp(1-p)}\).
Hint 2
\(540-512=28\).
Worked solution
Expected count \(512\); count standard deviation \(\sqrt{1024/4}=16\); frequency standard deviation \(1/(2\sqrt{1024})=1/64\approx0.016\). A count of \(540\) is \(28/16\approx1.75\) standard deviations high: unusual but not alarming. Tests should use tolerances of several standard deviations, chosen in advance.
A circuit is built with its own classical register: QuantumCircuit(1, 1), H, then measure(0, 0). The sampler result has no field named meas. Under which name are the counts stored, and why?
Hint 1
measure_all creates a register named meas.
Hint 2
QuantumCircuit(1, 1) creates a classical register named c.
Worked solution
The counts are under data.c. QuantumCircuit(1, 1) creates a default classical register named c, and the sampler’s result fields are named after the circuit’s classical registers. measure_all adds a new register named meas, which is why the chapter’s example used data.meas. Code that hard-codes the name breaks when the circuit is built differently.
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
circuit = QuantumCircuit(1, 1)
circuit.h(0)
circuit.measure(0, 0)
result = StatevectorSampler(seed=7).run([circuit], shots=100).result()[0]
assert list(result.data.keys()) == ["c"]
print(result.data.c.get_counts())
Which package is needed for each task: (a) sampling an ideal circuit locally with StatevectorSampler; (b) simulating with a noise model; (c) submitting a job to an IBM device? Does importing the remote client submit anything?
Hint 1
Section 2.2.1 separates the SDK, Aer, and Runtime.
Hint 2
Submission needs an account and an explicit call.
Worked solution
(a) The Qiskit SDK. (b) Qiskit Aer, the separate simulation package with noise models. (c) IBM Runtime. Importing the Runtime client only loads code; submitting requires credentials, a chosen backend, and an explicit run call, which is why the course keeps such calls behind explicit options.
Prepare a qubit with ry(\(\theta\)) so that it reads 1 with probability \(0.75\). (a) Find \(\theta\) using \(P(1)=\sin^2(\theta/2)\). (b) For \(4096\) shots, find the expected count and a five-standard-deviation tolerance on the frequency. (c) Write a test that checks the sampled frequency against this tolerance.
Hint 1
\(\sin^2(\theta/2)=3/4\) gives \(\theta=2\pi/3\).
Hint 2
Frequency standard deviation \(\sqrt{0.75\times0.25/4096}\approx0.0068\).
Worked solution
(a) \(\theta/2=\pi/3\), so \(\theta=2\pi/3\). (b) Expected count \(3072\); the frequency standard deviation is \(\sqrt{0.1875/4096}\approx0.0068\), so a five-sigma tolerance is about \(0.034\). (c) The test below asserts that the observed frequency lies within that tolerance of \(0.75\); with a fixed seed it is also a reproducible regression test.
The chapter’s test asserts \(|f-0.5|<0.05\) for \(4096\) fair-coin shots. (a) How many standard deviations is \(0.05\)? Estimate how often a correct circuit fails the test. (b) A buggy circuit has \(P(1)=0.45\). How often does it pass? (c) What does this say about choosing shots and tolerance together?
Hint 1
Frequency standard deviation \(1/128\approx0.0078\).
Hint 2
For \(p=0.45\), the mean sits exactly at the tolerance edge.
Worked solution
(a) \(0.05/0.0078\approx6.4\) standard deviations; a correct circuit fails with probability around \(10^{-10}\). (b) For \(p=0.45\), the observed frequency is centred on \(0.45\), exactly at the boundary, so the bug passes about half the time. (c) A wide tolerance avoids false alarms but misses moderate bugs. Choosing the smallest error that must be detected, and then the shots needed to resolve it at the desired confidence, gives a test that is both reliable and sensitive; checking exact probabilities with Statevector avoids sampling altogether when possible.
Run a single sampler call on two circuits: H then measure, and X then measure, with \(1000\) shots and a fixed seed. How do you access each circuit’s counts, and what should each contain?
Hint 1
result() returns one entry per submitted circuit, in order.
Hint 2
X then measure always gives 1.
Worked solution
The result is indexed in submission order: result()[0] for the H circuit and result()[1] for the X circuit, each with its own data.meas. The H counts are near \(500/500\); the X counts are exactly {'1': 1000}. Batching circuits in one call is efficient, but the index ties each result to its circuit, so the order must be preserved.
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
h = QuantumCircuit(1)
h.h(0)
h.measure_all()
x = QuantumCircuit(1)
x.x(0)
x.measure_all()
results = StatevectorSampler(seed=3).run([h, x], shots=1000).result()
assert results[1].data.meas.get_counts() == {"1": 1000}
assert abs(results[0].data.meas.get_counts().get("1", 0) - 500) < 80
Show what a seed does. (a) Run the fair-coin circuit twice with seed 7 and once with seed 8. Which counts agree? (b) What does a fixed seed guarantee, and what does it not guarantee?
Hint 1
A seed fixes the pseudorandom sequence of one implementation.
Hint 2
Different library versions may consume random numbers differently.
Worked solution
(a) The two seed-7 runs give identical counts; the seed-8 run gives different counts, still near \(2048\). (b) A seed makes a particular sampling run repeatable with the same software and settings, which is valuable for regression tests. It does not make the result “correct,” it does not guarantee identical counts across library versions or simulators, and it does not remove statistical uncertainty: a seeded sample is still one sample.
A student increases the shots from \(4096\) to \(40960\) with the same seed and expects every count to be exactly ten times larger. Why is this wrong, and how do the frequency fluctuations change?
Hint 1
A new shot count changes the sampled sequence.
Hint 2
The frequency standard deviation scales as \(1/\sqrt R\).
Worked solution
Counts are random samples: with more shots the sampler draws a different and longer sequence, so the new counts are not a scaled copy of the old ones. What changes predictably is the precision: the frequency standard deviation shrinks by \(\sqrt{10}\approx3.2\), from about \(0.0078\) to \(0.0025\). Expect the frequency to move closer to \(0.5\) on average, not the counts to scale exactly.
Write a reusable test helper that decides whether counts from a fair-coin circuit are consistent with probability \(\frac12\), using a tolerance of \(z\) standard deviations of the frequency. Apply it to a seeded run. How would you choose \(z\) for a suite of \(100\) such tests?
Hint 1
Tolerance \(z/(2\sqrt{R})\) for a fair coin.
Hint 2
With many tests, the chance that some correct test fails adds up.
Worked solution
The helper below computes the tolerance \(z/(2\sqrt R)\) and compares the observed frequency. With \(z=5\) a correct test fails with probability under \(10^{-6}\), so even \(100\) tests rarely raise a false alarm (about \(10^{-4}\) in total); with \(z=2\), about \(5\%\) of correct tests would fail somewhere in every run of the suite. The number of tests, the acceptable false-alarm rate, and the smallest bug to catch together determine \(z\) and the shots.
For deterministic properties, sampling is unnecessary. Use Statevector to obtain the exact outcome probabilities of the ry(\(2\pi/3\)) circuit (after removing its final measurements), and test them against \(0.25\) and \(0.75\). When is sampling still the right test?
Hint 1
remove_final_measurements(inplace=False) returns a unitary circuit.
Hint 2
probabilities_dict gives exact probabilities keyed by bit string.
Worked solution
Statevector computes the exact probabilities \(0.25\) and \(0.75\), so the test can use a tight numerical tolerance and has no statistical failures. Sampling remains the right test when the behavior being checked is statistical or involves classical processing of samples: estimator code, mid-circuit measurements with feedback, noise simulations, or the sampler interface itself. Use exact methods for the mathematics and sampling for the pipeline.
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
circuit = QuantumCircuit(1)
circuit.ry(2 * np.pi / 3, 0)
circuit.measure_all()
probabilities = Statevector.from_instruction(circuit.remove_final_measurements(inplace=False)).probabilities_dict()
assert abs(probabilities["0"] - 0.25) < 1e-12 and abs(probabilities["1"] - 0.75) < 1e-12