Quantum circuit simulator
I wrote a quantum circuit simulator in NumPy without any quantum library, and its final states match Qiskit to within 1e-9 on every standard gate and on 30 random circuits.
What it is
I built a quantum circuit simulator from scratch in Python with NumPy, and it doesn't use Qiskit or any other quantum library. A FastAPI backend runs the simulation. A React front end lets you build a circuit by drag and drop, and it shows each qubit on a 3D Bloch sphere, which pictures the state of one qubit.
The most useful part is how I checked it. A test suite runs every standard gate and 30 random circuits on both my simulator and Qiskit, and the final states must agree to within 1e-9.
The backend also runs Grover's search, the quantum Fourier transform, phase estimation, VQE for the H₂ molecule, QAOA, and three error correcting codes. VQE and QAOA tune a circuit with a classical optimizer. VQE finds the lowest energy of a molecule, and QAOA solves graph problems such as MaxCut.
How it works
The state of n qubits is a vector of 2^n complex amplitudes. When a circuit has noise, the simulator uses a density matrix instead. A density matrix is a 2^n by 2^n matrix, and it can also describe a mix of states with given probabilities.
Gates are applied by tensor contraction. The code reshapes the vector into a tensor with one axis per qubit, and it multiplies the gate into only the axes the gate touches. Each gate then costs O(2^n) instead of O(4^n). The density matrix code reuses the same kernel on its rows and its columns.
When all measurements come at the end, the executor simulates the gates once and draws every shot from the final distribution. A WebSocket lets the front end step through a circuit one gate at a time.
Decisions
- I used the state vector as the main representation, because it is exact and works for any gate. Tensor network and stabilizer methods scale better, but only for restricted kinds of circuits.
- I use a density matrix only when there is noise. Mixed states need it, but it squares the memory cost.
- I fixed one qubit ordering for the whole engine, and the gate kernel enforces it. An earlier version hid a mismatch with a table that reordered qubits by gate name, and I removed that table.
apply_gatereturns a new state instead of changing the old one. Snapshots for step by step execution are then simple, and bugs from shared state go away.
What was hard
- Speed. The first version built a full 2^n by 2^n matrix for every gate, and it filled gates on more than one qubit entry by entry in a Python loop. I rewrote gate application as a tensor contraction in commit
8bdd5aa. - Grover's search broke above 3 qubits. The old multi-controlled Z gate was a simplified chain of Toffoli gates, and I replaced it with a recursive decomposition that needs no extra helper qubits.
- The executor ignored the X and Y measurement bases. It now rotates into the Z basis, measures, and rotates back.
- The map of entanglement between pairs of qubits gave the same value for every pair that shared a first qubit. I changed it to the concurrence of each pair, which is a standard measure of how entangled two qubits are.
Results
- Agreement with Qiskit.
tests/test_qiskit_validation.pyapplies each standard gate on every qubit placement, and it runs 30 random circuits of 2 to 5 qubits with a depth of 20. My states must reach a fidelity above 1 minus 1e-9 with Qiskit'sStatevector, where fidelity measures how close two quantum states are. The suite runs in CI on Python 3.10, 3.11 and 3.12. - Speed. My design notes record that one layer of gates on 10 qubits went from about 19 seconds to a few milliseconds after the rewrite.
- Grover's search. The tests require a success probability above 0.9 for 2 to 5 qubits, within 1e-8 of the value given by the formula.
What I would do next
My design notes list the known limits. The practical size is about 14 qubits, and the hard limit is 20. The server also only accepts requests from local development addresses, so a public deployment needs its own configuration.