Custom Modifiers¶
Note
Introduced in v1.1.1.
When a function is called inside a modifier block, Guppy normally generates its controlled or daggered implementation from the function body. This requires the body to satisfy the restrictions described in control, dagger, and function flags. For example, a runtime loop cannot be daggered automatically, even when we know how to write its inverse.
Custom modifiers let us provide these implementations ourselves using @guppy.unitary.
This is also useful when we have a more efficient implementation than the one generated by the compiler.
Calls still use the usual modifier syntax; Guppy selects the custom implementation where one is available.
Syntax and Basic Behaviour¶
We group the ordinary function and its modified implementations in a Python class decorated with @guppy.unitary.
The __call__ method defines the ordinary operation, while the optional daggered, controlled, and ctrl_daggered methods define its daggered, controlled, and controlled-daggered versions.
Each method is decorated with @guppy.
For example, the following function can be controlled, but its runtime loop prevents automatic daggering.
from guppylang import array, guppy
from guppylang.std.quantum import angle, qubit, rz
@guppy(controllable=True)
def repeated_rz(q: qubit, a: angle, repetitions: int) -> None:
for _ in range(repetitions):
rz(q, a)
repeated_rz.check()
To make this function fully unitary, we can define its daggered and controlled-daggered versions explicitly.
Both implementations undo the repeated rotations by negating the angle.
The forward loop can be controlled automatically, so we mark __call__ as controllable=True:
from guppylang.std.builtins import control, dagger, nat
@guppy.unitary
class repeated_rz:
@guppy(controllable=True)
def __call__(q: qubit, a: angle, repetitions: int) -> None:
for _ in range(repetitions):
rz(q, a)
@guppy
def daggered(q: qubit, a: angle, repetitions: int) -> None:
for _ in range(repetitions):
rz(q, -a)
@guppy
def ctrl_daggered[n: nat](
q: qubit, a: angle, repetitions: int, controls: array[qubit, n]
) -> None:
for _ in range(repetitions):
with control(controls):
rz(q, -a)
@guppy
def use_repeated_rz(c: qubit, q: qubit, a: angle) -> None:
repeated_rz(q, a, 3) # Uses __call__.
with dagger:
repeated_rz(q, a, 3) # Uses daggered.
with control(c):
repeated_rz(q, a, 3) # Automatically synthesizes controlled version of __call__.
with control(c), dagger:
repeated_rz(q, a, 3) # Uses ctrl_daggered.
use_repeated_rz.check()
Call repeated_rz like a function; the surrounding modifiers select the implementation.
Although the definition uses Python class syntax, it groups function implementations rather than defining objects.
There is no self parameter and no instance to construct.
Declaring a Custom Modifier¶
A custom modifier declaration starts with a __call__ method, which defines the operation performed by an ordinary function call.
This method is required, even if the operation will only be used inside modifier blocks.
We can then add daggered, controlled, and ctrl_daggered to supply whichever modified implementations we need, subject to the combinations described in the next section.
Each method is a separate Guppy function decorated with @guppy; none takes self or accesses instance state.
The signatures describe different versions of the same operation, so they must agree:
Method |
Required signature |
|---|---|
|
The ordinary function signature. |
|
The same input types, ownership annotations, type parameters, and return type as |
|
The signature of |
|
The same signature requirements as |
Here is a complete declaration for the \(S\) gate, with all three modified implementations supplied explicitly:
from guppylang.std.quantum import s, sdg
@guppy.unitary
class custom_s:
@guppy
def __call__(q: qubit) -> None:
s(q)
@guppy
def daggered(q: qubit) -> None:
sdg(q)
@guppy
def controlled[n: nat](q: qubit, controls: array[qubit, n]) -> None:
with control(controls):
s(q)
@guppy
def ctrl_daggered[n: nat](q: qubit, controls: array[qubit, n]) -> None:
with control(controls):
sdg(q)
custom_s.check()
Here, daggered takes the same q: qubit argument and returns None, just like __call__.
Both controlled methods add [n: nat] and a final controls: array[qubit, n] argument.
This array is borrowed, so do not annotate it with @owned.
The generic length lets the same implementation handle one or several control qubits; calling custom_s(q) inside control(c) supplies an array of length one.
A custom method cannot add ordinary arguments or change their types.
For example, this daggered method has an extra argument, so its signature is rejected:
@guppy.unitary
class extra_dagger_argument:
@guppy
def __call__(q: qubit) -> None:
pass
@guppy
def daggered(q: qubit, extra: int) -> None:
pass
extra_dagger_argument.check()
Error: Incompatible signature for custom `daggered` implementation (at <In[4]>:8:4)
|
6 |
7 | @guppy
8 | def daggered(q: qubit, extra: int) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 | pass
| ^^^^^^^^^^^^ Expected signature `qubit -> None`, got `(qubit, int) ->
| None`
Note: A custom `daggered` implementation must have the same signature as its
parent function.
Guppy compilation failed due to 1 previous error
A controlled implementation must preserve the original arguments before adding the control array.
Changing q from qubit to int is therefore also rejected:
@guppy.unitary
class changed_control_argument:
@guppy
def __call__(q: qubit) -> None:
pass
@guppy
def controlled[n: nat](q: int, controls: array[qubit, n]) -> None:
pass
changed_control_argument.check()
Error: Incompatible signature for custom `controlled` implementation (at <In[5]>:8:4)
|
6 |
7 | @guppy
8 | def controlled[n: nat](q: int, controls: array[qubit, n]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 | pass
| ^^^^^^^^^^^^ Expected signature `forall n: nat. (qubit, array[qubit, n])
| -> None`, got `forall n: nat. (int, array[qubit, n]) ->
| None`
Note: A custom `controlled` implementation must have its parent function's
signature followed by an `array[qubit, n]` input containing the control qubits.
Guppy compilation failed due to 1 previous error
Only the four recognised methods are allowed inside the class.
Additional methods, class attributes such as label = "rotation", and other class-body statements are rejected.
For example, defining a class attribute is invalid:
@guppy.unitary
class class_attribute:
label = "rotation"
@guppy
def __call__(q: qubit) -> None:
pass
Error: Unexpected statement (at <In[6]>:3:4)
|
1 | @guppy.unitary
2 | class class_attribute:
3 | label = "rotation"
| ^^^^^^^^^^^^^^^^^^ Unexpected statement in in a `@guppy.unitary` class
Help: Only Guppy functions named: '__call__', 'controlled', 'ctrl_daggered' or
'daggered' are allowed as methods in a `@guppy.unitary` class.
Guppy compilation failed due to 1 previous error
An additional method is also invalid, even if it is decorated with @guppy:
@guppy.unitary
class extra_method:
@guppy
def __call__(q: qubit) -> None:
pass
@guppy
def helper(q: qubit) -> None:
pass
Error: Unexpected statement (at <In[7]>:8:4)
|
6 |
7 | @guppy
8 | def helper(q: qubit) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 | pass
| ^^^^^^^^^^^^ Unexpected statement in in a `@guppy.unitary` class
Help: Only Guppy functions named: '__call__', 'controlled', 'ctrl_daggered' or
'daggered' are allowed as methods in a `@guppy.unitary` class.
Guppy compilation failed due to 1 previous error
If an implementation needs a helper, define it outside the class, as with controlled_h_body below:
from guppylang.std.quantum import h
@guppy
def controlled_h_body[n: nat](q: qubit, controls: array[qubit, n]) -> None:
with control(controls):
h(q)
@guppy.unitary
class custom_h:
@guppy
def __call__(q: qubit) -> None:
h(q)
@guppy
def controlled[n: nat](q: qubit, controls: array[qubit, n]) -> None:
controlled_h_body(q, controls)
@guppy
def use_custom_h(c: qubit, q: qubit) -> None:
with control(c):
custom_h(q)
use_custom_h.check()
Using decorators with custom implementations¶
Metadata and function flags belong on __call__.
For example, this declaration sets an expected qubit count and allows automatic generation of all modified versions:
from guppylang.decorator import expected_qubits
from guppylang.std.quantum import x
@guppy.unitary
class hinted_x:
@guppy(unitary=True)
@expected_qubits(1)
def __call__(q: qubit) -> None:
x(q)
hinted_x.check()
Place @expected_qubits below @guppy, as shown above.
Custom methods cannot set their own expected qubit count:
@guppy.unitary
class metadata_on_custom_method:
@guppy
def __call__(q: qubit) -> None:
x(q)
@guppy
@expected_qubits(1)
def daggered(q: qubit) -> None:
x(q)
Error: Unexpected `@expected_qubits` decorator (at <In[10]>:9:4)
|
7 | @guppy
8 | @expected_qubits(1)
9 | def daggered(q: qubit) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unexpected `@expected_qubits` decorator
Help: This method cannot cannot use `@expected_qubits`; Use the decorator on top
of `__call__` to set the expected qubits.
Guppy compilation failed due to 1 previous error
Likewise, setting a function flag on a custom method is rejected:
@guppy.unitary
class flag_on_custom_method:
@guppy
def __call__(q: qubit) -> None:
x(q)
@guppy(daggerable=True)
def daggered(q: qubit) -> None:
x(q)
Error: Unexpected unitary flags (at <In[11]>:8:4)
|
6 |
7 | @guppy(daggerable=True)
8 | def daggered(q: qubit) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unexpected unitary flags in in the method `@guppy` decorator
Help: This method cannot set unitary flags. To set unitary flag to the unitary
class, use the decorator on top of `__call__`.
Guppy compilation failed due to 1 previous error
The @guppy.unitary decorator itself takes no options, so flags cannot go there either:
@guppy.unitary(unitary=True)
class flag_on_class:
@guppy
def __call__(q: qubit) -> None:
x(q)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
[... skipping hidden 1 frame]
Cell In[12], line 1
----> 1 @guppy.unitary(unitary=True)
2 class flag_on_class:
3 @guppy
4 def __call__(q: qubit) -> None:
5 x(q)
TypeError: `@guppy.unitary` does not accept keyword arguments. Put them on the `@guppy` decorator of the `__call__` method instead.
Custom implementations are checked as ordinary Guppy functions, which is why repeated_rz.daggered can contain a loop.
You are responsible for implementing the correct controlled or inverse operation: here, negating the angle undoes the repeated rotations.
Guppy does not prove that the implementations match.
Custom and Default Implementations¶
For each modifier combination, Guppy uses the matching custom method if present.
Otherwise, it generates that version from __call__, provided its declared flags allow it.
Flags still impose their usual restrictions on the body of __call__.
For example, this operation has a custom inverse and uses automatic generation for both controlled versions:
@guppy.unitary
class phase_gate:
@guppy(unitary=True)
def __call__(q: qubit) -> None:
s(q)
@guppy
def daggered(q: qubit) -> None:
sdg(q)
@guppy
def use_phase_gate(c: qubit, q: qubit) -> None:
with dagger:
phase_gate(q) # Uses the custom daggered method.
with control(c):
phase_gate(q) # Controlled version of __call__ automatically.
use_phase_gate.check()
Here, unitary=True permits both automatic transformations, whereas the earlier repeated_rz example supplied ctrl_daggered because its loop could not be daggered automatically.
Removing the flag from phase_gate.__call__ would leave a valid declaration that supports only daggering, through its custom method.
Capabilities are therefore partly inferred: controlled supplies controllability, and daggered supplies daggerability without requiring those flags on __call__.
The decorator’s name alone does not enable either capability.
Guppy checks that these capabilities form a complete set:
If both control and dagger are supported, through methods or flags, provide
ctrl_daggeredor declare__call__asunitary=True. Guppy does not derive the combined version by controllingdaggeredor daggeringcontrolled.If
ctrl_daggeredis provided, separate control and dagger support must also exist. With no flags, supply bothcontrolledanddaggered; with onlycontrollable=True, supplydaggered; with onlydaggerable=True, supplycontrolled. Withunitary=True, either can be generated automatically.
For example, controllable=True together with a custom daggered method is insufficient without a controlled-daggered implementation:
@guppy.unitary
class missing_ctrl_daggered:
@guppy(controllable=True)
def __call__(q: qubit) -> None:
s(q)
@guppy
def daggered(q: qubit) -> None:
sdg(q)
Error: Invalid `@guppy.unitary` implementation (at <In[14]>:2:0)
|
1 | @guppy.unitary
2 | class missing_ctrl_daggered:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A `@guppy.unitary` class implementing `daggered` with `__call__` declared as
`controllable=True` requires either a `ctrl_daggered` implementation or
`unitary=True` on `__call__`
Guppy compilation failed due to 1 previous error
Add ctrl_daggered, or change the flag on __call__ to unitary=True, to make this declaration valid.
Conversely, supplying only ctrl_daggered does not provide the separate controlled and daggered versions:
@guppy.unitary
class missing_controlled_and_daggered:
@guppy
def __call__(q: qubit) -> None:
s(q)
@guppy
def ctrl_daggered[n: nat](q: qubit, controls: array[qubit, n]) -> None:
with control(controls):
sdg(q)
Error: Invalid `@guppy.unitary` implementation (at <In[15]>:2:0)
|
1 | @guppy.unitary
2 | class missing_controlled_and_daggered:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A `@guppy.unitary` class implementing `ctrl_daggered` requires either
`controlled` and `daggered` implementations or `unitary=True` on `__call__`
Guppy compilation failed due to 1 previous error
Supply both missing methods, or enable their automatic generation with unitary=True on __call__.