Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
384 changes: 384 additions & 0 deletions sphinx/language_guide/modifiers/custom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
---
file_format: mystnb
kernelspec:
name: python3
---

# Custom Modifiers

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](control.md), [dagger](dagger.md), and [function flags](functions.md).
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.

```{code-cell} ipython3
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`:

```{code-cell} ipython3
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 |
| --- | --- |
| `__call__` | The ordinary function signature. |
| `daggered` | The same input types, ownership annotations, type parameters, and return type as `__call__`. |
| `controlled` | The signature of `__call__`, with one additional final type parameter `n: nat` and final argument `controls: array[qubit, n]`. |
| `ctrl_daggered` | The same signature requirements as `controlled`. |

Here is a complete declaration for the $S$ gate, with all three modified implementations supplied explicitly:

```{code-cell} ipython3
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:

```{code-cell} ipython3
---
tags: [raises-exception]
---
@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()
```

A controlled implementation must preserve the original arguments before adding the control array.
Changing `q` from `qubit` to `int` is therefore also rejected:

```{code-cell} ipython3
---
tags: [raises-exception]
---
@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()
```


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:

```{code-cell} ipython3
---
tags: [raises-exception]
---
@guppy.unitary
class class_attribute:
label = "rotation"

@guppy
def __call__(q: qubit) -> None:
pass
```

An additional method is also invalid, even if it is decorated with `@guppy`:

```{code-cell} ipython3
---
tags: [raises-exception]
---
@guppy.unitary
class extra_method:
@guppy
def __call__(q: qubit) -> None:
pass

@guppy
def helper(q: qubit) -> None:
pass
```

If an implementation needs a helper, define it outside the class, as with `controlled_h_body` below:

```{code-cell} ipython3
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 metadata with custom implementations

Metadata and [function flags](functions.md) belong on `__call__`.
For example, this declaration sets an expected qubit count and allows automatic generation of all modified versions:

```{code-cell} ipython3
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:

```{code-cell} ipython3
---
tags: [raises-exception]
---
@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)
```

Likewise, setting a function flag on a custom method is rejected:

```{code-cell} ipython3
---
tags: [raises-exception]
---
@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)
```

The `@guppy.unitary` decorator itself takes no options, so flags cannot go there either:

```{code-cell} ipython3
---
tags: [raises-exception]
---
@guppy.unitary(unitary=True)
class flag_on_class:
@guppy
def __call__(q: qubit) -> None:
x(q)
```

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:

```{code-cell} ipython3
@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_daggered` or declare `__call__` as `unitary=True`.
Guppy does not derive the combined version by controlling `daggered` or daggering `controlled`.
- If `ctrl_daggered` is provided, separate control and dagger support must also exist.
With no flags, supply both `controlled` and `daggered`; with only `controllable=True`, supply `daggered`; with only `daggerable=True`, supply `controlled`.
With `unitary=True`, either can be generated automatically.

For example, `controllable=True` together with a custom `daggered` method is insufficient without a controlled-daggered implementation:

```{code-cell} ipython3
---
tags: [raises-exception]
---
@guppy.unitary
class missing_ctrl_daggered:
@guppy(controllable=True)
def __call__(q: qubit) -> None:
s(q)

@guppy
def daggered(q: qubit) -> None:
sdg(q)
```

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:

```{code-cell} ipython3
---
tags: [raises-exception]
---
@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)
```

Supply both missing methods, or enable their automatic generation with `unitary=True` on `__call__`.
3 changes: 2 additions & 1 deletion sphinx/language_guide/modifiers/modifiers_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,5 @@ local_assignment.check()
control.md
dagger.md
functions.md
```
custom.md
```
Loading