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
from guppylang.std.option import Option, nothing, some
from guppylang.std.debug import state_output
from guppylang.std.quantum import (
cz,
discard,
h,
measure,
qubit,
ry,
rz,
Measurement,
)
from guppylang.std.platform import output
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(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(attempts)
# Check whether the option contains a value.
if option_t.is_some():
# Unwrap the qubit.
t = option_t.unwrap()
state_output("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]).read()
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]).read()
| ^^^^^ 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 with nothing().
n = guppy.nat_var("n")
@guppy
def measure_mask(
qs: array[Option[qubit], n], mask: array[bool, n] @ owned
) -> array[Option[Measurement], n]:
"""Measure all qubits in `qs` with a corresponding `True` index in `mask`."""
# As measurements are linear, we also use an optional array for them.
res = array(nothing[Measurement]() 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] = some(measure(q.unwrap()))
idx += 1
return res
@guppy
def main() -> None:
qs = array(some(qubit()) for _ in range(3))
mask = array(True, False, True)
# We need to consume the array of measurement options as they are linear.
for opt_msmt in measure_mask(qs, mask):
if opt_msmt.is_some():
m = opt_msmt.unwrap()
m.read()
else:
opt_msmt.unwrap_nothing()
# 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();