Modifiers¶
Modifiers transform a block of quantum operations. They can be used to generate controlled and daggered versions of single gate, a block containing many gates or a Guppy function. Guppy generates the transformed operations, this means that the user does not need to provide the implementation of the controlled or daggered operation themselves.
The modifier changes the underlying gates. For example, controlling a block adds the control to every gate it produces; effectively, a function f(q) becomes a controlled operation ctrl-f(c, q) with an additional control-qubit input. A dagger block reverses the gate order and replaces each gate with its inverse.
Syntax¶
We can make use of modifiers inside a context manager using the with keyword.
Let’s start with a simple example of a controlled single-qubit operation. We will realize this with the control modifier.
from guppylang import guppy, array
from guppylang.std.builtins import control, output
from guppylang.std.quantum import qubit, h, measure
@guppy
def controlled_h() -> None:
c = qubit()
q = qubit()
h(c)
with control(c):
h(q)
bit_c = measure(c)
bit_q = measure(q)
output("result", array(bit_c.read(), bit_q.read()))
controlled_h.emulator(n_qubits=2).with_shots(100).run().collated_counts()
Counter({(('result', '00'),): 44,
(('result', '11'),): 28,
(('result', '10'),): 28})
Here we can observe that the h operation is applied only when the control qubit c is in the \(\ket{1}\) state.
from guppylang.std.builtins import dagger, output
from guppylang.std.quantum import angle, h, measure, qubit, rx
@guppy
def rotate_then_dagger() -> None:
q = qubit()
a = angle(1 / 4)
rx(q, a)
with dagger:
rx(q, a)
bit = measure(q)
output("result", bit.read())
rotate_then_dagger.emulator(n_qubits=2).with_shots(100).run().collated_counts()
Counter({(('result', '0'),): 100})
Since we are applying a rotation followed by its inverse, the qubit is always measured in the \(\ket{0}\) state.
Multiple modifiers may also be combined or nested.
from guppylang.std.quantum import s, qubit
from guppylang.std.builtins import control, dagger
@guppy
def controlled_inverse(c: qubit, q: qubit) -> None:
with control(c), dagger:
s(q)
controlled_inverse.check()
Here we take the \(S\) gate and modify it with control and dagger. The controlled and daggered version of the gate is synthetised by the compiler at compilation time, in fact since the gate is a unitary operation we can always produce its controlled-daggered version. When applied, this function acts as a \(CS^\dagger\) gate.
Modifiers and variable scope¶
The body has access to variables from its enclosing scope, but it cannot take ownership of them. For instance, the following program is rejected:
from guppylang.std.builtins import owned
from guppylang.std.quantum import discard
@guppy.declare(unitary=True)
def consume(q: qubit @ owned) -> None: ...
@guppy
def cannot_take_ownership(q: qubit) -> None:
with dagger:
consume(q)
cannot_take_ownership.check()
Error: Not owned (at <In[4]>:10:16)
|
8 | def cannot_take_ownership(q: qubit) -> None:
9 | with dagger:
10 | consume(q)
| ^ Function `consume` wants to take ownership of this argument,
| but we cannot transfer ownership inside a modifier body
Notes:
|
7 | @guppy
8 | def cannot_take_ownership(q: qubit) -> None:
| -------- Argument `q` defined here ...
9 | with dagger:
| ------ ... outside the modifier block
Guppy compilation failed due to 1 previous error
This restriction prevents a modifier from discarding a qubit: daggering or controlling a discard operation in fact can lead to unexpected results.
Moreover, assignments in a modifier block are local to that block, including assignments that reuse an outer name.
In the following example, denominator is not available outside the with control: block.
from guppylang.std.quantum import angle, rx
@guppy
def local_assignment(q: qubit, c: qubit) -> None:
outer_var = 1
with control(c):
denominator = 4
outer_var = 2
rx(q, angle(outer_var / denominator))
# `denominator` is not available here.
rx(q, angle(1 / denominator))
local_assignment.check()
Error: Variable assigned in modifier block (at <In[5]>:11:20)
|
9 | rx(q, angle(outer_var / denominator))
10 | # `denominator` is not available here.
11 | rx(q, angle(1 / denominator))
| ^^^^^^^^^^^ `denominator` cannot be used...
Note:
|
6 | with control(c):
7 | denominator = 4
| --------------- ... since it was assigned in a modifier block
Help: Variables assigned inside a modifier block are not available outside the
block
Guppy compilation failed due to 1 previous error
The reason for this restriction is that the assignment inside a controlled block is not controlled, so the denominator variable is always defined. Thus having such a variable available outside the block would lead to unexpected results, since it would be defined even if the control qubit is in the \(\ket{0}\) state.
For a similar reason, in the next example, outer_var is not available outside the with control: block, even though it was assigned in the outer scope. The assignment inside the block in fact overwrites the scope of the outer variable.
@guppy
def local_assignment(q: qubit, c: qubit) -> None:
outer_var = 1
with control(c):
denominator = 4
outer_var = 2
rx(q, angle(outer_var / denominator))
# `outer_var` is not available here either
rx(q, angle(outer_var / 4))
local_assignment.check()
Error: Variable assigned in modifier block (at <In[6]>:9:16)
|
7 | rx(q, angle(outer_var / denominator))
8 | # `outer_var` is not available here either
9 | rx(q, angle(outer_var / 4))
| ^^^^^^^^^ `outer_var` cannot be used...
Note:
|
5 | denominator = 4
6 | outer_var = 2
| ------------- ... since it was assigned in a modifier block
Help: Variables assigned inside a modifier block are not available outside the
block
Guppy compilation failed due to 1 previous error