Deferring measurements¶
Download this notebook - deferred_measurement.ipynb
In this example we will illustrate why using std.qsystem.lazy_measure can be useful.
from guppylang import guppy
from guppylang.std.builtins import array, owned
from guppylang.std.quantum import qubit, measure
from guppylang.std.qsystem import lazy_measure
from guppylang.std.platform import result
from typing import no_type_check
Let’s say we want to measure an array of qubits one by one and output the results. You might do something like eager_read below:
N = guppy.nat_var("N")
@guppy
@no_type_check
def eager_read(qs: array[qubit, N] @ owned) -> None:
for q in qs:
result("t", measure(q)) # forces immediate measurement of q
# [... more quantum operations ...]
eager_read.check()
It is important to understand that while std.quantum.measure returns a bool on the surface, on Quantinuum systems the physical measurement doesn’t always happen immediately. A measurement call requests a measurement, which can then be deferred until it is actually required. At that point, any further execution is blocked, so by delaying it we give the runtime more opportunities to parallelise operations between request and block.
In the example above, using result does mean forcing an immediate measurement of q, which isn’t the best for performance. However, this isn’t obvious from a user perspective.
Now consider the same function using lazy_measure:
@guppy
@no_type_check
def lazy_read(qs: array[qubit, N] @ owned) -> None:
for q in qs:
result("t", lazy_measure(q))
# [... more quantum operations ...]
lazy_read.check()
Error: Invalid call of overloaded function (at <In[3]>:5:15)
|
3 | def lazy_read(qs: array[qubit, N] @ owned) -> None:
4 | for q in qs:
5 | result("t", lazy_measure(q))
| ^^^^^^^^^^^^^^^^^^^^ No variant of overloaded function `result` takes arguments
| `str`, `Measurement`
Note: Available overloads are:
def result(tag: str @comptime, value: int) -> None
def result(tag: str @comptime, value: nat) -> None
def result(tag: str @comptime, value: bool) -> None
def result(tag: str @comptime, value: float) -> None
def result(tag: str @comptime, value: array[int, n]) -> None
def result(tag: str @comptime, value: array[nat, n]) -> None
def result(tag: str @comptime, value: array[bool, n]) -> None
def result(tag: str @comptime, value: array[float, n]) -> None
Guppy compilation failed due to 1 previous error
Simply replacing the method results in an error because lazy_measure returns a value of type Measurement. In order to obtain a bool, we have to explicitaly use read() on the value.
@guppy
@no_type_check
def lazy_read(qs: array[qubit, N] @ owned) -> None:
for q in qs:
result("t", lazy_measure(q).read())
# [... more quantum operations ...]
lazy_read.check()
The program now type-checks and ends up compiling to the exact same operation order as eager_read. We can now see where exactly the read() happens, rather than it being done implicitly, so we can try to move it further down the program.
@guppy
@no_type_check
def lazy_read_improved(qs: array[qubit, N] @ owned) -> None:
ms = array(lazy_measure(q) for q in qs)
# [... more quantum operations ...]
for m in ms:
# This `read` call only blocks execution when we are at the end of the program
result("t", m.read())
lazy_read_improved.check()
Now that each measurement is only read at the end of the program, physical measurements can be deferred to a better point in execution.
Of course in this case we could have also collected the bool returns of measure or used measure_array to achieve the same outcome, however there might be cases where other solutions are less obvious. It can therefore be useful to use lazy_measure in programs where you want more control over when measurements should happen.
For convenience, __bool__ is implemented on the Measurement type as syntactic sugar for read(), so it is called automatically in conditionals for example:
@guppy
@no_type_check
def lazy_conditional(q: qubit @ owned) -> None:
if lazy_measure(q):
result("t", 1)
else:
result("t", 0)
lazy_conditional.check();