kkoci / guppy-estimand
Publicmarimo run;
in marimo edit, click the ▶ run button at the bottom right once). Then
edit any cell, or change the scheme and code distance controls
under "Compare all programs", and every result updates.estimate() needs only a scheme and a code distance.@guppy
def bell_and_t() -> None:
q0 = qubit()
q1 = qubit()
h(q0)
cx(q0, q1)
t(q0)
measure(q0)
measure(q1)
estimate(bell_and_t.compile(), scheme=..., data_d=...)
beverland, d = 17 · 2 logical qubits · gates: t: 1, clifford: 2, measurement: 2qft is a @guppy.comptime function, so its loops unroll at compile time. Allocating the register with array(qubit() for _ in range(n)) and freeing it with discard_array both compile to loops, so this needs upper_bound=True and a trip count (n) for each loop. The comparison table below has n = 2 to 6.@guppy
def swap(q0: qubit, q1: qubit) -> None:
cx(q0, q1)
cx(q1, q0)
cx(q0, q1)
@guppy
def cphase(control: qubit, target: qubit, theta: angle) -> None:
rz(control, theta / 2.0)
crz(control, target, theta)
@guppy.comptime
def qft[n: nat](qs: array[qubit, n]) -> None:
for i in range(n):
h(qs[i])
for j in range(i + 1, n):
cphase(qs[j], qs[i], pi / 2.0 ** (j - i))
for k in range(n // 2):
swap(qs[k], qs[n - k - 1])
@guppy
def qft_n4() -> None:
qs = array(qubit() for _ in range(4))
qft(qs)
discard_array(qs)
estimate(qft_n4.compile(), scheme=..., data_d=...,
upper_bound=True, loop_trip_counts={node_id: 4, ...})
beverland, d = 17 · 4 logical qubits · gates: clifford: 10, rotation: 12with control(...) makes a doubly controlled X, counted as one Toffoli. The oracle's if tests on marked stay as runtime branches, so the estimate takes the more expensive side of each and is an upper bound. The program has 8 loops, each with its own trip count.def optimal_iterations(n_items: int = 8, n_marked: int = 1) -> int:
theta = math.asin(math.sqrt(n_marked / n_items))
return round(math.pi / (4 * theta) - 0.5)
@guppy
def oracle[marked: nat](qs: array[qubit, 3]) -> None:
if (marked >> 2) & 1 == 0:
x(qs[0])
if (marked >> 1) & 1 == 0:
x(qs[1])
if marked & 1 == 0:
x(qs[2])
h(qs[2])
with control(qs[0], qs[1]):
x(qs[2])
h(qs[2])
if (marked >> 2) & 1 == 0:
x(qs[0])
if (marked >> 1) & 1 == 0:
x(qs[1])
if marked & 1 == 0:
x(qs[2])
@guppy
def diffuser(qs: array[qubit, 3]) -> None:
for i in range(3):
h(qs[i])
for i in range(3):
x(qs[i])
h(qs[2])
with control(qs[0], qs[1]):
x(qs[2])
h(qs[2])
for i in range(3):
x(qs[i])
for i in range(3):
h(qs[i])
@guppy
def grover_search[marked: nat, iterations: nat](qs: array[qubit, 3]) -> None:
for i in range(3):
h(qs[i])
for _ in range(iterations):
oracle[marked](qs)
diffuser(qs)
@guppy
def grover_main() -> None:
qs = array(qubit() for _ in range(3))
grover_search[MARKED, ITERATIONS](qs)
discard_array(qs)
estimate(grover_main.compile(), scheme=..., data_d=...,
upper_bound=True, loop_trip_counts={node_id: count, ...})
beverland, d = 17 · 3 logical qubits · gates: toffoli: 4, clifford: 47marimo edit. qubit, h, x, t, cx, rz, measure and
discard are already imported.beverland, d = 17 · 2 logical qubits · gates: t: 2, clifford: 2, measurement: 2upper_bound=True: each branch
counts its more expensive side and each loop runs its supplied trip
count. They are worst-case bounds, not point estimates. For QFT the
bound is tight: QFT has no runtime branches, and its loops only
allocate and free qubits. Grover (marked item 5) is a real
bound because of the oracle's branches.
rotation gates are passed to Qualtran as they are, not synthesized
into T gates first.qubit()s alive at the same time. A physical qubit is a hardware
qubit. Under the surface code, one logical qubit is stored in a patch of
about physical qubits. The computation also needs routing space
between patches, and magic-state factories that prepare the T and
Toffoli states the code can't apply directly. So physical qubits
outnumber logical ones by a factor of hundreds to thousands.
estimate(..., target_error=1e-3), and guppy-estimand
picks the smallest odd that meets it. A budget below the factory's
own error can't be met at any , and estimate() raises ValueError.LoopTripCountMissing, naming
the first loop without a count. The notebook's discover_trip_counts
helper keeps adding a count for each loop it's told about until the walk
succeeds. The counts it passed, as {node_id: count}:
{7: 4, 215: 4}{7: 3, 640: 3, 752: 3, 895: 3, 1006: 3, 117: 3, 229: 2, 337: 3}beverland presetbeverland preset's formulas were checked against the papers they
cite, and two don't match. Both are filed upstream and still open. The
full derivation is in VERIFICATION.md.
ceil(1.5n) tiles, but the cited
paper (Litinski, arXiv:1808.02892) says 1.5n + 3. guppy-estimand
uses the paper's formula, so the physical qubit counts above are
already corrected. For Bell + T at d = 17,
guppy-estimand reports 4,614 physical qubits and
Qualtran's unpatched model reports 2,880.
(VERIFICATION.md, section 8)a = 0.03)
to Litinski's magic-state factory, whose own paper uses a = 0.1.
VERIFICATION.md finds the factory error about 4.9x lower than
Litinski's constant would give. This is a judgement about combining
two papers rather than a clear bug, so it's flagged here, not changed.
(VERIFICATION.md, section 7)gidney_fowler.