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();