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

Open
wants to merge 21 commits into
base: symbolic-rules-01
Choose a base branch
from
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
1 change: 1 addition & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
: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
117 changes: 59 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_of_self_adjoint,
pow_rotation,
repeat_pow_base,
same_type_adjoint_decomp,
same_type_adjoint_ops,
self_adjoint,
)
from .utils import DecompositionError, DecompositionNotApplicable, translate_op_alias

Expand Down Expand Up @@ -162,10 +164,24 @@ 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
):
# Only when the base op is not self-adjoint or a rotation with a single rotation
# angle that could be trivially negated do we need to apply adjoint to each of
# the base op's decomposition rules.
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_of_self_adjoint not in decomps
):
# Only when the operator is not self-adjoint or a rotation with a single rotation
# angle that could be trivially multiplied with the power do we need to apply the
# power to each of the base op's decomposition rules.
decomps.extend(self._get_pow_decompositions(op_node))

elif op_node.op_type in (qml.ops.Controlled, qml.ops.ControlledOp):
Expand All @@ -178,10 +194,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,56 +216,63 @@ 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."""
try:
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is moved from another method.

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)
except DecompositionNotApplicable:
pass # ignore decompositions that are not applicable to the given op params.

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]
Comment on lines -226 to -230
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is removed. There is no reason why we need to switch the order of adjoint and pow such that the adjoint is applied first.


if base_class in same_type_adjoint_ops():
return [same_type_adjoint_decomp]
Comment on lines -232 to -233
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch is now generalized away into operator specific symbolic decomposition rules


if (
issubclass(base_class, qml.ops.Controlled)
and base_params["base_class"] in same_type_adjoint_ops()
):
return [adjoint_controlled_decomp]
Comment on lines -235 to -239
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also removed. There is no reason why we need to switch the order of ctrl and adjoint such that adjoint is applied first.


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]
Comment on lines +271 to +273
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually apply the pow first, then adjoint, because typically adjoint does not turn the operator into a different type, but pow might. For example, pow(SX, 2) is a PauliX. If we apply the adjoint on the SX first, SX gets decomposed, while we could've just had PauliX.


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

def _get_controlled_decompositions(
Expand Down Expand Up @@ -277,28 +300,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 @@ -381,3 +381,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