Postselection: exit and panic

Download this notebook - postselect.ipynb

In this example we will look at two ways of ending a Guppy program early:

  1. exit will end the current shot and carry on with subsequent ones. We will use this to implement postselection.

  2. panic is used to signal some unexpected error and as such it will end the shot and not run any subsequent ones.

from typing import Counter

from guppylang import guppy
from guppylang.std.builtins import result, array, exit, panic
from guppylang.std.quantum import measure_array, measure, qubit, h, cx
from guppylang.defs import GuppyFunctionDefinition

from selene_sim import DepolarizingErrorModel
from selene_sim.exceptions import SelenePanicError

Postselection

We can use postselection to implement fault tolerant state preparation for the [[7, 1, 3]] Steane code.

Let’s first define our “Steane qubit” as a struct containing our 7 data qubits, then write a function to prepare an encoded \(|0\rangle\) non-fault tolerantly. We use the preparation circuit from Realization of real-time fault-tolerant quantum error correction.

@guppy.struct
class SteaneQubit:
    data_qs: array[qubit, 7]


@guppy
def non_ft_zero() -> SteaneQubit:
    data_qubits = array(qubit() for _ in range(7))
    plus_ids = array(0, 4, 6)
    for i in plus_ids:
        h(data_qubits[i])

    cx_pairs = array((0, 1), (4, 5), (6, 3), (6, 5), (4, 2), (0, 3), (4, 1), (3, 2))
    for c, t in cx_pairs:
        cx(data_qubits[c], data_qubits[t])
    return SteaneQubit(data_qubits)

We can now implement fault-tolerant preparation using postselection. We can use an ancilla to check the prepared state, and if we detect an error use exit to end the shot with a message about why we exited.

@guppy
def ft_zero() -> SteaneQubit:
    q = non_ft_zero()
    ancilla = qubit()
    flags = array(1, 3, 5)
    for f in flags:
        cx(q.data_qs[f], ancilla)
    if measure(ancilla):
        exit("Postselected: FT prep failed", 1)
    return q

Let’s define a couple of utility functions - a Guppy function to check the parity of a bit array, and a python function to run our program and report the results.

We use a simple depolarizing error model to induce errors in the preparation.

n = guppy.nat_var("n")


@guppy
def parity_check(data_bits: array[bool, n]) -> bool:
    out = False
    for i in range(n):
        out ^= data_bits[i]
    return out


error_model = DepolarizingErrorModel(
    random_seed=1234,
    # single qubit gate error rate
    p_1q=1e-3,
    # two qubit gate error rate
    p_2q=1e-3,
    # set state preparation and measurement error rates to 0
    p_meas=0,
    p_init=0,
)


def run(main_def: GuppyFunctionDefinition) -> Counter:
    res = (
        main_def.emulator(n_qubits=8)
        .stabilizer_sim()
        .with_seed(42)
        .with_shots(1000)
        .with_error_model(error_model)
        .run()
    )

    return res.collated_counts()

We can now define our main program and run it. We know that all basis states of the encoded Steane \(|0\rangle\) state have a \(0\) parity, so we can use that to verify our preparation.

@guppy
def main() -> None:
    steane_q = ft_zero()

    # Measure the data qubits
    data = measure_array(steane_q.data_qs)
    result("parity", parity_check(data))


run(main)
Counter({(('parity', '0'),): 968,
         (('exit: Postselected: FT prep failed', '1'),): 23,
         (('parity', '1'),): 9})

As we can see, the state preparation succeeded in 968 cases out of 1000. In 23 of the unsuccessful cases, an error was detected on the ancillas and was discarded through postselection. In the remaining 9 cases, the state failed in a way that was not detected by postselection but was instead detected by a parity check performed after measuring all of the qubits.

Note the result tag in the discarded shot is prefixed with exit: followed by the specified message, and the value of the result entry is the error code (1 in this case).

Panic

