
“Reversibility is just bookkeeping.”
Last time we built the rollout circuit. Today we’ll run it backward and see why we kept all that extra data.
The inverse is one line of code. Getting the forward computation right takes more work.
A.inverse() reverses the gate order and replaces each gate with its inverse. Qiskit handles that part for us.
It will also happily undo the wrong computation. It has no idea what the Sway rules are, or whether we cleared our scratch at the right time.
A includes preparation, the rollout, and the payoff. This assumes a unitary circuit; measurements and resets stay outside it.
Lesson 3 made three choices: keep the old colors, clear the selector before placing a stone, put randomness in registers. Today each becomes a question about that circuit.
What must the inverse still read? When can scratch return to zero? How do we check the circuit plays the right game? The checks are small Qiskit circuits that run, on tiny boards.
Same moves and dice as Lesson 3. These pictures show the board at the round boundaries. Later placements can still write to the current color register.
Two rounds use three color registers: 27 qubits. Occupancy has its own nine qubits.
Could we save 18 qubits by changing the colors in place? Let’s try it.
Let’s use just two stones: Black at cell 0, White at cell 1. Their dice are 0 and 3. Both have zero friendly neighbors.
A stone flips when its die is less than 4 minus its friendly neighbors. We’re numbering cells and die faces from zero; the die has faces 0–19.
Cell 0 flips to White. What should cell 1 do? And what happens if it reads that neighbor after the flip?
Give everyone about 30 seconds before advancing.
Reading the old board, cell 1 should flip to Black. White sees Black, so it has zero friends and 3 < 4.
Update cell 0 first, and the same dice leave cell 1 White. It now sees a White neighbor. One friend, so 3 < 3 is false.
Every decision needs to read the old colors. We can run the gates one after another and still follow that rule.
Here’s another problem with changing the input. Start with two Black stones and a work bit at zero.
The equality check writes 1 because the colors match. Now flip the first stone and run the check again. The colors differ, so we XOR zero into the work bit.
Changing the input before cleanup leaves the work bit at 1. The second comparison is answering a different question.
Writing the new color somewhere else keeps the original comparison available to undo.
Occupancy is simpler. These are the same four moves from Lesson 3: 4, 1, 3, 6. A color flip doesn’t change whether a cell is occupied.
Keep the move index, and we can undo the placement. XOR the occupancy bit at that index again. The sentinel index does nothing.
Going backward, remove the stone before clearing its move index. Later operations have to be undone first, of course.
Both boards ask for rank zero, the first empty cell. That’s cell 0 on the top board and cell 1 on the bottom.
Both placements give the same visible board. From that board alone, how would we know which input to go back to?
The saved move index keeps the two cases distinct. A unitary needs that distinction somewhere. It doesn’t mean this is the only way to store it.
Records stay around until the inverse has finished using them. That’s our dice, boards, move indices, and ranks.
Scratch goes back to zero before the next block borrows it. We can clear it while the inputs that produced it are still available.
The selector borrows four bits each for its prefix, equality check, and temporary index, plus one match flag.
We copy the temporary index into a move record, then clear the temporary copy. This is our chosen storage layout; other designs could recompute some records.
The selector needs 13 work qubits. One event cell needs eight. The payoff needs nine.
The selector, event, and payoff can share the same 13 qubits. The picture shows one borrower of each kind. Each dip to zero is a cleanup boundary.
The rollout repeats this pattern. Even neighboring event cells take turns borrowing the pool.
The next block expects those qubits to be zero. A circuit can still be reversible with dirty work, but the next borrower may compute the wrong result.
Back to round 2. Seven empty cells, rank two, and we’ve already saved cell 3 as the move.
The scan leaves the counter at seven. Do we place the stone first, or clear the counter first?
What will the counter hold in each case? Take a moment before advancing.
We’re following just the counter here; the full selector clears its other work bits too.
Place first, and the counter ends at one. There are only six empty cells left: seven increments, six decrements.
Clear the counter first, and it returns to zero. Then place using the saved index. All seven empties were still there for the seven decrements. Clearing undoes only the scratch; the stone is the result, and the global inverse undoes it later.
Undo has to see what do saw. The downloadable demo runs both orders on this board with a real four-bit counter.
Try a new rank-two query on the same board, but start the counter at one.
The clean counter picks cell 3. The dirty counter picks cell 2. Every comparison is shifted by one.
Rank zero doesn’t find a match at all. On this board the dirty counter never reaches zero or wraps around.
That’s one leftover bit changing the next move. We’re looking at the decoder here, not estimating a whole game’s win rate.
W counts Black, counts White, and checks whether Black has more stones.
circuit.compose(W, inplace=True)
circuit.cx(win_flag, payoff)
circuit.compose(W.inverse(), inplace=True)Compute the win flag, copy it into the payoff, then undo W. The payoff starts at zero, so a CNOT does the copy.
All nine work bits return to zero. The board and payoff stay. Keep the board unchanged until cleanup finishes.
On a superposition, that CNOT can entangle the payoff with the board. We’re copying a Boolean result, not cloning an arbitrary quantum state.
First, does the circuit agree with the classical rules? Check the output, the inputs we promised to keep, and every scratch bit. Our tiny event has only four color inputs, so we check all four.
Then check amplitudes, including phases. Probabilities alone can hide a phase error. The demo also checks a superposition of those four inputs.
Finally, does forward followed by backward restore the input?
demo.py checks these little circuits, not the full 169-qubit statevector. A few sampled full rollouts wouldn’t prove the whole oracle correct.
Start Black, White. The in-place circuit gives White, White. The game required White, Black. Even the scratch comes out clean on this branch.
Now run its inverse. We get the original input back perfectly. Both the correct and wrong circuits pass that check.
A passing round trip doesn’t tell us we played the right game. We still need the comparison against the rules.
Prepare A once. Then each turn goes: mark the payoff, run A backward, reflect about all zero, run A forward. Read the boxes left to right.
The payoff mark is Z on the payoff qubit. The zero reflection includes every input register, including work and payoff. The usual overall minus sign only changes a global phase here.
A includes the randomness preparation, rollout, and payoff. We need the inverse of all of it. That’s the Q rotation from Lesson 2.
Here we use two uniform data bits. We win only when both are zero, so a = 1/4.
After marking the payoff, should A backward give us all zero again? Pause, then advance for the answer.
Without the mark, yes. With the mark, this example gives all zero only a quarter of the time. Here a = 1/4, so that probability is (1 − 2a)².
The phase mark changed the state we’re trying to undo. The inverse still works.
Finish the turn with the zero reflection and A. The good probability goes from 1/4 to 1, just like Lesson 2.
169 qubits: 155 records, 13 scratch, one payoff. This is the paper’s 3×3, two-round layout. We haven’t shown that it’s the smallest possible allocation.
The records are 90 dice qubits, 36 for state, 16 for move indices, and 13 for ranks. State includes occupancy and all three color registers.
Most of the storage is the history we’re keeping. Clearing scratch helps, but saving much more means finding a way to release or recompute some of those records.
| Board · rounds | Qubits | Gates |
|---|---|---|
| 3×3 · 2 | 169 | 9,768 |
| 5×5 · 5 | 916 | 76,720 |
| 10×10 · 5 | 3,363 | 481,201 |
| 10×10 · 10 | 6,503 | 793,901 |
| 20×20 · 10 | 25,189 | 6,072,641 |
These are counts for one forward call, from the paper’s resource and scaling tables.
They’re before native-gate decomposition and fault-tolerance overhead, so they aren’t hardware timings.
Look at the two 10×10 rows. Twice as many rounds nearly doubles the qubits. The gate count grows too, though setup and payoff work don’t double.
We pay for those gates again when we run backward. Q also needs its reflections.
Back to Lesson 1: What does each oracle call cost?
One last check. We compute a win flag, change the board, then undo the comparison. What goes wrong?
Give everyone about 45 seconds to suggest a fix.
Cleanup needs the original board. Copy the flag to payoff and undo the comparison before changing the board. Or keep the old board until cleanup is done.
Calling inverse() won’t fix the order for us.
Download demo.py and run it with uv. It’ll install Qiskit and check the examples we’ve just used.
These are small circuits we can inspect, including the two bugs and one full turn of Q. The code doesn’t simulate all 169 qubits.
Next we’ll look at garbage collection: which records can we release early, and what would we have to recompute?