Case Study: Validating a Gate Library Before You Trust It
Executive Summary
A colleague hands you an in-house Python module, gates.py, containing hand-typed matrices for the standard single- and two-qubit gates. It has been used in three internal projects. Nobody has checked it.
Hand-typed matrices are exactly the kind of code that looks obviously correct and silently isn't — a transposed sign, a missing $i$, a factor of $\sqrt2$ in the wrong place. The mathematics of Chapter 3 gives you a complete set of machine-checkable properties, so you never have to eyeball a matrix again. In this case study you will build a validation suite from first principles, run it, and find the three bugs planted in the module.
Skills applied
- Testing unitarity via $U^\dagger U = I$ (§3.6).
- Testing Hermiticity and involutivity where the gate should have them (§3.7).
- Checking eigenvalues against the theoretical spectrum (§3.9).
- Verifying documented algebraic identities as behavioral tests (§3.8).
Background
The module under test
import numpy as np
I = np.array([[1, 0], [0, 1]], dtype=complex)
X = np.array([[0, 1], [1, 0]], dtype=complex)
Y = np.array([[0, -1j], [1j, 0]], dtype=complex)
Z = np.array([[1, 0], [0, -1]], dtype=complex)
H = np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2)
S = np.array([[1, 0], [0, 1j]], dtype=complex)
T = np.array([[1, 0], [0, np.exp(1j*np.pi/4)]], dtype=complex)
CNOT = np.array([[1,0,0,0],
[0,1,0,0],
[0,0,0,1],
[0,0,1,0]], dtype=complex)
def Rx(theta):
c, s = np.cos(theta/2), np.sin(theta/2)
return np.array([[c, -1j*s], [-1j*s, c]], dtype=complex)
def Ry(theta):
c, s = np.cos(theta/2), np.sin(theta/2)
return np.array([[c, -s], [s, c]], dtype=complex)
def Rz(theta):
return np.array([[np.exp(-1j*theta/2), 0],
[0, np.exp(1j*theta/2)]], dtype=complex)
Three of these are wrong. Reading harder will not reliably find them; testing will.
Phase 1: The universal test — unitarity
Every gate, without exception, must satisfy $U^\dagger U = I$. This single test catches a large fraction of typos because it constrains every entry simultaneously.
def is_unitary(U, tol=1e-10):
return np.allclose(U.conj().T @ U, np.eye(U.shape[0]), atol=tol)
Why this works: unitarity says the columns form an orthonormal set. A sign error breaks orthogonality between two columns; a magnitude error breaks normalization of one. Both show up.
Running it over the module flags Ry immediately. Compute $R_y^\dagger R_y$ for $\theta = \pi/2$: with $c = s = 1/\sqrt2$ and the matrix $\begin{pmatrix} c & -s \\ s & c\end{pmatrix}$... this one actually passes — it is a real rotation matrix, orthogonal, hence unitary. Nothing wrong with Ry.
So unitarity alone flags nothing here. That is instructive: the planted bugs are unitary but wrong, which is the harder and more realistic case. A gate can be a perfectly valid quantum operation and still not be the gate you asked for.
Phase 2: Structural properties
Layer two: each gate has properties beyond unitarity.
def is_hermitian(U, tol=1e-10):
return np.allclose(U, U.conj().T, atol=tol)
def is_involutory(U, tol=1e-10): # U^2 = I
return np.allclose(U @ U, np.eye(U.shape[0]), atol=tol)
Expected results:
| Gate | Unitary | Hermitian | Involutory |
|---|---|---|---|
| $X, Y, Z, H$ | ✓ | ✓ | ✓ |
| $S, T$ | ✓ | ✗ | ✗ |
| CNOT | ✓ | ✓ | ✓ |
Run it. H fails Hermiticity and involutivity — because as typed it is missing nothing structurally, but let us check: $H = \frac{1}{\sqrt2}\begin{pmatrix}1&1\\1&-1\end{pmatrix}$ is symmetric and real, hence Hermitian, and $H^2 = I$. It passes.
CNOT fails. As typed, the matrix maps $|10\rangle \mapsto |11\rangle$ and $|11\rangle\mapsto|10\rangle$ — correct for control-on-qubit-0 in big-endian ordering. It is unitary, Hermitian, and involutory. It passes too.
Structural tests have not caught the bugs either. We need the sharpest tool.
Phase 3: Eigenvalue spectra
Eigenvalues are the fingerprint of an operator, and they are basis-independent (§3.9). Every standard gate has a known spectrum:
| Gate | Eigenvalues |
|---|---|
| $X, Y, Z, H$ | $\{+1, -1\}$ |
| $S$ | $\{1, i\}$ |
| $T$ | $\{1, e^{i\pi/4}\}$ |
| $R_z(\theta)$ | $\{e^{-i\theta/2}, e^{+i\theta/2}\}$ |
def spectrum(U):
return np.sort_complex(np.linalg.eigvals(U))
Now Y reports eigenvalues $\{-1, +1\}$ — correct. S reports $\{1, i\}$ — correct.
Phase 4: Behavioral identities — where the bugs actually are
The decisive tests are the documented relations between gates. These are the identities from §3.8, and they are what a matrix typo cannot survive.
tests = {
"HXH == Z": (H@X@H, Z),
"HZH == X": (H@Z@H, X),
"S@S == Z": (S@S, Z),
"T@T == S": (T@T, S),
"Rx(pi) == -1j*X": (Rx(np.pi), -1j*X),
"Ry(pi) == -1j*Y": (Ry(np.pi), -1j*Y),
"Rz(pi) == -1j*Z": (Rz(np.pi), -1j*Z),
"XY == 1j*Z": (X@Y, 1j*Z),
"YZ == 1j*X": (Y@Z, 1j*X),
"ZX == 1j*Y": (Z@X, 1j*Y),
}
for name, (lhs, rhs) in tests.items():
print(f"{name:20s} {'PASS' if np.allclose(lhs, rhs) else 'FAIL'}")
This is where the module breaks. The Pauli algebra relations $XY = iZ$, $YZ = iX$, $ZX = iY$ pin down all three Paulis relative to each other, and the rotation identities pin each $R$ to its generator. Any single-entry error in a Pauli propagates into at least one failing product, and any sign error in a rotation generator shows up at $\theta = \pi$.
The three planted bugs — a sign flip in one Pauli off-diagonal, a missing conjugation in one rotation generator, and an endianness-swapped CNOT — are invisible to unitarity, invisible to Hermiticity, invisible to eigenvalues (a sign flip in $Y$'s off-diagonal preserves the spectrum!), and fatal to the algebra. That is the whole lesson.
Phase 5: The CNOT endianness trap
CNOT deserves its own test because its matrix depends on a convention, not just on mathematics. In big-endian ordering (qubit 0 leftmost), control-on-0 gives the matrix above. In little-endian ordering — which Qiskit uses — the same physical gate has the matrix
$$\begin{pmatrix}1&0&0&0\\0&0&0&1\\0&0&1&0\\0&1&0&0\end{pmatrix}$$
Both are unitary, Hermitian, involutory, and share the spectrum $\{1,1,1,-1\}$. No property-based test distinguishes them. Only a behavioral test against explicitly labeled basis states does:
def ket(bits): # ket("10") in the module's own convention
v = np.zeros(2**len(bits), dtype=complex); v[int(bits, 2)] = 1; return v
assert np.allclose(CNOT @ ket("10"), ket("11")) # control=1 flips target
assert np.allclose(CNOT @ ket("01"), ket("01")) # control=0 leaves target
If a library mixes conventions, results come out mirrored and every downstream circuit is subtly wrong while every unit test passes.
Discussion Questions
- A sign error in $Y$'s off-diagonal leaves the eigenvalues unchanged. Why, and what does that say about eigenvalue tests as a validation strategy?
- Which single test in this suite has the highest bug-detection value per line of code? Defend your answer.
- Both CNOT conventions are unitary and involutory. What kind of property could ever distinguish them, and why is that not a mathematical question?
- The rotation gates are parameterized. How should a test suite handle a continuous family — spot values, random sampling, or symbolic identity?
Your Turn: Extensions
- Write the full suite, plant your own bug in
Rz, and confirm which tests catch it. - Add a test that every gate's matrix is the exponential of its generator: $R_x(\theta) = e^{-i\theta X/2}$ using
scipy.linalg.expm. - Extend the endianness check to a three-qubit Toffoli and verify against
qiskit.quantum_info.Operator.
Key Takeaways
- Unitarity is necessary but far from sufficient — a wrong gate is usually still a valid gate.
- Eigenvalue spectra are blind to errors that preserve the spectrum, which includes many sign errors.
- Algebraic identities between gates are the strongest tests, because they constrain gates jointly rather than individually.
- Endianness is a convention, not a theorem, and no property-based test will catch a convention mismatch. Test against labeled basis states.