Starting point: Earlier programming chapters; relevant constructions from Quantum Computing and Quantum Algorithms.
Anchor example: Build a parameterized controlled subroutine with explicit workspace requirements.
This lesson includes explanations, worked examples, and problems with solutions. Advanced results whose proofs are outside the chapter are identified explicitly.
Every circuit so far has been written out gate by gate, and checked against its state, its matrix or its observables. Real algorithms are not written that way. They use the same construction many times: on different wires, at different angles, under the control of another qubit, or run backwards.
How can a circuit become a reliable reusable component? A Python function that builds a fresh circuit on each call is the first step. But a component is useful only if everyone agrees how its wires are to be connected, so its interface must be stated. Using a component as a single gate, controlling it and inverting it each has a trap, and one of them makes a global phase observable. Finally, components that need extra qubits to work in must promise to return those qubits clean, and that promise has to be tested on superpositions.
The result is a small set of rules that lets us trust a piece of circuit we did not just write.
5.1 Circuit Interfaces
Suppose a computation needs the same \(R_y\) rotation in several places, at different angles. We could copy its lines each time, but every copy is a chance for a mistake. A component that can be reused needs two things: a way to produce it on demand, and a stated agreement about how it connects to the rest of a circuit. We take them in that order.
5.1.1 Python circuit factories
Why not build the rotation circuit once and reuse that one object everywhere? Recall that two Python names can refer to one circuit. Suppose two experiments share the object and the first appends a measurement to it. The second experiment now contains that measurement too, although nobody added it there. Its result depends on what ran before.
The cure is a function that builds a new circuit every time it is called. Such a function is called a circuit factory. In the companion, rotation(theta) creates a one-qubit circuit, applies \(R_y(\theta)\) and returns it. The input name="rotation" labels the circuit, so the label appears when it is drawn as a box. Because the caller receives the circuit rather than a result, the caller also decides where it is placed and when it is run.
Problem 5.1. Why return a fresh circuit instead of a shared global one?
Worked solution
Appending gates mutates circuit objects. A shared object can accumulate instructions across calls and make experiments depend on call history.
5.1.2 Register contracts and composition
A factory returns a correct circuit. What can still go wrong when we plug it into a larger one? Consider a component that computes the AND of two bits into a third qubit, using a Toffoli gate. A function from input bits to one output bit is called a Boolean predicate. The component is correct only if the larger circuit attaches its wires as intended: inputs on the first two wires, the result qubit, called the target, on the third.
So the component needs a stated agreement, a contract, written beside the factory. For the AND component it reads: “wires 0 and 1 are inputs and are left unchanged; wire 2 is the target and is flipped exactly when both inputs are 1.” When we place the component with compose, its qubits argument must follow the contract. A component that is mathematically right but attached to the wrong wires is still a bug, and the bit-order convention makes such mistakes easy.
5.2 Reusable Operations
A factory gives us circuits. Algorithms ask more of a component: to act as one gate, to act only when a control qubit is 1, to run backwards, and to be evaluated at many angles. Each request has a trap. We take control first, because it reveals that a phase we called unobservable can matter, and then turn to inverses and symbolic angles.
5.2.1 Custom gates and controlled gates
Many algorithms need “apply \(U\) only if the control qubit is 1.” Could we write a host-language if around the append? No: at construction time the control qubit has no value, and in superposition it never has one. We need a single unitary that acts as \(I\) on the target when the control is \(\ket0\) and as \(U\) when it is \(\ket1\), the controlled \(U\).
Qiskit builds it in two steps. to_gate() packages a circuit as one gate object, and control(1) adds one control qubit. In qc.append(controlled, [0, 1]) the control comes first in the list, so qubit 0 controls and qubit 1 is the target. Control is not free. A controlled \(R_y\), for example, decomposes into two CNOTs and two one-qubit gates.
There is a subtler trap. Suppose a component’s circuit implements \(e^{i\varphi}I\), the identity up to a global phase. Alone, that phase changes no prediction. Is it still harmless when controlled? With the control on qubit 0, the controlled operation is \[\operatorname{diag}(1,\,e^{i\varphi},\,1,\,e^{i\varphi})=I\otimes P(\varphi),\] where \(P(\varphi)=\operatorname{diag}(1,e^{i\varphi})\) is a phase gate on the control. If the control is in \(\ket+\), it becomes \((\ket0+e^{i\varphi}\ket1)/\sqrt2\), a different state. So a global phase of the base operation becomes a relative phase on the control. A component that may be controlled must keep its global phase, and Qiskit records it in the circuit’s global_phase attribute.
5.2.2 Inverses and parameter binding
Suppose we need the rotation at 100 different angles. Must we build 100 circuits? Instead, we can build one circuit in which the angle is a symbol. Parameter("theta") creates such a symbol, and passing it to the factory gives a circuit with \(R_y(\theta)\) and \(\theta\) left open. To fix a value we call assign_parameters({angle: 0.7}). Its input is a dictionary from symbol to number, and it returns a copy of the circuit with the number in place, leaving the symbolic circuit unchanged.
Running backwards is the other request. inverse() reverses the order of the gates and replaces each by its adjoint, as for any product of unitaries. A circuit that contains measurement or reset has no such inverse, as we saw for reset.
The companion uses both. It binds the angle to 0.7, composes the bound circuit with its inverse, and checks that the product is the identity matrix. It then controls the bound rotation and draws the result.
Problem 5.2. Does U followed by U† test the intended specification?
Worked solution
It checks reversibility and the implementation of the inverse, but a wrong U can also pass. Compare with independently predicted outputs or matrices.
Problem 5.3. Can a symbolic circuit be sampled before all necessary parameters are bound?
Worked solution
Only if parameter values are supplied through a supported execution interface. Otherwise its numerical evolution is unspecified and execution should reject it.
5.3 Workspace Contracts
Some components cannot be built from their inputs and target alone. Computing the AND of three bits with Toffoli gates, for instance, needs a qubit to hold an intermediate result. What must such an extra qubit contain when the component starts, and what must the component leave in it? We first state the starting requirement, then the cleanup requirement and how to test it.
5.3.1 Ancilla allocation
Suppose we want \(t\oplus(x_0\wedge x_1\wedge x_2)\) on a target \(t\). A Toffoli has only two controls, so we first compute \(x_0\wedge x_1\) into an extra qubit \(a\), then apply a Toffoli controlled by \(a\) and \(x_2\) onto \(t\). An extra qubit used as workspace is called an ancilla.
What must \(a\) start in? A Toffoli adds \(x_0x_1\) to \(a\), so \(a\) holds \(x_0\wedge x_1\) only if it started at 0. If it started at 1, the target would receive the wrong value. So the contract must say “one clean ancilla in \(\ket0\).” A qubit whose state is unknown, which must be handed back unchanged, is called a borrowed qubit. It is a different resource and needs a different construction.
An ancilla is a qubit, not a temporary Python variable. When a Python name stops referring to it, nothing physical happens. Whatever correlation it has with the data remains.
5.3.2 Uncomputation and cleanup checks
After the second Toffoli, \(a\) still holds \(x_0\wedge x_1\). Why not leave it there? On a basis input nothing seems wrong. On a superposition \(\sum_x\alpha_x\ket x\), however, the state becomes \(\sum_x\alpha_x\ket x\ket{t\oplus f(x)}\ket{x_0x_1}\). The ancilla now carries information about the inputs, so it is entangled with them. As with one half of a Bell pair, the inputs alone lose the off-diagonal entries that later interference needs.
So we apply the first Toffoli again. It adds \(x_0x_1\) a second time and returns \(a\) to 0 for every input. Undoing a computation to clear workspace is called uncomputation. The full state then factors as \(\left(\sum_x\alpha_x\ket x\ket{t\oplus f(x)}\right)\otimes\ket0_a\).
How do we test that? Checking basis inputs one at a time shows that \(a\) returns to 0 for each, which is necessary. It does not show that the useful registers keep the right relative phases. A test on a superposition of inputs does, by comparing the whole state with the factored form above. Measuring the ancilla and always seeing 0 is useful evidence, but it says nothing about the target’s action. A phase-sensitive test of this kind is exactly what an oracle needs, and we build one when we implement oracles.
Our components now have interfaces we can trust. Running them still gives us only counts or amplitudes. How does a program get from a circuit to the particular number, a probability or an average, that it actually needs?
Encapsulating subcircuits via to_gate() or to_instruction() with explicit parameter bindings turns ad-hoc gate lists into composable subroutines.
Every reversible subroutine must satisfy a workspace contract: ancilla qubits allocated for intermediate scratch steps must be uncomputed back to \(|0\rangle\) before the subroutine returns.
Controlled versions of custom gates (.control(k)) propagate phase kickback and enable modular construction of phase estimation and LCU circuits.
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.
The factory below appends to a module-level circuit and returns it. What does each returned object contain after two calls, and why is this a bug?
from qiskit import QuantumCircuit
shared = QuantumCircuit(1)
def hadamard_layer():
shared.h(0)
return shared
first = hadamard_layer()
second = hadamard_layer()
print(first is second, first.size(), second.size())
Hint 1
Both calls return the same Python object.
Hint 2
Compare with a factory that creates a new QuantumCircuit each time.
Worked solution
Both names refer to the same circuit, which now holds two H gates, so the first result changed after it was returned. Any experiment that used it silently ran the wrong circuit. A factory should construct and return a fresh circuit on every call, as the chapter’s rotation factory does.
from qiskit import QuantumCircuit
shared = QuantumCircuit(1)
def hadamard_layer():
shared.h(0)
return shared
first = hadamard_layer()
second = hadamard_layer()
assert first is second and first.size() == 2
def fresh_hadamard_layer():
circuit = QuantumCircuit(1)
circuit.h(0)
return circuit
assert fresh_hadamard_layer() is not fresh_hadamard_layer()
After bound = component.assign_parameters({theta: 0.7}), which circuit still has a free parameter: component or bound? What does that imply for reusing component?
Hint 1
assign_parameters returns a new circuit by default.
Hint 2
Inspect each circuit’s parameters attribute.
Worked solution
component still lists theta in its parameters; bound has none. assign_parameters returns a new, bound circuit and leaves the template symbolic, so the same component can be bound to many values in a sweep without being rebuilt.
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
theta = Parameter("theta")
component = QuantumCircuit(1)
component.ry(theta, 0)
bound = component.assign_parameters({theta: 0.7})
assert len(component.parameters) == 1 and len(bound.parameters) == 0
Package ry(0.7) as a gate, add one control, and transpile the two-qubit circuit to the basis cx, rz, sx, x. How many CNOTs does the controlled rotation need?
Hint 1
A controlled single-qubit rotation needs two CNOTs.
Hint 2
Use transpile with basis_gates and count_ops.
Worked solution
The transpiled circuit uses \(2\) CNOTs plus single-qubit gates. Adding a control turned one rotation into an entangling subcircuit; for a controlled multi-gate routine the cost grows with every gate controlled. control() is a mathematical construction with a real price, not a free flag.
The chapter checks that a bound rotation composed with its inverse is the identity. (a) Show that this test also passes for a wrong rotation, ry(\(-0.7\)). (b) Write a test that checks the specification itself: the state prepared from \(\ket0\) should be \(\cos(0.35)\ket0+\sin(0.35)\ket1\). (c) What does the \(UU^\dagger\) test actually establish?
Hint 1
Every unitary composed with its own inverse is the identity.
Hint 2
Compare Statevector.from_instruction with the expected amplitudes.
Worked solution
(a) For any unitary \(U\), \(UU^\dagger=I\), so ry(\(-0.7\)) passes just as ry(\(0.7\)) does. (b) Compare the prepared statevector with the expected amplitudes; the wrong rotation gives \(\cos(0.35)\ket0-\sin(0.35)\ket1\) and fails. (c) The inverse test checks only that the circuit is unitary and that inverse() is consistent with it. It says nothing about whether the circuit implements the intended operation; tests must compare against an independent specification.
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Operator, Statevector
def rotation(angle):
circuit = QuantumCircuit(1)
circuit.ry(angle, 0)
return circuit
wrong = rotation(-0.7)
assert np.allclose(Operator(wrong.compose(wrong.inverse())).data, np.eye(2))
expected = Statevector([np.cos(0.35), np.sin(0.35)])
assert Statevector.from_instruction(rotation(0.7)).equiv(expected)
assert not Statevector.from_instruction(wrong).equiv(expected)
A predicate factory computes \(x_0\land x_1\) into a scratch qubit (2) with a Toffoli, copies it into a target (3) with a CNOT, and forgets to uncompute. (a) For the inputs in uniform superposition, find the probability that the scratch qubit is left in \(\ket1\). (b) Fix the factory and verify that the scratch qubit returns to \(\ket0\) on this coherent input. (c) Why test with a superposition rather than with each basis input separately?
Hint 1
The scratch holds \(x_0x_1\), which is \(1\) for one input in four.
Hint 2
Uncompute with a second Toffoli after the copy.
Worked solution
(a) The scratch qubit holds \(x_0x_1\), so it is \(\ket1\) with probability \(\frac14\), and it is entangled with the inputs. (b) Applying the Toffoli again after the copy returns the scratch to \(\ket0\) for every input, and its reduced state is exactly \(\ket0\bra0\). (c) Each basis input alone would show a definite scratch value and might pass a check that only looks at the target. The coherent input reveals whether the scratch factors out, which is what later interference requires.
A circuit applies ry(theta) and then rz(alpha) to one qubit. A student passes the parameter values [0.0, \(\pi/2\)] to an Estimator, intending theta \(=0\) and alpha \(=\pi/2\), and measures \(\langle X\rangle\). (a) What value do they expect? (b) Qiskit orders a circuit’s parameters by name. What value is returned, and why? (c) How should the values be supplied to avoid this?
Hint 1
list(circuit.parameters) is [alpha, theta].
Hint 2
ry(\(\pi/2\)) prepares \(\ket+\), and rz(0) leaves it alone.
Worked solution
(a) With theta \(=0\) the qubit stays \(\ket0\), so \(\langle X\rangle=0\). (b) The circuit’s parameters are ordered alpha, theta, so the values assign alpha \(=0\) and theta \(=\pi/2\): ry(\(\pi/2\)) prepares \(\ket+\) and the result is \(1\). (c) Inspect circuit.parameters before building value arrays, or bind with an explicit dictionary such as {theta: 0.0, alpha: \(\pi/2\)}. Positional parameter arrays are a frequent source of silent errors.
import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
theta, alpha = Parameter("theta"), Parameter("alpha")
circuit = QuantumCircuit(1)
circuit.ry(theta, 0)
circuit.rz(alpha, 0)
assert list(circuit.parameters) == [alpha, theta]
value = StatevectorEstimator().run([(circuit, SparsePauliOp("X"), [[0.0, np.pi / 2]])]).result()[0].data.evs
assert np.allclose(value, [1.0])
explicit = circuit.assign_parameters({theta: 0.0, alpha: np.pi / 2})
assert np.allclose(StatevectorEstimator().run([(explicit, SparsePauliOp("X"))]).result()[0].data.evs, 0.0)
rz(\(\lambda\)) and p(\(\lambda\)) differ only by a global phase. Compare their controlled versions crz(0.9) and cp(0.9) with Operator equivalence. Then find the one-qubit gate on the control that makes them equal.
Hint 1
\(R_z(\lambda)=e^{-i\lambda/2}P(\lambda)\).
Hint 2
The global phase \(e^{-i\lambda/2}\) becomes a phase on the control-1 branch.
Worked solution
The controlled versions are not equivalent: the phase \(e^{-i\lambda/2}\) that was global for the single-qubit gate now multiplies only the control-1 branch. Adding p(\(0.45\)) on the control to crz(0.9) restores the factor and makes it equal to cp(0.9). A routine correct “up to global phase” must have that phase fixed before it is controlled.
from qiskit import QuantumCircuit
from qiskit.quantum_info import Operator
phase = QuantumCircuit(2)
phase.cp(0.9, 0, 1)
rotation = QuantumCircuit(2)
rotation.crz(0.9, 0, 1)
assert not Operator(phase).equiv(Operator(rotation))
rotation.p(0.45, 0)
assert Operator(phase).equiv(Operator(rotation))
A student believes inverse() cannot be applied until all parameters are bound, “because the inverse angle is not known yet.” Build ry(theta).inverse() with theta unbound, bind theta afterwards, and check the result.
Hint 1
The inverse of ry(theta) is ry(\(-\)theta), a symbolic expression.
Hint 2
Bind after inverting and compare operators.
Worked solution
inverse() works symbolically: it produces ry(\(-\)theta), whose angle is a parameter expression. Binding theta \(=0.7\) afterwards gives the same operator as inverting the bound circuit. Symbolic circuits can be composed, inverted, and controlled before binding, which is what lets one template serve a whole parameter sweep.
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.quantum_info import Operator
theta = Parameter("theta")
circuit = QuantumCircuit(1)
circuit.ry(theta, 0)
symbolic_inverse = circuit.inverse().assign_parameters({theta: 0.7})
bound_inverse = circuit.assign_parameters({theta: 0.7}).inverse()
assert Operator(symbolic_inverse).equiv(Operator(bound_inverse))
Try to package a circuit that contains a measurement with to_gate(), and then with to_instruction(). Explain the difference between a gate and an instruction, and what it means for inverting or controlling the packaged object.
Hint 1
A gate must be unitary.
Hint 2
Instructions may act on classical bits.
Worked solution
to_gate() raises a QiskitError, because a gate represents a unitary and a measurement is not one. to_instruction() succeeds: an instruction may include classical bits and non-unitary steps. Consequently a gate can be inverted and controlled, while such an instruction generally cannot. Packaging should preserve this distinction so that later code cannot ask for an inverse that does not exist.
A borrowed (dirty) qubit \(b\) may start in any unknown state and must be returned unchanged. Show that the sequence cx(x, b), cx(b, t), cx(x, b), cx(b, t) adds \(x\) to the target \(t\) for every value of \(b\), and leaves \(b\) unchanged. Verify it on all eight basis inputs. Why is this useful?
Hint 1
After the first CNOT, \(b\) holds \(b\oplus x\).
Hint 2
The target receives \(b\oplus x\), then \(b\): the \(b\) parts cancel.
Worked solution
Tracking values: \(b\to b\oplus x\); \(t\to t\oplus b\oplus x\); \(b\to b\); \(t\to t\oplus x\). The unknown \(b\) appears twice in the target and cancels, so the sequence implements \(t\mathrel{\oplus}=x\) and restores \(b\), on all eight inputs and, by linearity, on superpositions. Borrowed qubits let a circuit use idle wires as temporary workspace without requiring them to be clean, saving qubits in large constructions.
from itertools import product
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
for x, b, t in product([0, 1], repeat=3):
circuit = QuantumCircuit(3) # qubit 0 = x, qubit 1 = b, qubit 2 = t
for qubit, value in enumerate([x, b, t]):
if value:
circuit.x(qubit)
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(0, 1)
circuit.cx(1, 2)
probabilities = Statevector.from_instruction(circuit).probabilities_dict()
assert probabilities[f"{t ^ x}{b}{x}"] > 0.999