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)
---------------------------------------------------------------------------
EmulatorError 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:228, in EmulatorInstance.run(self)
224 shot_results.append(tag, value)
225 except Exception as e: # noqa: BLE001
226 # In this case, casting a wide net on exceptions is
227 # suitable.
--> 228 raise EmulatorError(
229 completed_shots=EmulatorResult(all_results),
230 failing_shot=shot_results,
231 underlying_exception=e,
232 ) from None
233 all_results.append(shot_results)
234 return EmulatorResult(all_results)
EmulatorError: Panic (#1001): Invalid data index in physical_h