T state distillation and the Option
type¶
Download this notebook - t_factory.ipynb
In this example we will demonstrate how to create a T state using magic state distillation, including the use of the Option
type to indicate success or failure in a repeat-until-success algorithm.
import numpy as np
from guppylang.decorator import guppy
from guppylang.std.angles import angle, pi
from guppylang.std.builtins import array, owned, comptime
from guppylang.std.option import Option, nothing, some
from guppylang.std.debug import state_result
from guppylang.std.quantum import (
cz,
discard,
h,
measure,
qubit,
ry,
rz,
)
from selene_sim import build, Quest
from hugr.qsystem.result import QsysResult
np.set_printoptions(precision=4, suppress=True, linewidth=120)
Preparation¶
We begin by implementing a function to prepare an approximate T state, taken from https://arxiv.org/abs/2310.12106.
phi = np.arccos(1 / 3)
@guppy
def prepare_approx() -> qubit:
q = qubit()
ry(q, angle(comptime(phi)))
rz(q, pi / 4)
return q
Distillation¶
This is the inverse of the \([[5,3,1]]\) encoder in figure 3 of https://arxiv.org/abs/2208.01863.
@guppy
def distill(
target: qubit @ owned,
qs: array[qubit, 4] @ owned,
) -> tuple[qubit, bool]:
"""First argument is the target qubit which will be returned from the circuit.
Other arguments are ancillae, which should also be in an approximate T state.
Returns target qubit and a bool, which is true if the distillation succeeded.
"""
cz(qs[0], qs[1])
cz(qs[2], qs[3])
cz(target, qs[0])
cz(qs[1], qs[2])
cz(target, qs[3])
# Measuring gives false for success, true for failure.
# We check for all falses to say whether distillation succeeded.
for i in range(4):
h(qs[i])
bits = array(not measure(q) for q in qs)
# Guppy doesn't yet support the `any` or `all` operators yet.
success = True
for b in bits:
success &= b
return target, success
Repeat-Until-Success and Option
¶
We can now put both of the above together by preparing 5 qubits using prepare_approx
and then attempting to distill a T state from them using distill
, for some maximum number of attempts.
@guppy
def t_state(attempts: int) -> Option[qubit]:
"""Create a T state using magic state distillation with a fixed number of attempts.
On success returns a qubit in a magic T state.
On failure (i.e. number of attempts are exceeded) returns nothing.
"""
if attempts > 0:
tgt = prepare_approx()
qs = array(prepare_approx() for _ in range(4))
q, success = distill(tgt, qs)
if success:
return some(q)
else:
# Discard the qubit and start over.
# Note, this could just as easily be a while loop!
discard(q)
return t_state(attempts - 1)
# We ran out of attempts.
return nothing()
Note the use of the Option[qubit]
type for the result. Option types enable us to represent either a value, using some(value)
or the absence of it, using nothing()
. In this case it is a good way to represent failure or success without having to rely on errors. Before using the value if it exists, it needs to be unwrapped.
attempts = 3
@guppy
def main() -> None:
option_t = t_state(comptime(attempts))
# Check whether the option contains a value.
if option_t.is_some():
# Unwrap the qubit.
t = option_t.unwrap()
state_result("t_state", t)
discard(t)
else:
# Since qubits are linear, Option[qubit] is also linear, so we need to consume
# it either way.
option_t.unwrap_nothing()
shots = main.emulator(n_qubits=8).with_seed(1).run()
# See the `Debugging with `state_result` statements` example notebook for more details
# about state results.
for states in shots.partial_state_dicts():
if "t_state" in states:
dist = states["t_state"].state_distribution()
print(np.allclose(dist[0].state, np.array([0.2967+0.j, 0.8094-0.5068j]), rtol=1e-4))
True
A similar concept that is also available in the Guppy standard library is the Either
type, which generalises the concept of having an optional value to having a value that could be of one of two types, either left(value)
or right(value)
.
Selectively measuring arrays with Option
¶
Consider this attempt at measuring a subscript of an array of qubits:
@guppy
def attempt_measure(qs: array[qubit, 10] @ owned) -> None:
measure(qs[5])
compiled = attempt_measure.compile()
Error: Subscript consumed (at <In[7]>:3:12)
|
1 | @guppy
2 | def attempt_measure(qs: array[qubit, 10] @ owned) -> None:
3 | measure(qs[5])
| ^^^^^ Cannot consume a subscript of `qs` with non-copyable type
| `array[qubit, 10]`
Note: Subscripts on non-copyable types are only allowed to be borrowed, not
consumed
Guppy compilation failed due to 1 previous error
As expected, this leads to an error because you can’t consume subscripts of a linear array.
However we can use arrays of type array[Option[qubit], N]
to measure some qubits in an array without consuming the whole array at once by swapping qubits you want to measure withnothing()
.
n = guppy.nat_var("n")
@guppy
def measure_mask(
qs: array[Option[qubit], n], mask: array[bool, n] @ owned
) -> array[int, n]:
"""Measure all gubits in `qs` with a corresponding `True` index in `mask`."""
res = array(-1 for _ in range(n))
idx = 0
for m in mask:
if m and qs[idx].is_some():
# `take` swaps an optional value with nothing().
q = qs[idx].take()
res[idx] = int(measure(q.unwrap()))
idx += 1
return res
@guppy
def main() -> None:
qs = array(some(qubit()) for _ in range(3))
mask = array(True, False, True)
measure_mask(qs, mask)
# We need to consume the array of options at some point, as it can't be leaked.
for q_opt in qs:
if q_opt.is_some():
q = q_opt.unwrap()
discard(q)
else:
q_opt.unwrap_nothing()
main.compile();