Postselection: exit and panic¶
Download this notebook - postselect.ipynb
In this example we will look at two ways of ending a Guppy program early:
exit
will end the current shot and carry on with subsequent ones. We will use this to implement postselection.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