The panic function is similar to exit but is used for exceptional circumstances - when something unexpected has gone wrong. For example we could define a physical hadamard function that takes an index to act on the data qubit array. If the index is out of bounds, we can panic with a helpful message. This will raise an error during the simulation, and no subsequent shots will be run.

@guppy
def physical_h(q: SteaneQubit, data_idx: int) -> None:
    if data_idx >= 7:
        panic("Invalid data index in physical_h")
    h(q.data_qs[data_idx])


@guppy
def main() -> None:
    steane_q = ft_zero()

    # add a physical H gate
    physical_h(steane_q, 8)

    data = measure_array(steane_q.data_qs)
    result("parity", parity_check(data))

run(main)
---------------------------------------------------------------------------
SelenePanicError                          Traceback (most recent call last)
Cell In[6], line 18
     15     data = measure_array(steane_q.data_qs)
     16     result("parity", parity_check(data))
---> 18 run(main)

Cell In[4], line 31, in run(main_def)
     24 def run(main_def: GuppyFunctionDefinition) -> Counter:
     25     res = (
     26         main_def.emulator(n_qubits=8)
     27         .stabilizer_sim()
     28         .with_seed(42)
     29         .with_shots(1000)
     30         .with_error_model(error_model)
---> 31         .run()
     32     )
     34     return res.collated_counts()

File /app/docs/.venv/lib/python3.11/site-packages/guppylang/emulator/instance.py:211, in EmulatorInstance.run(self)
    207 result_stream = self._run_instance()
    209 # TODO progress bar on consuming iterator?
--> 211 return EmulatorResult(result_stream)

File /app/docs/.venv/lib/python3.11/site-packages/guppylang/emulator/result.py:70, in EmulatorResult.__init__(self, results)
     67 def __init__(
     68     self, results: Iterable[QsysShot | Iterable[TaggedResult]] | None = None
     69 ):
---> 70     super().__init__(results=results)

File /app/docs/.venv/lib/python3.11/site-packages/hugr/qsystem/result.py:156, in QsysResult.__init__(self, results)
    153 def __init__(
    154     self, results: Iterable[QsysShot | Iterable[TaggedResult]] | None = None
    155 ):
--> 156     self.results = [
    157         res if isinstance(res, QsysShot) else QsysShot(res) for res in results or []
    158     ]

File /app/docs/.venv/lib/python3.11/site-packages/hugr/qsystem/result.py:157, in <listcomp>(.0)
    153 def __init__(
    154     self, results: Iterable[QsysShot | Iterable[TaggedResult]] | None = None
    155 ):
    156     self.results = [
--> 157         res if isinstance(res, QsysShot) else QsysShot(res) for res in results or []
    158     ]

File /app/docs/.venv/lib/python3.11/site-packages/hugr/qsystem/result.py:61, in QsysShot.__init__(self, entries)
     60 def __init__(self, entries: Iterable[TaggedResult] | None = None):
---> 61     self.entries = list(entries or [])

File /app/docs/.venv/lib/python3.11/site-packages/selene_sim/result_handling/parse_shot.py:154, in parse_shot(parser, event_hook, parse_results, stdout_file, stderr_file)
    152 # pass panic errors to the caller
    153 except SelenePanicError as panic:
--> 154     raise panic
    155 except SeleneRuntimeError as error:
    156     error.stdout = stdout_file.read_text()

File /app/docs/.venv/lib/python3.11/site-packages/selene_sim/result_handling/parse_shot.py:139, in parse_shot(parser, event_hook, parse_results, stdout_file, stderr_file)
    137 if parse_results:
    138     if parsed.code >= 1000:
--> 139         raise SelenePanicError(
    140             message=parsed.message,
    141             code=parsed.code,
    142             stdout=stdout_file.read_text(),
    143             stderr=stderr_file.read_text(),
    144         )
    145     else:
    146         yield (f"exit: {parsed.message}", parsed.code)

SelenePanicError: Panic (#1001): Invalid data index in physical_h