Building a Small Simulator From Scratch
The fastest way to find out whether you actually understand quantum circuits is to implement one. A vector, a handful of matrices, and a sampler — a few dozen lines of Python — will expose every gap that reading hides.
A simulator small enough to read in one sitting teaches more than any SDK, because every abstraction the SDK hides — basis ordering, normalization, the difference between a state and a sample — becomes a line of code you wrote and a test you can fail.
This chapter specifies that simulator: what it must represent, what invariants it must enforce, which tests catch the classic mistakes, and how to use it as a reference when larger tools produce confusing output.
Core concepts: statevector simulation, unitary gates, basis measurement.
Why build one at all
SDKs are good at hiding exactly the things a learner needs to see. Basis ordering, gate conventions, the gap between a statevector and a histogram — all of it disappears behind a run() call. A minimal statevector simulator puts those decisions back in your hands: states are complex vectors, gates are linear maps, measurement probabilities are squared magnitudes, and sampling is a separate step that approximates the state without being the state.
The simulator does not need to be fast. It needs to be inspectable. If you cannot explain every line, you are not ready to debug higher-level tooling — because when an SDK gives you strange output, the question will be whether the bug is in the library, the backend, or your model of the circuit, and you will need ground truth to answer it.
Three invariants do the work
The entire simulator rests on three checks, each of which becomes a few lines of code.
A valid state is normalized:
A closed-system gate is unitary:
And measurement follows the Born rule:
Everything else is engineering: matrix-vector updates, a basis-index convention you state once and enforce everywhere, and a sampler that draws from the probability distribution the Born rule defines.
Worked example: the Bell-state trace
The canonical end-to-end test is the Bell pair from the figure:
- build the two-qubit zero state,
- apply H to qubit 0,
- apply CNOT with control 0 and target 1,
- compute the measurement probabilities,
- sample counts with a fixed seed.
The exact probabilities must come out as:
- :
- :
- : 0
- : 0
The zeros matter as much as the halves — interference is what makes this quantum rather than a coin flip. With finite shots the counts only approximate these numbers. With a fixed seed the sample is reproducible for the same code; without one, repeated runs differ while still matching the same distribution. Both statements belong in your notes, because confusing them later causes real debugging pain.
Where simulators go wrong
The first trap is checking only final counts. Counts can hide a wrong state ordering behind a lucky sample, and a histogram that "looks Bell-ish" proves little. Check exact statevector probabilities first; treat sampling as a separate layer with its own test.
The second trap is a silent basis-order convention. If your index order is unclear — which qubit is the most significant bit of the index — every multi-qubit gate can be subtly wrong while single-qubit tests still pass. Name the convention in a comment, test it with CNOT on every basis state, and never borrow an SDK's convention without writing down that you did.
The engineering view
Treat the simulator as a reference implementation: small enough to read, slow enough to be obvious, tested enough to catch wrong mental models. The test suite should cover:
- normalization rejects an invalid state,
- H applied twice returns the input,
- X maps to ,
- CNOT maps all four basis states correctly,
- Bell probabilities are nonzero only on and ,
- seeded sampling returns a stable count total.
Keep the public API boring: zero_state, apply_gate, apply_cnot, measurement_probabilities, sample_counts. A clever class hierarchy would defeat the purpose, which is a transparent bridge from math to code. Once that bridge is solid, SDK abstractions become easy to evaluate — you know exactly what they must preserve, and you have ground truth to check them against.
What this buys you in diligence
When a team presents quantum software results, listen for the distinction between statevector probabilities, sampled counts, and noisy hardware outcomes. A team that conflates them — quoting hardware counts as if they were exact amplitudes, or simulator amplitudes as if they were measured data — has weak evidence no matter how polished the demo.
A small simulator trace is not a product. It is a competence check, and a cheap one: anyone who builds quantum software should be able to walk you through theirs.
Exercise
Trace the statevector by hand and by code. Run the mini-simulator test suite and one Bell-state walkthrough.
- Record: the basis order, the gate sequence, the exact probabilities, the shot count, the seed, and the sampled counts.
- Write: a code-walk note connecting every major simulator function to one of the three invariants — normalization, unitarity, or the Born rule.
- Check: mark which quantities in your note must be exact and which may vary statistically.
- Decide: write one sentence stating whether an SDK result agrees with your first-principles trace — and what you would suspect first if it did not.
Check your understanding
Implement one gate application and one measurement function, then compare your hand prediction against the test output. A passing result explains any mismatch in terms of basis order, normalization, or sampling — not "the library does something weird."
Oral defense: explain why two runs of the same correct simulator can produce different counts, and what you would fix to make them agree.
If you get stuck
If the matrix updates or the Born-rule probabilities feel mechanical rather than obvious, work through Chapter 11 (Matrices as Gates) and Chapter 14 (Observables, Eigenvectors, and Measurement) before continuing. The simulator is those two chapters, rendered as code — it will be much easier to build once the math underneath it is familiar.