Skip to content

[Decomposition] Clean up custom logic for adjoint and pow #7352

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 73 commits into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
925f8a2
[Decomposition] Custom decomposition rules for symbolic operators
astralcai Apr 29, 2025
fa09eac
fix doc?
astralcai Apr 29, 2025
95c0383
tests and bug fix
astralcai Apr 29, 2025
8f0a0de
update changelog
astralcai Apr 29, 2025
5b074ed
add more tests
astralcai Apr 29, 2025
ff90882
one more test case
astralcai Apr 29, 2025
9a57289
Merge branch 'master' into symbolic-rules-01
astralcai Apr 29, 2025
956b9a2
pylint
astralcai Apr 29, 2025
a8ccb91
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
astralcai Apr 29, 2025
32dba0b
pylint
astralcai Apr 29, 2025
edff8d4
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
astralcai Apr 29, 2025
7b37f5b
[Decomposition] Clean up custom logic for adjoint and pow
astralcai Apr 29, 2025
9483966
changelog
astralcai Apr 29, 2025
9a141f7
pylint and black
astralcai Apr 29, 2025
8551113
ooops
astralcai Apr 29, 2025
a3705e8
unused import
astralcai Apr 29, 2025
12a52c1
remove test
astralcai Apr 29, 2025
7a0d0d1
add missing coverage
astralcai Apr 29, 2025
7188cee
tests and missing coverage
astralcai Apr 29, 2025
58a3b4d
fix test
astralcai Apr 30, 2025
499cf59
fix siswap and tests
astralcai Apr 30, 2025
82c8780
ooops
astralcai Apr 30, 2025
85f0485
add missing coverage
astralcai Apr 30, 2025
46a18a0
missing coverage
astralcai Apr 30, 2025
92f0b84
changelog
astralcai Apr 30, 2025
2060ca1
revert some changes
astralcai Apr 30, 2025
26a314d
Apply suggestions from code review
astralcai May 1, 2025
02f08f2
Apply suggestions from code review
astralcai May 1, 2025
fa54ba2
lol
astralcai May 1, 2025
b6d6dbe
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
astralcai May 2, 2025
30c0c3c
ooops
astralcai May 2, 2025
c10e20e
Merge branch 'symbolic-rules-01' of https://github.com/PennyLaneAI/pe…
astralcai May 2, 2025
a85a40f
minor fix
astralcai May 2, 2025
c18b60d
Merge branch 'master' into symbolic-rules-01
astralcai May 2, 2025
fb6b917
Merge branch 'symbolic-rules-01' of https://github.com/PennyLaneAI/pe…
astralcai May 2, 2025
5db0fc2
get rid of funny business
astralcai May 5, 2025
28e0f0d
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
astralcai May 5, 2025
aeeb4c5
Merge branch 'symbolic-rules-01' into symbolic-rules-02
astralcai May 5, 2025
8bd5e8f
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
astralcai May 5, 2025
87a7ea6
Merge branch 'symbolic-rules-01' into symbolic-rules-02
astralcai May 5, 2025
74604ab
minor rename
astralcai May 7, 2025
220f671
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
astralcai May 7, 2025
d12873d
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
astralcai May 9, 2025
ed646dd
Merge branch 'symbolic-rules-01' into symbolic-rules-02
astralcai May 9, 2025
3bce1fb
rename pow_self_adjoint to pow_involutory
astralcai May 12, 2025
9f77626
apply suggestions from code review
astralcai May 12, 2025
9da982d
error message for when a symbolic operator name isn't recognized
astralcai May 12, 2025
4dac3ae
Merge branch 'master' into symbolic-rules-01
astralcai May 13, 2025
951bb2d
Merge branch 'symbolic-rules-01' into symbolic-rules-02
astralcai May 13, 2025
2da187e
update docs
astralcai May 14, 2025
39358ab
Update pennylane/decomposition/decomposition_graph.py
astralcai May 15, 2025
dc939f9
[Decomposition] Register conditions to decomposition rules
astralcai May 14, 2025
f9de715
Merge branch 'conditional-decomp' into symbolic-rules-01
astralcai May 15, 2025
384cc32
Merge branch 'symbolic-rules-01' into symbolic-rules-02
astralcai May 15, 2025
db98d92
clean up adjoint and pow conditional decomposition rules
astralcai May 15, 2025
b649ce1
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
astralcai May 15, 2025
316d4c5
pylint
astralcai May 15, 2025
5b959e9
Merge branch 'conditional-decomp' into symbolic-rules-01
astralcai May 15, 2025
3d668ff
Merge branch 'symbolic-rules-01' into symbolic-rules-02
astralcai May 15, 2025
0267157
sphinx
astralcai May 15, 2025
69c35c7
add missing coverage
astralcai May 15, 2025
b6a0dd4
Merge branch 'conditional-decomp' into symbolic-rules-01
astralcai May 15, 2025
9db998e
Merge branch 'symbolic-rules-01' into symbolic-rules-02
astralcai May 15, 2025
ea4af0d
something I missed
astralcai May 15, 2025
d3a9075
fix something
astralcai May 15, 2025
1645b6a
ooops
astralcai May 15, 2025
e209a58
missing coverage
astralcai May 15, 2025
9b06b7c
Apply suggestions from code review
astralcai May 15, 2025
120fc3f
Merge branch 'master' into conditional-decomp
astralcai May 16, 2025
2f2e050
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
astralcai May 19, 2025
e9e5b06
Merge branch 'conditional-decomp' into symbolic-rules-01
astralcai May 19, 2025
d6cfe39
Merge branch 'symbolic-rules-01' into symbolic-rules-02
astralcai May 19, 2025
66b2bfb
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
astralcai May 21, 2025
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
2 changes: 2 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@
:func:`~.transforms.decompose` transform, allowing custom decomposition rules to be defined and
registered for symbolic operators.
[(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347)
[(#7352)](https://github.com/PennyLaneAI/pennylane/pull/7352)

```python
@qml.register_resources({qml.RY: 1})
def my_adjoint_ry(phi, wires, **_):
Expand Down
123 changes: 65 additions & 58 deletions pennylane/decomposition/decomposition_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,19 @@
controlled_global_phase_decomp,
controlled_x_decomp,
)
from .decomposition_rule import DecompositionRule, list_decomps
from .decomposition_rule import DecompositionRule, list_decomps, null_decomp
from .resources import CompressedResourceOp, Resources, resource_rep
from .symbolic_decomposition import (
AdjointDecomp,
adjoint_controlled_decomp,
adjoint_pow_decomp,
adjoint_rotation,
cancel_adjoint,
decompose_to_base,
flip_pow_adjoint,
make_adjoint_decomp,
merge_powers,
pow_involutory,
pow_rotation,
repeat_pow_base,
same_type_adjoint_decomp,
same_type_adjoint_ops,
self_adjoint,
)
from .utils import DecompositionError, translate_op_alias

Expand Down Expand Up @@ -162,10 +164,30 @@ def _get_decompositions(self, op_node: CompressedResourceOp) -> list[Decompositi

decomps = self._alt_decomps.get(op_name, []) + list_decomps(op_name)

if issubclass(op_node.op_type, qml.ops.Adjoint):
if (
issubclass(op_node.op_type, qml.ops.Adjoint)
and self_adjoint not in decomps
and adjoint_rotation not in decomps
):
# In general, we decompose the adjoint of an operator by applying adjoint to the
# decompositions of the operator. However, this is not necessary if the operator
# is self-adjoint or if it has a single rotation angle which can be trivially
# inverted to obtain its adjoint. In this case, `self_adjoint` or `adjoint_rotation`
# would've already been retrieved as a potential decomposition rule for this
# operator, so there is no need to consider the general case.
decomps.extend(self._get_adjoint_decompositions(op_node))

elif issubclass(op_node.op_type, qml.ops.Pow):
elif (
issubclass(op_node.op_type, qml.ops.Pow)
and pow_rotation not in decomps
and pow_involutory not in decomps
):
# Similar to the adjoint case, the `_get_pow_decompositions` contains the general
# approach we take to decompose powers of operators. However, if the operator is
# involutory or if it has a single rotation angle that can be trivially multiplied
# with the power, we would've already retrieved `pow_involutory` or `pow_rotation`
# as a potential decomposition rule for this operator, so there is no need to consider
# the general case.
decomps.extend(self._get_pow_decompositions(op_node))

elif op_node.op_type in (qml.ops.Controlled, qml.ops.ControlledOp):
Expand All @@ -178,10 +200,10 @@ def _construct_graph(self, operations):
for op in operations:
if isinstance(op, Operator):
op = resource_rep(type(op), **op.resource_params)
idx = self._recursively_add_op_node(op)
idx = self._add_op_node(op)
self._original_ops_indices.add(idx)

def _recursively_add_op_node(self, op_node: CompressedResourceOp) -> int:
def _add_op_node(self, op_node: CompressedResourceOp) -> int:
"""Recursively adds an operation node to the graph.

An operator node is uniquely defined by its operator type and resource parameters, which
Expand All @@ -200,55 +222,62 @@ def _recursively_add_op_node(self, op_node: CompressedResourceOp) -> int:
return op_node_idx

for decomposition in self._get_decompositions(op_node):
self._add_decomp_rule_to_op(decomposition, op_node, op_node_idx)
self._add_decomp(decomposition, op_node, op_node_idx)

return op_node_idx

def _add_decomp_rule_to_op(
self, rule: DecompositionRule, op_node: CompressedResourceOp, op_node_idx: int
):
"""Adds a special decomposition rule to the graph."""
def _add_decomp(self, rule: DecompositionRule, op_node: CompressedResourceOp, op_idx: int):
"""Adds a decomposition rule to the graph."""
if not rule.is_applicable(**op_node.params):
return # skip the decomposition rule if it is not applicable
decomp_resource = rule.compute_resources(**op_node.params)
d_node_idx = self._recursively_add_decomposition_node(rule, decomp_resource)
self._graph.add_edge(d_node_idx, op_node_idx, 0)
d_node = _DecompositionNode(rule, decomp_resource)
d_node_idx = self._graph.add_node(d_node)
if not decomp_resource.gate_counts:
# If an operator decomposes to nothing (e.g., a Hadamard raised to a
# power of 2), we must still connect something to this decomposition
# node so that it is accounted for.
self._graph.add_edge(self._start, d_node_idx, 0)
for op in decomp_resource.gate_counts:
op_node_idx = self._add_op_node(op)
self._graph.add_edge(op_node_idx, d_node_idx, (op_node_idx, d_node_idx))
self._graph.add_edge(d_node_idx, op_idx, 0)

def _get_adjoint_decompositions(self, op_node: CompressedResourceOp) -> list[DecompositionRule]:
"""Retrieves a list of decomposition rules for an adjoint operator."""
"""Gets the decomposition rules for the adjoint of an operator."""

base_class, base_params = (op_node.params["base_class"], op_node.params["base_params"])

# Special case: adjoint of an adjoint cancels out
if issubclass(base_class, qml.ops.Adjoint):
return [cancel_adjoint]

if (
issubclass(base_class, qml.ops.Pow)
and base_params["base_class"] in same_type_adjoint_ops()
):
return [adjoint_pow_decomp]

if base_class in same_type_adjoint_ops():
return [same_type_adjoint_decomp]

if (
issubclass(base_class, qml.ops.Controlled)
and base_params["base_class"] in same_type_adjoint_ops()
):
return [adjoint_controlled_decomp]

base_rep = resource_rep(base_class, **base_params)
return [AdjointDecomp(base_rule) for base_rule in self._get_decompositions(base_rep)]
# General case: apply adjoint to each of the base op's decomposition rules.
base = resource_rep(base_class, **base_params)
return [make_adjoint_decomp(base_decomp) for base_decomp in self._get_decompositions(base)]

@staticmethod
def _get_pow_decompositions(op_node: CompressedResourceOp) -> list[DecompositionRule]:
"""Retrieves a list of decomposition rules for a power operator."""
"""Gets the decomposition rules for the power of an operator."""

base_class = op_node.params["base_class"]

# Special case: power of zero
if op_node.params["z"] == 0:
return [null_decomp]

if op_node.params["z"] == 1:
return [decompose_to_base]

# Special case: power of a power
if issubclass(base_class, qml.ops.Pow):
return [merge_powers]

# Special case: power of an adjoint
if issubclass(base_class, qml.ops.Adjoint):
return [flip_pow_adjoint]

# General case: repeat the operator z times
return [repeat_pow_base]

def _get_controlled_decompositions(
Expand Down Expand Up @@ -276,28 +305,6 @@ def _get_controlled_decompositions(
base_rep = resource_rep(base_class, **op_node.params["base_params"])
return [ControlledBaseDecomposition(rule) for rule in self._get_decompositions(base_rep)]

def _recursively_add_decomposition_node(
self, rule: DecompositionRule, decomp_resource: Resources
) -> int:
"""Recursively adds a decomposition node to the graph.

A decomposition node is defined by a decomposition rule and a first-order resource estimate
of this decomposition as computed with resource params passed from the operator node.

"""

d_node = _DecompositionNode(rule, decomp_resource)
d_node_idx = self._graph.add_node(d_node)
if not decomp_resource.gate_counts:
# If an operator decomposes to nothing (e.g., a Hadamard raised to a
# power of 2), we must still connect something to this decomposition
# node so that it is accounted for.
self._graph.add_edge(self._start, d_node_idx, 0)
for op in decomp_resource.gate_counts:
op_node_idx = self._recursively_add_op_node(op)
self._graph.add_edge(op_node_idx, d_node_idx, (op_node_idx, d_node_idx))
return d_node_idx

def solve(self, lazy=True):
"""Solves the graph using the Dijkstra search algorithm.

Expand Down
6 changes: 6 additions & 0 deletions pennylane/decomposition/decomposition_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,9 @@ def has_decomp(op_type: Type[Operator] | str) -> bool:
op_type = op_type.__name__
op_type = translate_op_alias(op_type)
return op_type in _decompositions and len(_decompositions[op_type]) > 0


@register_resources({})
def null_decomp(*_, **__):
"""A decomposition rule that does nothing."""
return
11 changes: 1 addition & 10 deletions pennylane/decomposition/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
from functools import cached_property
from typing import Optional, Type

import numpy as np

import pennylane as qml
from pennylane.operation import Operator

Expand Down Expand Up @@ -326,7 +324,7 @@ def controlled_resource_rep(
num_control_wires += base_params["num_control_wires"]
num_zero_control_values += base_params["num_zero_control_values"]
num_work_wires += base_params["num_work_wires"]
base_params = base_params["base"].resource_params
base_params = {"num_wires": base_params["num_target_wires"]}

return CompressedResourceOp(
qml.ops.Controlled,
Expand Down Expand Up @@ -356,11 +354,6 @@ def adjoint_resource_rep(base_class: Type[Operator], base_params: dict = None):
)


def _is_integer(x):
"""Checks if x is an integer."""
return isinstance(x, int) or np.issubdtype(getattr(x, "dtype", None), np.integer)


def pow_resource_rep(base_class, base_params, z):
"""Creates a ``CompressedResourceOp`` representation of the power of an operator.

Expand All @@ -370,8 +363,6 @@ def pow_resource_rep(base_class, base_params, z):
z (int or float): the power

"""
if (not qml.math.is_abstract(z)) and (not _is_integer(z) or z < 0):
raise NotImplementedError("Non-integer powers or negative powers are not supported yet.")
base_resource_rep = resource_rep(base_class, **base_params)
return CompressedResourceOp(
qml.ops.Pow,
Expand Down
Loading