From 925f8a27f69c5bcae1d2163400eeb17431a4978a Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 10:42:38 -0400 Subject: [PATCH 01/45] [Decomposition] Custom decomposition rules for symbolic operators --- doc/releases/changelog-dev.md | 75 ++++++++--- .../decomposition/decomposition_graph.py | 125 +++++++++--------- pennylane/decomposition/decomposition_rule.py | 42 ++++-- .../decomposition/symbolic_decomposition.py | 6 +- pennylane/decomposition/utils.py | 2 +- tests/decomposition/conftest.py | 22 +-- tests/decomposition/test_decomp_utils.py | 1 + .../decomposition/test_decomposition_graph.py | 14 +- .../decomposition/test_decomposition_rule.py | 2 +- .../test_symbolic_decomposition.py | 28 ++-- 10 files changed, 183 insertions(+), 134 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 3e5e44d6d36..59e7d50e3ee 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -92,32 +92,65 @@ qml.RZ(omega, wires=wires[0]) qml.GlobalPhase(-phase) + # This decomposition will be ignored for `QubitUnitary` on more than one wire. qml.add_decomps(QubitUnitary, zyz_decomposition) ``` - - This decomposition will be ignored for `QubitUnitary` on more than one wire. - -* The :func:`~.transforms.decompose` transform now supports symbolic operators (e.g., `Adjoint` and `Controlled`) specified as strings in the `gate_set` argument - when the new graph-based decomposition system is enabled. - [(#7331)](https://github.com/PennyLaneAI/pennylane/pull/7331) - ```python - from functools import partial - import pennylane as qml +* Symbolic operator types (e.g., `Adjoint`, `Controlled`, and `Pow`) can now be specified as strings + in various parts of the new graph-based decomposition system, specifically: + * The `gate_set` argument of the :func:`~.transforms.decompose` transform now supports adding symbolic + operators to the target gate set. + [(#7331)](https://github.com/PennyLaneAI/pennylane/pull/7331) + ```python + from functools import partial + import pennylane as qml - qml.decomposition.enable_graph() + qml.decomposition.enable_graph() - @partial(qml.transforms.decompose, gate_set={"T", "Adjoint(T)", "H", "CNOT"}) - @qml.qnode(qml.device("default.qubit")) - def circuit(): - qml.Toffoli(wires=[0, 1, 2]) - ``` - ```pycon - >>> print(qml.draw(circuit)()) - 0: ───────────╭●───────────╭●────╭●──T──╭●─┤ - 1: ────╭●─────│─────╭●─────│───T─╰X──T†─╰X─┤ - 2: ──H─╰X──T†─╰X──T─╰X──T†─╰X──T──H────────┤ - ``` + @partial(qml.transforms.decompose, gate_set={"T", "Adjoint(T)", "H", "CNOT"}) + @qml.qnode(qml.device("default.qubit")) + def circuit(): + qml.Toffoli(wires=[0, 1, 2]) + ``` + ```pycon + >>> print(qml.draw(circuit)()) + 0: ───────────╭●───────────╭●────╭●──T──╭●─┤ + 1: ────╭●─────│─────╭●─────│───T─╰X──T†─╰X─┤ + 2: ──H─╰X──T†─╰X──T─╰X──T†─╰X──T──H────────┤ + ``` + * Symbolic operator types can now be given as strings to the `op_type` argument of :func:`~.add_decomps`, + or as keys of the dictionaries passed to the `alt_decomps` and `fixed_decomps` arguments of the + :func:`~.transforms.decompose` transform, allowing custom decomposition rules to be defined and + registered for symbolic operators. + ```python + @register_resources({qml.RY: 1}) + def my_adjoint_ry(phi, wires, **_): + qml.RY(-phi, wires=wires) + + @qml.register_resources({qml.RX: 1}) + def my_adjoint_rx(phi, wires, **__): + qml.RX(-phi, wires) + + # Registers a decomposition rule for the adjoint of RY globally + qml.add_decomps("Adjoint(RY)", my_adjoint_ry) + + @partial( + qml.transforms.decompose, + gate_set={"RX", "CNOT"}, + fixed_decomps={"Adjoint(RX)": my_adjoint_rx} + ) + @qml.qnode(qml.device("default.qubit")) + def circuit(): + qml.adjoint(qml.RX(0.5), wires=[0]) + qml.CNOT(wires=[0, 1]) + qml.adjoint(qml.RY(0.5), wires=[1]) + return qml.expval(qml.Z(0)) + ``` + ```pycon + >>> print(qml.draw(circuit)()) + 0: ──RX(-0.50)─╭●────────────┤ + 1: ────────────╰X──RY(-0.50)─┤ + ```

Improvements 🛠

diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index cdbbba327f7..2a28b40d804 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -44,11 +44,11 @@ from .resources import CompressedResourceOp, Resources, resource_rep from .symbolic_decomposition import ( AdjointDecomp, - adjoint_adjoint_decomp, adjoint_controlled_decomp, adjoint_pow_decomp, - pow_decomp, - pow_pow_decomp, + cancel_adjoint, + merge_powers, + repeat_pow_base, same_type_adjoint_decomp, same_type_adjoint_ops, ) @@ -140,8 +140,10 @@ def __init__( self._all_op_indices: dict[CompressedResourceOp, int] = {} # Stores the library of custom decomposition rules - self._fixed_decomps = fixed_decomps or {} - self._alt_decomps = alt_decomps or {} + fixed_decomps = fixed_decomps or {} + alt_decomps = alt_decomps or {} + self._fixed_decomps = {_to_name(k): v for k, v in fixed_decomps.items()} + self._alt_decomps = {_to_name(k): v for k, v in alt_decomps.items()} # Initializes the graph. self._graph = rx.PyDiGraph() @@ -150,11 +152,26 @@ def __init__( # Construct the decomposition graph self._construct_graph(operations) - def _get_decompositions(self, op_type) -> list[DecompositionRule]: + def _get_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: """Helper function to get a list of decomposition rules.""" - if op_type in self._fixed_decomps: - return [self._fixed_decomps[op_type]] - return self._alt_decomps.get(op_type, []) + list_decomps(op_type) + + op_name = _to_name(op) + + if op_name in self._fixed_decomps: + return [self._fixed_decomps[op_name]] + + decomps = self._alt_decomps.get(op_name, []) + list_decomps(op_name) + + if issubclass(op.op_type, qml.ops.Adjoint): + decomps.extend(self._get_adjoint_decompositions(op)) + + elif issubclass(op.op_type, qml.ops.Pow): + decomps.extend(self._get_pow_decompositions(op)) + + elif op.op_type in (qml.ops.Controlled, qml.ops.ControlledOp): + decomps.extend(self._get_controlled_decompositions(op)) + + return decomps def _construct_graph(self, operations): """Constructs the decomposition graph.""" @@ -182,17 +199,7 @@ def _recursively_add_op_node(self, op_node: CompressedResourceOp) -> int: self._target_ops_indices.add(op_node_idx) return op_node_idx - if op_node.op_type in (qml.ops.Controlled, qml.ops.ControlledOp): - # This branch only applies to general controlled operators - return self._add_controlled_decomp_node(op_node, op_node_idx) - - if issubclass(op_node.op_type, qml.ops.Adjoint): - return self._add_adjoint_decomp_node(op_node, op_node_idx) - - if issubclass(op_node.op_type, qml.ops.Pow): - return self._add_pow_decomp_node(op_node, op_node_idx) - - for decomposition in self._get_decompositions(op_node.op_type): + for decomposition in self._get_decompositions(op_node): self._add_decomp_rule_to_op(decomposition, op_node, op_node_idx) return op_node_idx @@ -208,88 +215,65 @@ def _add_decomp_rule_to_op( except DecompositionNotApplicable: pass # ignore decompositions that are not applicable to the given op params. - def _add_adjoint_decomp_node(self, op_node: CompressedResourceOp, op_node_idx: int) -> int: - """Adds an adjoint decomposition node.""" + def _get_adjoint_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: + """Retrieves a list of decomposition rules for an adjoint operator.""" - base_class, base_params = op_node.params["base_class"], op_node.params["base_params"] + base_class, base_params = op.params["base_class"], op.params["base_params"] if issubclass(base_class, qml.ops.Adjoint): - rule = adjoint_adjoint_decomp - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) - return op_node_idx + return [cancel_adjoint] if ( issubclass(base_class, qml.ops.Pow) and base_params["base_class"] in same_type_adjoint_ops() ): - rule = adjoint_pow_decomp - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) - return op_node_idx + return [adjoint_pow_decomp] if base_class in same_type_adjoint_ops(): - rule = same_type_adjoint_decomp - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) - return op_node_idx + return [same_type_adjoint_decomp] if ( issubclass(base_class, qml.ops.Controlled) and base_params["base_class"] in same_type_adjoint_ops() ): - rule = adjoint_controlled_decomp - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) - return op_node_idx - - for base_decomposition in self._get_decompositions(base_class): - rule = AdjointDecomp(base_decomposition) - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) + return [adjoint_controlled_decomp] - return op_node_idx + base_rep = resource_rep(base_class, **base_params) + return [AdjointDecomp(base_rule) for base_rule in self._get_decompositions(base_rep)] - def _add_pow_decomp_node(self, op_node: CompressedResourceOp, op_node_idx: int) -> int: - """Adds a power decomposition node to the graph.""" + @staticmethod + def _get_pow_decompositions(op: CompressedResourceOp) -> list[DecompositionRule]: + """Retrieves a list of decomposition rules for a power operator.""" - base_class = op_node.params["base_class"] + base_class = op.params["base_class"] if issubclass(base_class, qml.ops.Pow): - rule = pow_pow_decomp - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) - return op_node_idx + return [merge_powers] - rule = pow_decomp - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) - return op_node_idx + return [repeat_pow_base] - def _add_controlled_decomp_node(self, op_node: CompressedResourceOp, op_node_idx: int) -> int: + def _get_controlled_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: """Adds a controlled decomposition node to the graph.""" - base_class = op_node.params["base_class"] - num_control_wires = op_node.params["num_control_wires"] + base_class = op.params["base_class"] + num_control_wires = op.params["num_control_wires"] # Handle controlled global phase if base_class is qml.GlobalPhase: - rule = controlled_global_phase_decomp - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) - return op_node_idx + return [controlled_global_phase_decomp] # Handle controlled-X gates if base_class is qml.X: - rule = controlled_x_decomp - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) - return op_node_idx + return [controlled_x_decomp] # Handle custom controlled ops if (base_class, num_control_wires) in base_to_custom_ctrl_op(): custom_op_type = base_to_custom_ctrl_op()[(base_class, num_control_wires)] - rule = CustomControlledDecomposition(custom_op_type) - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) - return op_node_idx + return [CustomControlledDecomposition(custom_op_type)] # General case - for base_decomposition in self._get_decompositions(base_class): - rule = ControlledBaseDecomposition(base_decomposition) - self._add_decomp_rule_to_op(rule, op_node, op_node_idx) - - return op_node_idx + base_rep = resource_rep(base_class, **op.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 @@ -498,3 +482,12 @@ class _DecompositionNode: def count(self, op: CompressedResourceOp): """Find the number of occurrences of an operator in the decomposition.""" return self.decomp_resource.gate_counts.get(op, 0) + + +def _to_name(op): + if isinstance(op, type): + return op.__name__ + if isinstance(op, CompressedResourceOp): + return op.name + assert isinstance(op, str) + return translate_op_alias(op) diff --git a/pennylane/decomposition/decomposition_rule.py b/pennylane/decomposition/decomposition_rule.py index 9d1f79b24a3..9625c4973de 100644 --- a/pennylane/decomposition/decomposition_rule.py +++ b/pennylane/decomposition/decomposition_rule.py @@ -24,6 +24,7 @@ from pennylane.operation import Operator from .resources import CompressedResourceOp, Resources, resource_rep +from .utils import translate_op_alias @overload @@ -191,7 +192,7 @@ def __init__(self, func: Callable, resources: Callable | dict): self._source = inspect.getsource(func) if isinstance(resources, dict): - def resource_fn(): + def resource_fn(*_, **__): return resources self._compute_resources = resource_fn @@ -233,10 +234,10 @@ def _auto_wrap(op_type): _decompositions = defaultdict(list) -"""dict[type, list[DecompositionRule]]: A dictionary mapping operator types to decomposition rules.""" +"""dict[str, list[DecompositionRule]]: A dictionary mapping operator names to decomposition rules.""" -def add_decomps(op_type: Type[Operator], *decomps: DecompositionRule) -> None: +def add_decomps(op_type: Type[Operator] | str, *decomps: DecompositionRule) -> None: """Globally registers new decomposition rules with an operator class. .. note:: @@ -253,7 +254,8 @@ def add_decomps(op_type: Type[Operator], *decomps: DecompositionRule) -> None: decomposition rules that may be chosen if they lead to a more resource-efficient decomposition. Args: - op_type: the operator type for which new decomposition rules are specified. + op_type (type or str): the operator type for which new decomposition rules are specified. + For symbolic operators, use strings such as ``"Adjoint(RY)"``, ``"Pow(H)"``, ``"C(RX)"``, etc. decomps (DecompositionRule): new decomposition rules to add to the given ``op_type``. A decomposition is a quantum function registered with a resource estimate using ``qml.register_resources``. @@ -289,6 +291,17 @@ def my_hadamard2(wires): for the duration of the session. To add alternative decompositions for a particular circuit as opposed to globally, use the ``alt_decomps`` argument of the :func:`~pennylane.transforms.decompose` transform. + Custom decomposition rules can also be specified for symbolic operators. In this case, the + operator type can be specified as a string. For example, + + .. code-block:: python + + @register_resources({qml.RY: 1}) + def adjoint_ry(phi, wires, **_): + qml.RY(-phi, wires=wires) + + qml.add_decomps("Adjoint(RY)", adjoint_ry) + .. seealso:: :func:`~pennylane.transforms.decompose` """ @@ -297,10 +310,12 @@ def my_hadamard2(wires): "A decomposition rule must be a qfunc with a resource estimate " "registered using qml.register_resources" ) - _decompositions[op_type].extend(decomps) + if isinstance(op_type, type): + op_type = op_type.__name__ + _decompositions[translate_op_alias(op_type)].extend(decomps) -def list_decomps(op_type: Type[Operator]) -> list[DecompositionRule]: +def list_decomps(op_type: Type[Operator] | str) -> list[DecompositionRule]: """Lists all stored decomposition rules for an operator class. .. note:: @@ -311,7 +326,8 @@ def list_decomps(op_type: Type[Operator]) -> list[DecompositionRule]: decomposition rules for an operator. Args: - op_type: the operator class to retrieve decomposition rules for. + op_type (type or str): the operator class to retrieve decomposition rules for. For symbolic + operators, use strings such as ``"Adjoint(RY)"``, ``"Pow(H)"``, ``"C(RX)"``, etc. Returns: list[DecompositionRule]: a list of decomposition rules registered for the given operator. @@ -338,10 +354,12 @@ def _crx_to_rx_cz(phi, wires, **__): 1: ──RX(0.25)─╰Z──RX(-0.25)─╰Z─┤ """ - return _decompositions[op_type][:] + if isinstance(op_type, type): + op_type = op_type.__name__ + return _decompositions[translate_op_alias(op_type)][:] -def has_decomp(op_type: Type[Operator]) -> bool: +def has_decomp(op_type: Type[Operator] | str) -> bool: """Checks whether an operator has decomposition rules defined. .. note:: @@ -352,10 +370,14 @@ def has_decomp(op_type: Type[Operator]) -> bool: decomposition rules for an operator. Args: - op_type: the operator class to check for decomposition rules. + op_type (type or str): the operator class to check for decomposition rules. For symbolic + operators, use strings such as ``"Adjoint(RY)"``, ``"Pow(H)"``, ``"C(RX)"``, etc. Returns: bool: whether decomposition rules are defined for the given operator. """ + if isinstance(op_type, type): + op_type = op_type.__name__ + op_type = translate_op_alias(op_type) return op_type in _decompositions and len(_decompositions[op_type]) > 0 diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index e9d6ada4997..dc4acf40084 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -77,7 +77,7 @@ def _adjoint_adjoint_resource(*_, base_params, **__): @register_resources(_adjoint_adjoint_resource) -def adjoint_adjoint_decomp(*params, wires, base): # pylint: disable=unused-argument +def cancel_adjoint(*params, wires, base): # pylint: disable=unused-argument """Decompose the adjoint of the adjoint of a gate.""" _, [_, metadata] = base.base._flatten() # pylint: disable=protected-access new_struct = wires, metadata @@ -157,7 +157,7 @@ def _pow_resource(base_class, base_params, z): @register_resources(_pow_resource) -def pow_decomp(*_, base, z, **__): +def repeat_pow_base(*_, base, z, **__): """Decompose the power of a gate.""" assert isinstance(z, int) and z >= 0 for _ in range(z): @@ -175,7 +175,7 @@ def _pow_pow_resource(base_class, base_params, z): # pylint: disable=unused-arg @register_resources(_pow_pow_resource) -def pow_pow_decomp(*_, base, z, **__): +def merge_powers(*_, base, z, **__): """Decompose the power of the power of a gate.""" qml.pow(base.base, z=z * base.z) diff --git a/pennylane/decomposition/utils.py b/pennylane/decomposition/utils.py index 3203ee217d2..b911aacd90f 100644 --- a/pennylane/decomposition/utils.py +++ b/pennylane/decomposition/utils.py @@ -41,7 +41,7 @@ def translate_op_alias(op_alias): """Translates an operator alias to its proper name.""" if op_alias in OP_NAME_ALIASES: return OP_NAME_ALIASES[op_alias] - if match := re.match(r"C\((\w+)\)", op_alias): + if match := re.match(r"(?:C|Controlled)\((\w+)\)", op_alias): base_op_name = match.group(1) return f"C({translate_op_alias(base_op_name)})" if match := re.match(r"Adjoint\((\w+)\)", op_alias): diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py index b148d292144..540b0a14b71 100644 --- a/tests/decomposition/conftest.py +++ b/tests/decomposition/conftest.py @@ -35,7 +35,7 @@ def _cz_to_cnot(*_, **__): raise NotImplementedError -decompositions[qml.CZ] = [_cz_to_cnot] +decompositions["CZ"] = [_cz_to_cnot] @qml.register_resources({qml.Hadamard: 2, qml.CZ: 1}) @@ -43,7 +43,7 @@ def _cnot_to_cz(*_, **__): raise NotImplementedError -decompositions[qml.CNOT] = [_cnot_to_cz] +decompositions["CNOT"] = [_cnot_to_cz] def _multi_rz_decomposition_resources(num_wires): @@ -55,7 +55,7 @@ def _multi_rz_decomposition(*_, **__): raise NotImplementedError -decompositions[qml.MultiRZ] = [_multi_rz_decomposition] +decompositions["MultiRZ"] = [_multi_rz_decomposition] @qml.register_resources({qml.RZ: 2, qml.RX: 1, qml.GlobalPhase: 1}) @@ -68,7 +68,7 @@ def _hadamard_to_rz_ry(*_, **__): raise NotImplementedError -decompositions[qml.Hadamard] = [_hadamard_to_rz_rx, _hadamard_to_rz_ry] +decompositions["Hadamard"] = [_hadamard_to_rz_rx, _hadamard_to_rz_ry] @qml.register_resources({qml.RX: 1, qml.RZ: 2}) @@ -76,7 +76,7 @@ def _ry_to_rx_rz(*_, **__): raise NotImplementedError -decompositions[qml.RY] = [_ry_to_rx_rz] +decompositions["RY"] = [_ry_to_rx_rz] @qml.register_resources({qml.RX: 2, qml.CZ: 2}) @@ -84,7 +84,7 @@ def _crx_to_rx_cz(*_, **__): raise NotImplementedError -decompositions[qml.CRX] = [_crx_to_rx_cz] +decompositions["CRX"] = [_crx_to_rx_cz] @qml.register_resources({qml.RZ: 3, qml.CNOT: 2, qml.GlobalPhase: 1}) @@ -92,7 +92,7 @@ def _cphase_to_rz_cnot(*_, **__): raise NotImplementedError -decompositions[qml.ControlledPhaseShift] = [_cphase_to_rz_cnot] +decompositions["ControlledPhaseShift"] = [_cphase_to_rz_cnot] @qml.register_resources({qml.RZ: 1, qml.GlobalPhase: 1}) @@ -100,7 +100,7 @@ def _phase_shift_to_rz_gp(*_, **__): raise NotImplementedError -decompositions[qml.PhaseShift] = [_phase_shift_to_rz_gp] +decompositions["PhaseShift"] = [_phase_shift_to_rz_gp] @qml.register_resources({qml.RX: 1, qml.GlobalPhase: 1}) @@ -108,7 +108,7 @@ def _x_to_rx(*_, **__): raise NotImplementedError -decompositions[qml.X] = [_x_to_rx] +decompositions["PauliX"] = [_x_to_rx] @qml.register_resources({qml.PhaseShift: 1}) @@ -116,7 +116,7 @@ def _u1_ps(phi, wires, **__): qml.PhaseShift(phi, wires=wires) -decompositions[qml.U1] = [_u1_ps] +decompositions["U1"] = [_u1_ps] @qml.register_resources({qml.PhaseShift: 1}) @@ -124,4 +124,4 @@ def _t_ps(wires, **__): raise NotImplementedError -decompositions[qml.T] = [_t_ps] +decompositions["T"] = [_t_ps] diff --git a/tests/decomposition/test_decomp_utils.py b/tests/decomposition/test_decomp_utils.py index 22ef50a4b2d..48b02d7a908 100644 --- a/tests/decomposition/test_decomp_utils.py +++ b/tests/decomposition/test_decomp_utils.py @@ -63,5 +63,6 @@ def test_translate_op_alias(base_op_alias, expected_op_name): assert translate_op_alias(base_op_alias) == expected_op_name assert translate_op_alias(f"C({base_op_alias})") == f"C({expected_op_name})" + assert translate_op_alias(f"Controlled({base_op_alias})") == f"C({expected_op_name})" assert translate_op_alias(f"Adjoint({base_op_alias})") == f"Adjoint({expected_op_name})" assert translate_op_alias(f"Pow({base_op_alias})") == f"Pow({expected_op_name})" diff --git a/tests/decomposition/test_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py index 67c777464df..3b14c55c5e8 100644 --- a/tests/decomposition/test_decomposition_graph.py +++ b/tests/decomposition/test_decomposition_graph.py @@ -30,13 +30,15 @@ adjoint_resource_rep, controlled_resource_rep, pow_resource_rep, + resource_rep, ) +from pennylane.decomposition.decomposition_graph import _to_name @pytest.mark.unit @patch( "pennylane.decomposition.decomposition_graph.list_decomps", - side_effect=lambda x: decompositions[x], + side_effect=lambda x: decompositions[_to_name(x)], ) class TestDecompositionGraph: @@ -55,14 +57,14 @@ def custom_hadamard_2(wires): qml.RY(np.pi / 2, wires=wires) graph = DecompositionGraph(operations=[qml.Hadamard(0)], gate_set={"RX", "RY", "RZ"}) - assert graph._get_decompositions(qml.Hadamard) == decompositions[qml.Hadamard] + assert graph._get_decompositions(resource_rep(qml.H)) == decompositions["Hadamard"] graph = DecompositionGraph( operations=[qml.Hadamard(0)], gate_set={"RX", "RY", "RZ"}, fixed_decomps={qml.Hadamard: custom_hadamard}, ) - assert graph._get_decompositions(qml.Hadamard) == [custom_hadamard] + assert graph._get_decompositions(resource_rep(qml.H)) == [custom_hadamard] alt_dec = [custom_hadamard, custom_hadamard_2] graph = DecompositionGraph( @@ -70,8 +72,8 @@ def custom_hadamard_2(wires): gate_set={"RX", "RY", "RZ"}, alt_decomps={qml.Hadamard: alt_dec}, ) - exp_dec = alt_dec + decompositions[qml.Hadamard] - assert graph._get_decompositions(qml.Hadamard) == exp_dec + exp_dec = alt_dec + decompositions["Hadamard"] + assert graph._get_decompositions(resource_rep(qml.H)) == exp_dec graph = DecompositionGraph( operations=[qml.Hadamard(0)], @@ -79,7 +81,7 @@ def custom_hadamard_2(wires): alt_decomps={qml.Hadamard: alt_dec}, fixed_decomps={qml.Hadamard: custom_hadamard}, ) - assert graph._get_decompositions(qml.Hadamard) == [custom_hadamard] + assert graph._get_decompositions(resource_rep(qml.H)) == [custom_hadamard] def test_graph_construction(self, _): """Tests constructing a graph from a single Hadamard.""" diff --git a/tests/decomposition/test_decomposition_rule.py b/tests/decomposition/test_decomposition_rule.py index 22b251fe097..9c9c25007b3 100644 --- a/tests/decomposition/test_decomposition_rule.py +++ b/tests/decomposition/test_decomposition_rule.py @@ -177,7 +177,7 @@ def custom_decomp4(theta, wires, **__): with pytest.raises(TypeError, match="decomposition rule must be a qfunc with a resource"): qml.add_decomps(CustomOp, custom_decomp4) - _decompositions.pop(CustomOp) # cleanup + _decompositions.pop("CustomOp") # cleanup def test_auto_wrap_in_resource_op(self): """Tests that simply classes can be auto-wrapped in a ``CompressionResourceOp``.""" diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index 54ceea53cc6..b704fb0a6f1 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -20,11 +20,11 @@ from pennylane.decomposition.resources import Resources, pow_resource_rep from pennylane.decomposition.symbolic_decomposition import ( AdjointDecomp, - adjoint_adjoint_decomp, adjoint_controlled_decomp, adjoint_pow_decomp, - pow_decomp, - pow_pow_decomp, + cancel_adjoint, + merge_powers, + repeat_pow_base, same_type_adjoint_decomp, ) from tests.decomposition.conftest import to_resources @@ -39,12 +39,10 @@ def test_adjoint_adjoint(self): op = qml.adjoint(qml.adjoint(qml.RX(0.5, wires=0))) with qml.queuing.AnnotatedQueue() as q: - adjoint_adjoint_decomp(*op.parameters, wires=op.wires, **op.hyperparameters) + cancel_adjoint(*op.parameters, wires=op.wires, **op.hyperparameters) assert q.queue == [qml.RX(0.5, wires=0)] - assert adjoint_adjoint_decomp.compute_resources(**op.resource_params) == to_resources( - {qml.RX: 1} - ) + assert cancel_adjoint.compute_resources(**op.resource_params) == to_resources({qml.RX: 1}) @pytest.mark.jax def test_adjoint_adjoint_capture(self): @@ -58,7 +56,7 @@ def test_adjoint_adjoint_capture(self): qml.capture.enable() def circuit(): - adjoint_adjoint_decomp(*op.parameters, wires=op.wires, **op.hyperparameters) + cancel_adjoint(*op.parameters, wires=op.wires, **op.hyperparameters) plxpr = qml.capture.make_plxpr(circuit)() collector = CollectOpsandMeas() @@ -207,10 +205,10 @@ def test_pow_pow(self): op = qml.pow(qml.pow(qml.H(0), 3), 2) with qml.queuing.AnnotatedQueue() as q: - pow_pow_decomp(*op.parameters, wires=op.wires, **op.hyperparameters) + merge_powers(*op.parameters, wires=op.wires, **op.hyperparameters) assert q.queue == [qml.pow(qml.H(0), 6)] - assert pow_pow_decomp.compute_resources(**op.resource_params) == to_resources( + assert merge_powers.compute_resources(**op.resource_params) == to_resources( {pow_resource_rep(qml.H, {}, 6): 1} ) @@ -219,10 +217,10 @@ def test_pow_general(self): op = qml.pow(qml.H(0), 3) with qml.queuing.AnnotatedQueue() as q: - pow_decomp(*op.parameters, wires=op.wires, **op.hyperparameters) + repeat_pow_base(*op.parameters, wires=op.wires, **op.hyperparameters) assert q.queue == [qml.H(0), qml.H(0), qml.H(0)] - assert pow_decomp.compute_resources(**op.resource_params) == to_resources({qml.H: 3}) + assert repeat_pow_base.compute_resources(**op.resource_params) == to_resources({qml.H: 3}) @pytest.mark.jax def test_pow_general_capture(self): @@ -236,7 +234,7 @@ def test_pow_general_capture(self): qml.capture.enable() def circuit(): - pow_decomp(*op.parameters, wires=op.wires, **op.hyperparameters) + repeat_pow_base(*op.parameters, wires=op.wires, **op.hyperparameters) plxpr = qml.capture.make_plxpr(circuit)() collector = CollectOpsandMeas() @@ -251,7 +249,7 @@ def test_non_integer_pow_not_implemented(self): op = qml.pow(qml.H(0), 0.5) with pytest.raises(NotImplementedError, match="Non-integer or negative powers"): - pow_decomp.compute_resources(**op.resource_params) + repeat_pow_base.compute_resources(**op.resource_params) op = qml.pow(qml.H(0), -1) with pytest.raises(NotImplementedError, match="Non-integer or negative powers"): - pow_decomp.compute_resources(**op.resource_params) + repeat_pow_base.compute_resources(**op.resource_params) From fa09eac48f45474db1f7bea8f297e4ce04430a08 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 11:29:02 -0400 Subject: [PATCH 02/45] fix doc? --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 59e7d50e3ee..52b8fe5ff2a 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -118,7 +118,7 @@ 1: ────╭●─────│─────╭●─────│───T─╰X──T†─╰X─┤ 2: ──H─╰X──T†─╰X──T─╰X──T†─╰X──T──H────────┤ ``` - * Symbolic operator types can now be given as strings to the `op_type` argument of :func:`~.add_decomps`, + * Symbolic operator types can now be given as strings to the `op_type` argument of :func:`~.decomposition.add_decomps`, or as keys of the dictionaries passed to the `alt_decomps` and `fixed_decomps` arguments of the :func:`~.transforms.decompose` transform, allowing custom decomposition rules to be defined and registered for symbolic operators. From 95c03833e570d784f2c9d94a14767ca9e2675140 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 12:17:19 -0400 Subject: [PATCH 03/45] tests and bug fix --- .../decomposition/decomposition_graph.py | 22 +-- tests/decomposition/conftest.py | 33 ++++- .../decomposition/test_decomposition_graph.py | 131 ++++++++++-------- .../decomposition/test_decomposition_rule.py | 15 ++ 4 files changed, 135 insertions(+), 66 deletions(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index 2a28b40d804..66a63a42b23 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -54,6 +54,8 @@ ) from .utils import DecompositionError, DecompositionNotApplicable, translate_op_alias +NULL = "null" # sentinel value for the start node in the graph + class DecompositionGraph: # pylint: disable=too-many-instance-attributes """A graph that models a decomposition problem. @@ -136,7 +138,6 @@ def __init__( # Tracks the node indices of various operators. self._original_ops_indices: set[int] = set() - self._target_ops_indices: set[int] = set() self._all_op_indices: dict[CompressedResourceOp, int] = {} # Stores the library of custom decomposition rules @@ -150,6 +151,7 @@ def __init__( self._visitor = None # Construct the decomposition graph + self._start = self._graph.add_node(NULL) self._construct_graph(operations) def _get_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: @@ -196,7 +198,7 @@ def _recursively_add_op_node(self, op_node: CompressedResourceOp) -> int: self._all_op_indices[op_node] = op_node_idx if op_node.name in self._gate_set: - self._target_ops_indices.add(op_node_idx) + self._graph.add_edge(self._start, op_node_idx, 1) return op_node_idx for decomposition in self._get_decompositions(op_node): @@ -287,6 +289,11 @@ def _recursively_add_decomposition_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)) @@ -302,17 +309,12 @@ def solve(self, lazy=True): """ self._visitor = _DecompositionSearchVisitor(self._graph, self._original_ops_indices, lazy) - start = self._graph.add_node("dummy") - self._graph.add_edges_from( - [(start, op_node_idx, 1) for op_node_idx in self._target_ops_indices] - ) rx.dijkstra_search( self._graph, - source=[start], + source=[self._start], weight_fn=self._visitor.edge_weight, visitor=self._visitor, ) - self._graph.remove_node(start) if self._visitor.unsolved_op_indices: unsolved_ops = [self._graph[op_idx] for op_idx in self._visitor.unsolved_op_indices] op_names = set(op.name for op in unsolved_ops) @@ -450,6 +452,8 @@ def examine_edge(self, edge): return # nothing is to be done for edges leading to an operator node if target_idx not in self.distances: self.distances[target_idx] = Resources() # initialize with empty resource + if src_node == NULL: + return # special case for when the decomposition produces nothing self.distances[target_idx] += self.distances[src_idx] * target_node.count(src_node) if target_idx not in self._num_edges_examined: self._num_edges_examined[target_idx] = 0 @@ -465,7 +469,7 @@ def edge_relaxed(self, edge): """Triggered when an edge is relaxed during the Dijkstra search.""" src_idx, target_idx, _ = edge target_node = self._graph[target_idx] - if self._graph[src_idx] == "dummy": + if self._graph[src_idx] == NULL and not isinstance(target_node, _DecompositionNode): self.distances[target_idx] = Resources({target_node: 1}) elif isinstance(target_node, CompressedResourceOp): self.predecessors[target_idx] = src_idx diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py index 540b0a14b71..c9440af424c 100644 --- a/tests/decomposition/conftest.py +++ b/tests/decomposition/conftest.py @@ -19,7 +19,7 @@ from collections import defaultdict import pennylane as qml -from pennylane.decomposition import Resources +from pennylane.decomposition import DecompositionNotApplicable, Resources from pennylane.decomposition.decomposition_rule import _auto_wrap decompositions = defaultdict(list) @@ -125,3 +125,34 @@ def _t_ps(wires, **__): decompositions["T"] = [_t_ps] + + +@qml.register_resources({qml.H: 1}) +def _adjoint_hadamard(*_, **__): + raise NotImplementedError + + +decompositions["Adjoint(Hadamard)"] = [_adjoint_hadamard] + + +def _pow_hadamard_resource(z, **__): + return {qml.H: z % 2} + + +@qml.register_resources(_pow_hadamard_resource) +def _pow_hadamard(*_, wires, z, **__): + qml.cond(z % 2 == 1, qml.H)(wires=wires) + + +decompositions["Pow(Hadamard)"] = [_pow_hadamard] + + +def _controlled_hadamard_resource(num_control_wires, num_zero_control_values, **__): + if num_control_wires > 1: + raise DecompositionNotApplicable + return {qml.CH: 1, qml.X: num_zero_control_values * 2} + + +@qml.register_resources(_controlled_hadamard_resource) +def _controlled_hadamard(*_, **__): + raise NotImplementedError diff --git a/tests/decomposition/test_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py index 3b14c55c5e8..a4c9fc08fcf 100644 --- a/tests/decomposition/test_decomposition_graph.py +++ b/tests/decomposition/test_decomposition_graph.py @@ -88,17 +88,19 @@ def test_graph_construction(self, _): op = qml.Hadamard(wires=[0]) graph = DecompositionGraph(operations=[op], gate_set={"RX", "RZ", "GlobalPhase"}) - # 5 ops and 3 decompositions (2 for Hadamard and 1 for RY) - assert len(graph._graph.nodes()) == 8 - # 8 edges from ops to decompositions and 3 from decompositions to ops - assert len(graph._graph.edges()) == 11 + # 5 ops and 3 decompositions (2 for Hadamard and 1 for RY) and 1 dummy starting node + assert len(graph._graph.nodes()) == 9 + # 8 edges from ops to decompositions, 3 from decompositions to ops, and 3 from the + # dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 14 # Check that graph construction stops at gates in the target gate set. graph2 = DecompositionGraph(operations=[op], gate_set={"RY", "RZ", "GlobalPhase"}) - # 5 ops and 2 decompositions (RY is in the target gate set now) - assert len(graph2._graph.nodes()) == 7 - # 6 edges from ops to decompositions and 2 from decompositions to ops - assert len(graph2._graph.edges()) == 8 + # 5 ops and 2 decompositions (RY is in the target gate set now), and the dummy starting node + assert len(graph2._graph.nodes()) == 8 + # 6 edges from ops to decompositions and 2 from decompositions to ops, + # and 3 from the dummy starting node to the target gate set. + assert len(graph2._graph.edges()) == 11 def test_graph_construction_non_applicable_rules(self, _): """Tests rules that raise DecompositionNotApplicable are skipped.""" @@ -135,10 +137,12 @@ def some_other_rule(*_, **__): gate_set={"CNOT", "RZ"}, alt_decomps={CustomOp: [some_rule, some_other_rule]}, ) - # 3 ops (CustomOp, CNOT, RZ) and 1 decompositions (only some_other_rule) - assert len(graph._graph.nodes()) == 4 - # 2 edges from ops to decompositions and 1 from decompositions to ops - assert len(graph._graph.edges()) == 3 + # 3 ops (CustomOp, CNOT, RZ) and 1 decompositions (only some_other_rule), + # and the dummy starting node + assert len(graph._graph.nodes()) == 5 + # 2 edges from ops to decompositions, 1 from decompositions to ops, + # and 2 from the dummy starting node to the target gate set + assert len(graph._graph.edges()) == 5 def test_gate_set(self, _): """Tests that graph construction stops at the target gate set.""" @@ -175,10 +179,12 @@ def custom_decomp(wires): fixed_decomps={CustomOp: custom_decomp}, ) - # 1 node for CustomOp, 1 decomposition node, and 5 for the ops in the decomposition - assert len(graph._graph.nodes()) == 7 - # 5 edges from ops to decompositions and 1 edge from decompositions to ops - assert len(graph._graph.edges()) == 6 + # 1 node for CustomOp, 1 decomposition node, 5 for the ops in the decomposition, + # and the dummy starting node. + assert len(graph._graph.nodes()) == 8 + # 5 edges from ops to decompositions, 1 edge from decompositions to ops, and 5 + # edges from the dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 11 def test_graph_solve(self, _): """Tests solving a simple graph for the optimal decompositions.""" @@ -286,9 +292,11 @@ def _custom_decomp(*_, **__): ) # 10 ops (CustomOp, MultiRZ(4), MultiRZ(3), CNOT, CZ, RX, RY, RZ, Hadamard, GlobalPhase) # 7 decompositions (1 for CustomOp, 1 for each of the two MultiRZs, 1 for CNOT, 2 for Hadamard, and 1 for RY) - assert len(graph._graph.nodes()) == 17 - # 16 edges from ops to decompositions and 7 from decompositions to ops - assert len(graph._graph.edges()) == 23 + # and the dummy starting node + assert len(graph._graph.nodes()) == 18 + # 16 edges from ops to decompositions and 7 from decompositions to ops, + # and 4 edges from the dummy starting node to the target gate set + assert len(graph._graph.edges()) == 27 graph.solve() assert graph.resource_estimate(op) == to_resources( @@ -319,10 +327,11 @@ def test_controlled_global_phase(self, _): op1 = qml.ctrl(qml.GlobalPhase(0.5), control=[1]) op2 = qml.ctrl(qml.GlobalPhase(0.5), control=[1, 2]) graph = DecompositionGraph([op1, op2], gate_set={"ControlledPhaseShift", "PhaseShift"}) - # 4 op nodes and 2 decomposition nodes. - assert len(graph._graph.nodes()) == 6 - # 2 edges from decompositions to ops and 2 edges from ops to decompositions - assert len(graph._graph.edges()) == 4 + # 4 op nodes and 2 decomposition nodes, and 1 dummy starting node. + assert len(graph._graph.nodes()) == 7 + # 2 edges from decompositions to ops and 2 edges from ops to decompositions, + # and 2 edges from the dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 6 # Verify the decompositions graph.solve() @@ -344,10 +353,11 @@ def test_custom_controlled_op(self, _): operations=[op1, op2], gate_set={"CNOT", "CH"}, ) - # 4 op nodes and 2 decomposition nodes. - assert len(graph._graph.nodes()) == 6 + # 4 op nodes and 2 decomposition nodes, and the dummy starting node + assert len(graph._graph.nodes()) == 7 # 2 edges from decompositions to ops and 2 edges from ops to decompositions - assert len(graph._graph.edges()) == 4 + # and 2 edges from the dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 6 # Verify the decompositions graph.solve() @@ -415,10 +425,11 @@ def custom_controlled_decomp(wires): CustomControlledOp: [custom_controlled_decomp], }, ) - # 18 op nodes and 16 decomposition nodes. - assert len(graph._graph.nodes()) == 34 + # 18 op nodes and 16 decomposition nodes, and the dummy starting node + assert len(graph._graph.nodes()) == 35 # 16 edges from decompositions to ops and 36 edges from ops to decompositions - assert len(graph._graph.edges()) == 52 + # and 6 edge from the dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 58 graph.solve() @@ -445,8 +456,9 @@ def test_adjoint_adjoint(self, _): graph = DecompositionGraph(operations=[op], gate_set={"RX"}) # 2 operator nodes (Adjoint(Adjoint(RX)) and RX), and 1 decomposition node. - assert len(graph._graph.nodes()) == 3 - assert len(graph._graph.edges()) == 2 + # and the dummy starting node + assert len(graph._graph.nodes()) == 4 + assert len(graph._graph.edges()) == 3 graph.solve() with qml.queuing.AnnotatedQueue() as q: @@ -461,19 +473,19 @@ def test_adjoint_pow(self, _): op = qml.adjoint(qml.pow(qml.H(0), z=3)) graph = DecompositionGraph(operations=[op], gate_set={"H"}) - # 3 operator nodes: Adjoint(Pow(H)), Pow(H), and H - # 2 decomposition nodes for Adjoint(Pow(H)) and Pow(H) - assert len(graph._graph.nodes()) == 5 - # 2 edges from decompositions to ops and 2 edges from ops to decompositions - assert len(graph._graph.edges()) == 4 + # 3 operator nodes: Adjoint(Pow(H)), Pow(H), and H, and 1 dummy starting node + # 1 decomposition nodes for Adjoint(Pow(H)) and two decomposition nodes for Pow(H) + assert len(graph._graph.nodes()) == 7 + # 3 edges from decompositions to ops and 3 edges from ops to decompositions + # and 1 edge from the dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 7 graph.solve() with qml.queuing.AnnotatedQueue() as q: graph.decomposition(op)(*op.parameters, wires=op.wires, **op.hyperparameters) assert q.queue == [qml.pow(qml.H(0), z=3)] - # TODO: There should just be a single `H` after we have full support of Pow decompositions. - assert graph.resource_estimate(op) == to_resources({qml.H: 3}) + assert graph.resource_estimate(op) == to_resources({qml.H: 1}) def test_adjoint_custom(self, _): """Tests adjoint of an operator that defines its own adjoint.""" @@ -481,9 +493,9 @@ def test_adjoint_custom(self, _): op = qml.adjoint(qml.RX(0.5, wires=[0])) graph = DecompositionGraph(operations=[op], gate_set={"RX"}) - # 2 operator nodes (Adjoint(RX) and RX), and 1 decomposition node. - assert len(graph._graph.nodes()) == 3 - assert len(graph._graph.edges()) == 2 + # 2 operator nodes (Adjoint(RX) and RX), and 1 decomposition node, and 1 dummy starting node + assert len(graph._graph.nodes()) == 4 + assert len(graph._graph.edges()) == 3 graph.solve() with qml.queuing.AnnotatedQueue() as q: @@ -501,9 +513,11 @@ def test_adjoint_controlled(self, _): graph = DecompositionGraph(operations=[op, op2], gate_set={"ControlledPhaseShift", "CRX"}) # 5 operator nodes: Adjoint(C(RX)), Adjoint(C(U1)), CRX, C(U1), ControlledPhaseShift # 3 decomposition nodes leading into Adjoint(C(RX)), Adjoint(C(U1)), and C(U1) - assert len(graph._graph.nodes()) == 8 - # 3 edges from decompositions to ops and 3 edges from ops to decompositions - assert len(graph._graph.edges()) == 6 + # and the dummy starting node + assert len(graph._graph.nodes()) == 9 + # 3 edges from decompositions to ops and 3 edges from ops to decompositions, + # and 2 edges from the dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 8 graph.solve() with qml.queuing.AnnotatedQueue() as q: @@ -546,10 +560,13 @@ def custom_decomp(phi, wires): alt_decomps={CustomOp: [custom_decomp]}, ) # 10 operator nodes: A(CustomOp), A(H), A(CNOT), A(RX), A(T), H, CNOT, RX, A(PhaseShift), PhaseShift - # 6 decomposition nodes for: A(CustomOp), A(H), A(CNOT), A(RX), A(T), A(PhaseShift) - assert len(graph._graph.nodes()) == 16 - # 9 edges from ops to decompositions and 6 edges from decompositions to ops. - assert len(graph._graph.edges()) == 15 + # 5 decomposition nodes for: A(CustomOp), A(CNOT), A(RX), A(T), A(PhaseShift) + # 2 decomposition nodes for A(H) + # 1 dummy starting node + assert len(graph._graph.nodes()) == 18 + # 10 edges from ops to decompositions and 7 edges from decompositions to ops. + # and 4 edges from the dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 21 graph.solve() with qml.queuing.AnnotatedQueue() as q: @@ -566,27 +583,29 @@ def custom_decomp(phi, wires): {qml.H: 1, qml.CNOT: 2, qml.RX: 1, qml.PhaseShift: 1} ) - def test_pow_pow(self, _): + def test_nested_powers(self, _): """Tests nested power decompositions.""" op = qml.pow(qml.pow(qml.H(0), 3), 2) graph = DecompositionGraph(operations=[op], gate_set={"H"}) # 3 operator nodes: Pow(Pow(H)), Pow(H), and H - # 2 decomposition nodes for Pow(Pow(H)) and Pow(H) - assert len(graph._graph.nodes()) == 5 - # 2 edges from decompositions to ops and 2 edges from ops to decompositions - assert len(graph._graph.edges()) == 4 + # 1 decomposition nodes for Pow(Pow(H)) and 2 nodes for Pow(H) + # and the dummy starting node + assert len(graph._graph.nodes()) == 7 + # 3 edges from decompositions to ops and 3 edges from ops to decompositions + # and 1 edge from the dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 7 graph.solve() with qml.queuing.AnnotatedQueue() as q: graph.decomposition(op)(*op.parameters, wires=op.wires, **op.hyperparameters) assert q.queue == [qml.pow(qml.H(0), 6)] - assert graph.resource_estimate(op) == to_resources({qml.H: 6}) + assert graph.resource_estimate(op) == to_resources({}) op2 = qml.pow(qml.H(0), 6) with qml.queuing.AnnotatedQueue() as q: graph.decomposition(op2)(*op2.parameters, wires=op2.wires, **op2.hyperparameters) - assert q.queue == [qml.H(0), qml.H(0), qml.H(0), qml.H(0), qml.H(0), qml.H(0)] - assert graph.resource_estimate(op2) == to_resources({qml.H: 6}) + assert q.queue == [] + assert graph.resource_estimate(op2) == to_resources({}) diff --git a/tests/decomposition/test_decomposition_rule.py b/tests/decomposition/test_decomposition_rule.py index 9c9c25007b3..26247100296 100644 --- a/tests/decomposition/test_decomposition_rule.py +++ b/tests/decomposition/test_decomposition_rule.py @@ -179,6 +179,21 @@ def custom_decomp4(theta, wires, **__): _decompositions.pop("CustomOp") # cleanup + def test_custom_symbolic_decomposition(self): + """Tests that custom decomposition rules for symbolic operators can be registered.""" + + class CustomOp(qml.operation.Operation): # pylint: disable=too-few-public-methods + pass + + @qml.register_resources({qml.RX: 1, qml.RZ: 1}) + def my_adjoint_custom_op(theta, wires, **__): + qml.RX(theta, wires=wires[0]) + qml.RZ(theta, wires=wires[1]) + + qml.add_decomps("Adjoint(CustomOp)", my_adjoint_custom_op) + assert qml.decomposition.has_decomp("Adjoint(CustomOp)") + assert qml.list_decomps("Adjoint(CustomOp)") == [my_adjoint_custom_op] + def test_auto_wrap_in_resource_op(self): """Tests that simply classes can be auto-wrapped in a ``CompressionResourceOp``.""" From 8f0a0de39cd6336c114cf838cc8d6a443ba76ce9 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 12:21:13 -0400 Subject: [PATCH 04/45] update changelog --- doc/releases/changelog-dev.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 52b8fe5ff2a..ad9db07e711 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -122,6 +122,7 @@ or as keys of the dictionaries passed to the `alt_decomps` and `fixed_decomps` arguments of the :func:`~.transforms.decompose` transform, allowing custom decomposition rules to be defined and registered for symbolic operators. + [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347) ```python @register_resources({qml.RY: 1}) def my_adjoint_ry(phi, wires, **_): From 5b074ed794ee1cc4c8ed3d74b0262207a7f1c1f9 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 12:33:14 -0400 Subject: [PATCH 05/45] add more tests --- tests/decomposition/conftest.py | 12 ++++++--- .../decomposition/test_decomposition_graph.py | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py index c9440af424c..111eaa5ab30 100644 --- a/tests/decomposition/conftest.py +++ b/tests/decomposition/conftest.py @@ -128,8 +128,8 @@ def _t_ps(wires, **__): @qml.register_resources({qml.H: 1}) -def _adjoint_hadamard(*_, **__): - raise NotImplementedError +def _adjoint_hadamard(*_, wires, **__): + qml.H(wires) decompositions["Adjoint(Hadamard)"] = [_adjoint_hadamard] @@ -154,5 +154,9 @@ def _controlled_hadamard_resource(num_control_wires, num_zero_control_values, ** @qml.register_resources(_controlled_hadamard_resource) -def _controlled_hadamard(*_, **__): - raise NotImplementedError +def _controlled_hadamard(*_, wires, control_values, **__): + if not control_values[0]: + qml.PauliX(wires=wires[0]) + qml.CH(wires=wires) + if not control_values[0]: + qml.PauliX(wires=wires[0]) diff --git a/tests/decomposition/test_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py index a4c9fc08fcf..609df6660b0 100644 --- a/tests/decomposition/test_decomposition_graph.py +++ b/tests/decomposition/test_decomposition_graph.py @@ -609,3 +609,30 @@ def test_nested_powers(self, _): assert q.queue == [] assert graph.resource_estimate(op2) == to_resources({}) + + def test_custom_symbolic_decompositions(self, _): + """Tests that custom symbolic decompositions are used.""" + + graph = DecompositionGraph( + operations=[ + qml.adjoint(qml.H(0)), + qml.pow(qml.H(1), 3), + qml.ops.Controlled(qml.H(0), control_wires=1), + ], + gate_set={"H", "CH"}, + ) + + op1 = qml.adjoint(qml.H(0)) + op2 = qml.pow(qml.H(1), 3) + op3 = qml.ops.Controlled(qml.H(0), control_wires=1) + + graph.solve() + with qml.queuing.AnnotatedQueue() as q: + graph.decomposition(op1)(*op1.parameters, wires=op1.wires, **op1.hyperparameters) + graph.decomposition(op2)(*op2.parameters, wires=op2.wires, **op2.hyperparameters) + graph.decomposition(op3)(*op3.parameters, wires=op3.wires, **op3.hyperparameters) + + assert q.queue == [qml.H(0), qml.H(1), qml.CH(wires=[1, 0])] + assert graph.resource_estimate(op1) == to_resources({qml.H: 1}) + assert graph.resource_estimate(op2) == to_resources({qml.H: 1}) + assert graph.resource_estimate(op3) == to_resources({qml.CH: 1}) From ff9088226621c49cd4c1411dacd196dfef6e2bf1 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 12:52:22 -0400 Subject: [PATCH 06/45] one more test case --- tests/decomposition/test_decomposition_graph.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/decomposition/test_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py index 609df6660b0..956c17645ca 100644 --- a/tests/decomposition/test_decomposition_graph.py +++ b/tests/decomposition/test_decomposition_graph.py @@ -613,26 +613,35 @@ def test_nested_powers(self, _): def test_custom_symbolic_decompositions(self, _): """Tests that custom symbolic decompositions are used.""" + @qml.register_resources({qml.RX: 1}) + def my_adjoint_rx(theta, wires, **__): + qml.RX(-theta, wires=wires) + graph = DecompositionGraph( operations=[ qml.adjoint(qml.H(0)), qml.pow(qml.H(1), 3), qml.ops.Controlled(qml.H(0), control_wires=1), + qml.adjoint(qml.RX(0.5, wires=0)), ], - gate_set={"H", "CH"}, + fixed_decomps={"Adjoint(RX)": my_adjoint_rx}, + gate_set={"H", "CH", "RX"}, ) op1 = qml.adjoint(qml.H(0)) op2 = qml.pow(qml.H(1), 3) op3 = qml.ops.Controlled(qml.H(0), control_wires=1) + op4 = qml.adjoint(qml.RX(0.5, wires=0)) graph.solve() with qml.queuing.AnnotatedQueue() as q: graph.decomposition(op1)(*op1.parameters, wires=op1.wires, **op1.hyperparameters) graph.decomposition(op2)(*op2.parameters, wires=op2.wires, **op2.hyperparameters) graph.decomposition(op3)(*op3.parameters, wires=op3.wires, **op3.hyperparameters) + graph.decomposition(op4)(*op4.parameters, wires=op4.wires, **op4.hyperparameters) - assert q.queue == [qml.H(0), qml.H(1), qml.CH(wires=[1, 0])] + assert q.queue == [qml.H(0), qml.H(1), qml.CH(wires=[1, 0]), qml.RX(-0.5, wires=0)] assert graph.resource_estimate(op1) == to_resources({qml.H: 1}) assert graph.resource_estimate(op2) == to_resources({qml.H: 1}) assert graph.resource_estimate(op3) == to_resources({qml.CH: 1}) + assert graph.resource_estimate(op4) == to_resources({qml.RX: 1}) From 956b9a275f49e26fa674a2fd0b4c7d4648d606dd Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 13:54:34 -0400 Subject: [PATCH 07/45] pylint --- tests/decomposition/conftest.py | 15 --------------- tests/decomposition/test_decomposition_rule.py | 3 --- 2 files changed, 18 deletions(-) diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py index 111eaa5ab30..bd177694152 100644 --- a/tests/decomposition/conftest.py +++ b/tests/decomposition/conftest.py @@ -145,18 +145,3 @@ def _pow_hadamard(*_, wires, z, **__): decompositions["Pow(Hadamard)"] = [_pow_hadamard] - - -def _controlled_hadamard_resource(num_control_wires, num_zero_control_values, **__): - if num_control_wires > 1: - raise DecompositionNotApplicable - return {qml.CH: 1, qml.X: num_zero_control_values * 2} - - -@qml.register_resources(_controlled_hadamard_resource) -def _controlled_hadamard(*_, wires, control_values, **__): - if not control_values[0]: - qml.PauliX(wires=wires[0]) - qml.CH(wires=wires) - if not control_values[0]: - qml.PauliX(wires=wires[0]) diff --git a/tests/decomposition/test_decomposition_rule.py b/tests/decomposition/test_decomposition_rule.py index 26247100296..94f67064294 100644 --- a/tests/decomposition/test_decomposition_rule.py +++ b/tests/decomposition/test_decomposition_rule.py @@ -182,9 +182,6 @@ def custom_decomp4(theta, wires, **__): def test_custom_symbolic_decomposition(self): """Tests that custom decomposition rules for symbolic operators can be registered.""" - class CustomOp(qml.operation.Operation): # pylint: disable=too-few-public-methods - pass - @qml.register_resources({qml.RX: 1, qml.RZ: 1}) def my_adjoint_custom_op(theta, wires, **__): qml.RX(theta, wires=wires[0]) From 32dba0bf071d2b610dd1e986e344fcf8ddbbd0bc Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 14:03:00 -0400 Subject: [PATCH 08/45] pylint --- tests/decomposition/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py index bd177694152..f57a0267eaf 100644 --- a/tests/decomposition/conftest.py +++ b/tests/decomposition/conftest.py @@ -19,7 +19,7 @@ from collections import defaultdict import pennylane as qml -from pennylane.decomposition import DecompositionNotApplicable, Resources +from pennylane.decomposition import Resources from pennylane.decomposition.decomposition_rule import _auto_wrap decompositions = defaultdict(list) From 7b37f5bd8ef070a448587f18c2cbfa18a1385f22 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 14:56:32 -0400 Subject: [PATCH 09/45] [Decomposition] Clean up custom logic for adjoint and pow --- .../decomposition/decomposition_graph.py | 123 +++---- pennylane/decomposition/decomposition_rule.py | 6 + pennylane/decomposition/resources.py | 2 +- .../decomposition/symbolic_decomposition.py | 318 +++++++----------- pennylane/ops/functions/assert_valid.py | 4 + pennylane/ops/identity.py | 17 + pennylane/ops/op_math/controlled.py | 26 +- pennylane/ops/op_math/controlled_ops.py | 49 ++- pennylane/ops/qubit/arithmetic_ops.py | 3 + pennylane/ops/qubit/matrix_ops.py | 100 ++++-- pennylane/ops/qubit/non_parametric_ops.py | 168 ++++++++- .../ops/qubit/parametric_ops_multi_qubit.py | 47 ++- .../ops/qubit/parametric_ops_single_qubit.py | 46 ++- pennylane/ops/qubit/qchem_ops.py | 21 ++ tests/decomposition/conftest.py | 32 +- .../decomposition/test_decomposition_graph.py | 71 +--- tests/decomposition/test_resources.py | 2 +- .../test_symbolic_decomposition.py | 245 ++++++++------ 18 files changed, 788 insertions(+), 492 deletions(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index 66a63a42b23..9947d0ebb7e 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -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 @@ -164,10 +166,24 @@ def _get_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRul decomps = self._alt_decomps.get(op_name, []) + list_decomps(op_name) - if issubclass(op.op_type, qml.ops.Adjoint): + if ( + issubclass(op.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)) - elif issubclass(op.op_type, qml.ops.Pow): + elif ( + issubclass(op.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)) elif op.op_type in (qml.ops.Controlled, qml.ops.ControlledOp): @@ -180,10 +196,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 @@ -202,56 +218,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: 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) + decomp_resource = rule.compute_resources(**op.params) + 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) except DecompositionNotApplicable: pass # ignore decompositions that are not applicable to the given op params. - def _get_adjoint_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: - """Retrieves a list of decomposition rules for an adjoint operator.""" + def _get_adjoint_decompositions(self, op) -> list[DecompositionRule]: + """Gets the decomposition rules for the adjoint of an operator.""" base_class, base_params = op.params["base_class"], op.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: CompressedResourceOp) -> list[DecompositionRule]: - """Retrieves a list of decomposition rules for a power operator.""" + def _get_pow_decompositions(op) -> list[DecompositionRule]: + """Gets the decomposition rules for the power of an operator.""" base_class = op.params["base_class"] + # Special case: power of zero + if op.params["z"] == 0: + return [null_decomp] + + if op.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(self, op: CompressedResourceOp) -> list[DecompositionRule]: @@ -277,28 +300,6 @@ def _get_controlled_decompositions(self, op: CompressedResourceOp) -> list[Decom base_rep = resource_rep(base_class, **op.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. diff --git a/pennylane/decomposition/decomposition_rule.py b/pennylane/decomposition/decomposition_rule.py index 9625c4973de..79787656af9 100644 --- a/pennylane/decomposition/decomposition_rule.py +++ b/pennylane/decomposition/decomposition_rule.py @@ -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 diff --git a/pennylane/decomposition/resources.py b/pennylane/decomposition/resources.py index d4f71c06aaa..9c84258c7dd 100644 --- a/pennylane/decomposition/resources.py +++ b/pennylane/decomposition/resources.py @@ -326,7 +326,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, diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index dc4acf40084..2b576e69373 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -16,238 +16,166 @@ from __future__ import annotations -import functools +import numpy as np import pennylane as qml -from .controlled_decomposition import base_to_custom_ctrl_op from .decomposition_rule import DecompositionRule, register_resources from .resources import adjoint_resource_rep, pow_resource_rep, resource_rep +from .utils import DecompositionNotApplicable -class AdjointDecomp(DecompositionRule): # pylint: disable=too-few-public-methods - """The adjoint version of a decomposition rule.""" +def make_adjoint_decomp(base_decomposition: DecompositionRule): + """Create a decomposition rule for the adjoint of a decomposition rule.""" - def __init__(self, base_decomposition: DecompositionRule): - self._base_decomposition = base_decomposition - super().__init__(self._get_impl(), self._get_resource_fn()) + def _resource_fn(base_class, base_params): # pylint: disable=unused-argument + base_resources = base_decomposition.compute_resources(**base_params) + return { + adjoint_resource_rep(decomp_op.op_type, decomp_op.params): count + for decomp_op, count in base_resources.gate_counts.items() + } - def _get_impl(self): - """The implementation of the adjoint of a gate.""" + @register_resources(_resource_fn) + def _impl(*params, wires, base, **__): + # pylint: disable=protected-access + qml.adjoint(base_decomposition._impl)(*params, wires=wires, **base.hyperparameters) - def _impl(*params, wires, base, **__): - qml.adjoint(self._base_decomposition._impl)( # pylint: disable=protected-access - *params, wires, **base.hyperparameters - ) + return _impl - return _impl - def _get_resource_fn(self): - """The resource function of the adjoint of a gate.""" +def _cancel_adjoint_resource(*_, base_params, **__): + # The base of a nested adjoint is an adjoint, so the base of the base is the original operator, + # and the "base_params" of base_params are the resource params of the original operator. + base_class, base_params = base_params["base_class"], base_params["base_params"] + return {resource_rep(base_class, **base_params): 1} - def _resource_fn(base_class, base_params): # pylint: disable=unused-argument - base_resources = self._base_decomposition.compute_resources(**base_params) - return { - adjoint_resource_rep(decomp_op.op_type, decomp_op.params): count - for decomp_op, count in base_resources.gate_counts.items() - } - return _resource_fn +# pylint: disable=protected-access,unused-argument +@register_resources(_cancel_adjoint_resource) +def cancel_adjoint(*params, wires, base): + """Decompose the adjoint of the adjoint of an operator.""" + _, struct = base.base._flatten() + new_struct = (wires, *struct[1:]) + base.base._unflatten(params, new_struct) -def _same_type_adjoint_resource(base_class, base_params): - """Resources of the adjoint of a gate whose adjoint is an instance of its own type.""" - # This assumes that the adjoint of the gate has the same resources as the gate itself. +def _adjoint_rotation(base_class, base_params, **__): return {resource_rep(base_class, **base_params): 1} -@register_resources(_same_type_adjoint_resource) -def same_type_adjoint_decomp(*_, base, **__): - """Decompose the adjoint of a gate whose adjoint is an instance of its own type.""" - base.adjoint() +@register_resources(_adjoint_rotation) +def adjoint_rotation(phi, wires, base, **__): + """Decompose the adjoint of a rotation operator by negating the angle.""" + _, struct = base._flatten() + new_struct = (wires, *struct[1:]) + base._unflatten((-phi,), new_struct) -def _adjoint_adjoint_resource(*_, base_params, **__): - """Resources of the adjoint of the adjoint of a gate.""" - # The base of a nested adjoint is an adjoint, so the base of the base is - # the original gate, and the "base_params" of base_params is the parameters - # of the original gate. - base_class, base_params = base_params["base_class"], base_params["base_params"] - return {resource_rep(base_class, **base_params): 1} +def is_integer(x): + """Checks if x is an integer.""" + return isinstance(x, int) or np.issubdtype(getattr(x, "dtype", None), np.integer) -@register_resources(_adjoint_adjoint_resource) -def cancel_adjoint(*params, wires, base): # pylint: disable=unused-argument - """Decompose the adjoint of the adjoint of a gate.""" - _, [_, metadata] = base.base._flatten() # pylint: disable=protected-access - new_struct = wires, metadata - base.base._unflatten(params, new_struct) # pylint: disable=protected-access +def _repeat_pow_base_resource(base_class, base_params, z): + if (not is_integer(z)) or z < 0: + raise DecompositionNotApplicable + return {resource_rep(base_class, **base_params): z} -def _adjoint_controlled_resource(base_class, base_params): - """Resources of the adjoint of a controlled gate whose base has adjoint.""" +@register_resources(_repeat_pow_base_resource) +def repeat_pow_base(*params, wires, base, z, **__): + """Decompose the power of an operator by repeating the base operator. Assumes z + is a non-negative integer.""" - num_control_wires = base_params["num_control_wires"] - controlled_base_class = base_params["base_class"] + @qml.for_loop(0, z) + def _loop(i): + _, struct = base._flatten() + new_struct = (wires, *struct[1:]) + base._unflatten(params, new_struct) - # Handle controlled-X gates, the adjoint is just themselves - if controlled_base_class is qml.X: - if num_control_wires == 1: - return {resource_rep(qml.CNOT): 1} - if num_control_wires == 2: - return {resource_rep(qml.Toffoli): 1} - return { - resource_rep( - qml.MultiControlledX, - num_control_wires=num_control_wires, - num_zero_control_values=base_params["num_zero_control_values"], - num_work_wires=base_params["num_work_wires"], - ): 1 - } + _loop() - # Handle custom controlled gates. The adjoint of a general controlled operator that - # is equivalent to a custom controlled operator should just be the custom controlled - # operator given that its base has_adjoint. - custom_op_type = base_to_custom_ctrl_op().get((controlled_base_class, num_control_wires)) - if custom_op_type is not None: - # All gates in base_to_custom_ctrl_op do not have resource params. - return {resource_rep(custom_op_type): 1} - - # Handle the general case, here we assume that the adjoint of a controlled gate - # whose base has an adjoint that is of its own type, should have the same resource - # rep as the controlled gate itself. For example, Adjoint(Controlled(O)) should - # have the same resources as Controlled(O) if the adjoint of O is another O. - return {resource_rep(base_class, **base_params): 1} +def _merge_powers_resource(base_class, base_params, z): # pylint: disable=unused-argument + return { + pow_resource_rep( + base_params["base_class"], + base_params["base_params"], + z * base_params["z"], + ): 1 + } -@register_resources(_adjoint_controlled_resource) -def adjoint_controlled_decomp(*_, base, **__): - """Decompose the adjoint of a controlled gate whose base has adjoint. - Precondition: - - isinstance(base, qml.ops.Controlled) and base.base.has_adjoint +@register_resources(_merge_powers_resource) +def merge_powers(*params, wires, base, z, **__): + """Decompose nested powers by combining them.""" + _, struct = base.base._flatten() + new_struct = (wires, *struct[1:]) + base_op = base.base._unflatten(params, new_struct) + qml.pow(base_op, z * base.z) - """ - qml.ctrl( - base.base.adjoint(), - control=base.control_wires, - control_values=base.control_values, - work_wires=base.work_wires, - ) +def _flip_pow_adjoint_resource(base_class, base_params, z): # pylint: disable=unused-argument + # base class is adjoint, and the base of the base is the target class + target_class, target_params = base_params["base_class"], base_params["base_params"] + return { + adjoint_resource_rep( + qml.ops.Pow, {"base_class": target_class, "base_params": target_params, "z": z} + ): 1 + } -def _adjoint_pow_resource(base_class, base_params): # pylint: disable=unused-argument - """Resources of the adjoint of the power of a gate whose adjoint is of the same type.""" - base, base_params, z = base_params["base_class"], base_params["base_params"], base_params["z"] - # The adjoint of the base is assumed to be of the same type as the base. - return {pow_resource_rep(base, base_params, z): 1} +@register_resources(_flip_pow_adjoint_resource) +def flip_pow_adjoint(*params, wires, base, z, **__): + """Decompose the power of an adjoint by power to the base of the adjoint and + then taking the adjoint of the power.""" + _, struct = base.base._flatten() + new_struct = (wires, *struct[1:]) + base_op = base.base._unflatten(params, new_struct) + qml.adjoint(qml.pow(base_op, z)) -@register_resources(_adjoint_pow_resource) -def adjoint_pow_decomp(*_, base, **__): - """Decompose the adjoint of the power of a gate that has its own adjoint.""" - qml.pow(base.base.adjoint(), z=base.z) +def _pow_self_adjoint_resource(base_class, base_params, z): # pylint: disable=unused-argument + if (not is_integer(z)) or z < 0: + raise DecompositionNotApplicable + return {resource_rep(base_class, **base_params): z % 2} -def _pow_resource(base_class, base_params, z): - """Resources of the power of a gate.""" - if not isinstance(z, int) or z < 0: - raise NotImplementedError("Non-integer or negative powers are not supported yet.") - return {resource_rep(base_class, **base_params): z} +@register_resources(_pow_self_adjoint_resource) +def pow_of_self_adjoint(*params, wires, base, z, **__): + """Decompose the power of a self-adjoint operator, assumes z is an integer.""" -@register_resources(_pow_resource) -def repeat_pow_base(*_, base, z, **__): - """Decompose the power of a gate.""" - assert isinstance(z, int) and z >= 0 - for _ in range(z): - base._unflatten(*base._flatten()) # pylint: disable=protected-access - - -def _pow_pow_resource(base_class, base_params, z): # pylint: disable=unused-argument - """Resources of the power of the power of a gate.""" - base_class, base_params, base_z = ( - base_params["base_class"], - base_params["base_params"], - base_params["z"], - ) - return {pow_resource_rep(base_class, base_params, z * base_z): 1} - - -@register_resources(_pow_pow_resource) -def merge_powers(*_, base, z, **__): - """Decompose the power of the power of a gate.""" - qml.pow(base.base, z=z * base.z) - - -@functools.lru_cache(maxsize=1) -def same_type_adjoint_ops(): - """A set of operators whose adjoint is an instance of its own type.""" - return frozenset( - { - # identity - qml.Identity, - qml.GlobalPhase, - # non-parametric gates - qml.H, - qml.X, - qml.Y, - qml.Z, - qml.SWAP, - qml.ECR, - # single-qubit parametric gates - qml.Rot, - qml.U1, - qml.U2, - qml.U3, - qml.RX, - qml.RY, - qml.RZ, - qml.PhaseShift, - # multi-qubit parametric gates - qml.MultiRZ, - qml.PauliRot, - qml.PCPhase, - qml.IsingXX, - qml.IsingYY, - qml.IsingZZ, - qml.IsingXY, - qml.PSWAP, - qml.CPhaseShift00, - qml.CPhaseShift01, - qml.CPhaseShift10, - # matrix gates - qml.QubitUnitary, - qml.DiagonalQubitUnitary, - qml.BlockEncode, - qml.SpecialUnitary, - # custom controlled ops - qml.CH, - qml.CY, - qml.CZ, - qml.CNOT, - qml.CSWAP, - qml.CCZ, - qml.Toffoli, - qml.MultiControlledX, - qml.CRX, - qml.CRY, - qml.CRZ, - qml.CRot, - qml.ControlledPhaseShift, - # arithmetic ops - qml.QubitSum, - qml.IntegerComparator, - # qchem ops - qml.SingleExcitation, - qml.SingleExcitationMinus, - qml.SingleExcitationPlus, - qml.DoubleExcitation, - qml.DoubleExcitationPlus, - qml.DoubleExcitationMinus, - qml.OrbitalRotation, - qml.FermionicSWAP, - # templates - qml.CommutingEvolution, - } - ) + def f(): + _, struct = base._flatten() + new_struct = (wires, *struct[1:]) + base._unflatten(params, new_struct) + + qml.cond(z % 2 == 1, f)() + + +def _pow_rotation_resource(base_class, base_params, z): # pylint: disable=unused-argument + return {resource_rep(base_class, **base_params): 1} + + +# pylint: disable=protected-access +@register_resources(_pow_rotation_resource) +def pow_rotation(phi, wires, base, z, **__): + """Decompose the power of a general rotation operator by multiplying the power by the angle.""" + _, struct = base._flatten() + new_struct = (wires, *struct[1:]) + base._unflatten((phi * z,), new_struct) + + +def _decompose_to_base_resource(base_class, base_params, **__): + return {resource_rep(base_class, **base_params): 1} + + +@register_resources(_decompose_to_base_resource) +def decompose_to_base(*params, wires, base, **__): + """Decompose a symbolic operator to its base.""" + _, struct = base._flatten() + new_struct = (wires, *struct[1:]) + base._unflatten(params, new_struct) + + +self_adjoint: DecompositionRule = decompose_to_base diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index 68388d50e09..020fc1a87bc 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -112,6 +112,10 @@ def _check_decomposition_new(op, heuristic_resources=False): for rule in qml.list_decomps(type(op)): _test_decomposition_rule(op, rule, heuristic_resources=heuristic_resources) + for rule in qml.list_decomps(f"Adjoint({type(op).__name__})"): + adj_op = qml.ops.Adjoint(op) + _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) + def _test_decomposition_rule(op, rule: DecompositionRule, heuristic_resources=False): """Tests that a decomposition rule is consistent with the operator.""" diff --git a/pennylane/ops/identity.py b/pennylane/ops/identity.py index d7bb811f573..ef96cb3ddda 100644 --- a/pennylane/ops/identity.py +++ b/pennylane/ops/identity.py @@ -21,6 +21,9 @@ from scipy import sparse import pennylane as qml +from pennylane.decomposition import add_decomps +from pennylane.decomposition.decomposition_rule import null_decomp +from pennylane.decomposition.symbolic_decomposition import adjoint_rotation, pow_rotation from pennylane.operation import ( AllWires, AnyWires, @@ -62,6 +65,12 @@ class Identity(CVObservable, Operation): ev_order = 1 + resource_keys = set() + + @property + def resource_params(self) -> dict: + return {} + @classmethod def _primitive_bind_call( cls, wires: WiresLike = (), **kwargs @@ -235,6 +244,10 @@ def pow(self, z): simulators should always be equal to 1. """ +add_decomps(Identity, null_decomp) +add_decomps("Adjoint(Identity)", null_decomp) +add_decomps("Pow(Identity)", null_decomp) + class GlobalPhase(Operation): r"""A global phase operation that multiplies all components of the state by :math:`e^{-i \phi}`. @@ -476,3 +489,7 @@ def generator(self): # needs to return a new_opmath instance regardless of whether new_opmath is enabled, because # it otherwise can't handle Identity with no wires, see PR #5194 return qml.s_prod(-1, qml.I(self.wires)) + + +add_decomps("Adjoint(GlobalPhase)", adjoint_rotation) +add_decomps("Pow(GlobalPhase)", pow_rotation) diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index 0d9eeb12ba7..6143871fb81 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -33,7 +33,6 @@ from pennylane import math as qmlmath from pennylane import operation from pennylane.compiler import compiler -from pennylane.decomposition.controlled_decomposition import base_to_custom_ctrl_op from pennylane.operation import Operator from pennylane.wires import Wires, WiresLike @@ -1002,3 +1001,28 @@ def _(base, *control_wires, control_values=None, work_wires=None, id=None): # easier to just keep the same primitive for both versions # dispatch between the two types happens inside instance creation anyway ControlledOp._primitive = Controlled._primitive # pylint: disable=protected-access + + +@functools.lru_cache(maxsize=1) +def base_to_custom_ctrl_op(): + """A dictionary mapping base op types to their custom controlled versions. + + This dictionary is used under the assumption that all custom controlled operations do not + have resource params (which is why `ControlledQubitUnitary` is not included here). + + """ + + ops_with_custom_ctrl_ops = { + (qml.PauliZ, 1): qml.CZ, + (qml.PauliZ, 2): qml.CCZ, + (qml.PauliY, 1): qml.CY, + (qml.CZ, 1): qml.CCZ, + (qml.SWAP, 1): qml.CSWAP, + (qml.Hadamard, 1): qml.CH, + (qml.RX, 1): qml.CRX, + (qml.RY, 1): qml.CRY, + (qml.RZ, 1): qml.CRZ, + (qml.Rot, 1): qml.CRot, + (qml.PhaseShift, 1): qml.ControlledPhaseShift, + } + return ops_with_custom_ctrl_ops diff --git a/pennylane/ops/op_math/controlled_ops.py b/pennylane/ops/op_math/controlled_ops.py index edc2406c44c..7a19de5b175 100644 --- a/pennylane/ops/op_math/controlled_ops.py +++ b/pennylane/ops/op_math/controlled_ops.py @@ -24,6 +24,12 @@ import pennylane as qml from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition.symbolic_decomposition import ( + adjoint_rotation, + pow_of_self_adjoint, + pow_rotation, + self_adjoint, +) from pennylane.operation import AnyWires, Wires from pennylane.ops.qubit.parametric_ops_single_qubit import stack_last from pennylane.typing import TensorLike @@ -103,7 +109,12 @@ class ControlledQubitUnitary(ControlledOp): ndim_params = (2,) """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" - resource_keys = {"base", "num_control_wires", "num_zero_control_values", "num_work_wires"} + resource_keys = { + "num_target_wires", + "num_control_wires", + "num_zero_control_values", + "num_work_wires", + } grad_method = None """Gradient computation method.""" @@ -171,7 +182,7 @@ def __init__( @property def resource_params(self) -> dict: return { - "base": self.base, + "num_target_wires": len(self.base.wires), "num_control_wires": len(self.control_wires), "num_zero_control_values": len([val for val in self.control_values if not val]), "num_work_wires": len(self.work_wires), @@ -329,6 +340,8 @@ def _ch_to_ry_cz_ry(wires: WiresLike, **__): add_decomps(CH, _ch_to_ry_cz_ry) +add_decomps("Adjoint(CH)", self_adjoint) +add_decomps("Pow(CH)", pow_of_self_adjoint) class CY(ControlledOp): @@ -462,6 +475,8 @@ def _cy(wires: WiresLike, **__): add_decomps(CY, _cy) +add_decomps("Adjoint(CY)", self_adjoint) +add_decomps("Pow(CY)", pow_of_self_adjoint) class CZ(ControlledOp): @@ -578,6 +593,8 @@ def _cz_to_cnot(wires: WiresLike, **__): add_decomps(CZ, _cz_to_cps, _cz_to_cnot) +add_decomps("Adjoint(CZ)", self_adjoint) +add_decomps("Pow(CZ)", pow_of_self_adjoint) class CSWAP(ControlledOp): @@ -728,6 +745,8 @@ def _cswap(wires: WiresLike, **__): add_decomps(CSWAP, _cswap) +add_decomps("Adjoint(CSWAP)", self_adjoint) +add_decomps("Pow(CSWAP)", pow_of_self_adjoint) class CCZ(ControlledOp): @@ -924,6 +943,8 @@ def _ccz(wires: WiresLike, **__): add_decomps(CCZ, _ccz) +add_decomps("Adjoint(CCZ)", self_adjoint) +add_decomps("Pow(CCZ)", pow_of_self_adjoint) class CNOT(ControlledOp): @@ -1054,6 +1075,8 @@ def _cnot_to_cz_h(wires: WiresLike, **__): add_decomps(CNOT, _cnot_to_cz_h) +add_decomps("Adjoint(CNOT)", self_adjoint) +add_decomps("Pow(CNOT)", pow_of_self_adjoint) class Toffoli(ControlledOp): @@ -1266,6 +1289,8 @@ def _toffoli(wires: WiresLike, **__): add_decomps(Toffoli, _toffoli) +add_decomps("Adjoint(Toffoli)", self_adjoint) +add_decomps("Pow(Toffoli)", pow_of_self_adjoint) class MultiControlledX(ControlledOp): @@ -1547,6 +1572,10 @@ def decomposition(self): ) +add_decomps("Adjoint(MultiControlledX)", self_adjoint) +add_decomps("Pow(MultiControlledX)", pow_of_self_adjoint) + + class CRX(ControlledOp): r"""The controlled-RX operator @@ -1752,6 +1781,8 @@ def _crx_to_h_crz(phi: TensorLike, wires: WiresLike, **__): add_decomps(CRX, _crx_to_rx_cz, _crx_to_rz_ry, _crx_to_h_crz) +add_decomps("Adjoint(CRX)", adjoint_rotation) +add_decomps("Pow(CRX)", pow_rotation) class CRY(ControlledOp): @@ -1929,6 +1960,8 @@ def _cry(phi: TensorLike, wires: WiresLike, **__): add_decomps(CRY, _cry) +add_decomps("Adjoint(CRY)", adjoint_rotation) +add_decomps("Pow(CRY)", pow_rotation) class CRZ(ControlledOp): @@ -2147,6 +2180,8 @@ def _crz(phi: TensorLike, wires: WiresLike, **__): add_decomps(CRZ, _crz) +add_decomps("Adjoint(CRZ)", adjoint_rotation) +add_decomps("Pow(CRZ)", pow_rotation) class CRot(ControlledOp): @@ -2365,6 +2400,14 @@ def _crot(phi: TensorLike, theta: TensorLike, omega: TensorLike, wires: WiresLik add_decomps(CRot, _crot) +@register_resources({CRot: 1}) +def _adjoint_crot(phi, theta, omega, wires, **_): + CRot(-omega, -theta, -phi, wires=wires) + + +add_decomps("Adjoint(CRot)", _adjoint_crot) + + class ControlledPhaseShift(ControlledOp): r"""A qubit controlled phase shift. @@ -2565,5 +2608,7 @@ def _cphase_to_rz_cnot(phi: TensorLike, wires: WiresLike, **__): add_decomps(ControlledPhaseShift, _cphase_to_rz_cnot) +add_decomps("Adjoint(ControlledPhaseShift)", adjoint_rotation) +add_decomps("Pow(ControlledPhaseShift)", pow_rotation) CPhase = ControlledPhaseShift diff --git a/pennylane/ops/qubit/arithmetic_ops.py b/pennylane/ops/qubit/arithmetic_ops.py index 2a474049f59..5467cff37b4 100644 --- a/pennylane/ops/qubit/arithmetic_ops.py +++ b/pennylane/ops/qubit/arithmetic_ops.py @@ -23,6 +23,7 @@ import pennylane as qml from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition.symbolic_decomposition import pow_of_self_adjoint, self_adjoint from pennylane.operation import AnyWires, FlatPytree, Operation from pennylane.ops import Identity from pennylane.typing import TensorLike @@ -357,6 +358,8 @@ def _qubitsum_to_cnots(wires: WiresLike, **__): add_decomps(QubitSum, _qubitsum_to_cnots) +add_decomps("Adjoint(QubitSum)", self_adjoint) +add_decomps("Pow(QubitSum)", pow_of_self_adjoint) class IntegerComparator(Operation): diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py index 7b4c7daf0a5..5948832849c 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -26,19 +26,10 @@ from scipy.sparse import csr_matrix import pennylane as qml +from pennylane import math from pennylane import numpy as pnp -from pennylane.decomposition.decomposition_rule import add_decomps -from pennylane.math import ( - cast, - conj, - eye, - norm, - sqrt, - sqrt_matrix, - sqrt_matrix_sparse, - transpose, - zeros, -) +from pennylane.decomposition import add_decomps, register_resources, resource_rep +from pennylane.decomposition.symbolic_decomposition import is_integer from pennylane.operation import AnyWires, DecompositionUndefinedError, FlatPytree, Operation from pennylane.ops.op_math.decompositions.unitary_decompositions import ( rot_decomp_rule, @@ -365,6 +356,39 @@ def label( ) +def _qubit_unitary_resource(base_class, base_params): + return {resource_rep(base_class, **base_params): 1} + + +@register_resources(_qubit_unitary_resource) +def _adjoint_qubit_unitary(U, wires, **_): + U = ( + U.conjugate().transpose() + if sp.sparse.issparse(U) + else qml.math.moveaxis(qml.math.conj(U), -2, -1) + ) + QubitUnitary(U, wires=wires) + + +add_decomps("Adjoint(QubitUnitary)", _adjoint_qubit_unitary) + + +def _matrix_pow(U, z): + if sp.sparse.issparse(U): + return sp.sparse.linalg.matrix_power(U, z) + if is_integer(z) and qml.math.get_deep_interface(U) != "tensorflow": + return qml.math.linalg.matrix_power(U, z) + return qml.math.convert_like(fractional_matrix_power(U, z), U) + + +@register_resources(_qubit_unitary_resource) +def _pow_qubit_unitary(U, wires, z, **_): + QubitUnitary(_matrix_pow(U, z), wires=wires) + + +add_decomps("Pow(QubitUnitary)", _pow_qubit_unitary) + + class DiagonalQubitUnitary(Operation): r"""DiagonalQubitUnitary(D, wires) Apply an arbitrary diagonal unitary matrix with a dimension that is a power of two. @@ -393,6 +417,12 @@ class DiagonalQubitUnitary(Operation): grad_method = None """Gradient computation method.""" + resource_keys = {"num_wires"} + + @property + def resource_params(self) -> dict: + return {"num_wires": len(self.wires)} + @staticmethod def compute_matrix(D: TensorLike) -> TensorLike: # pylint: disable=arguments-differ r"""Representation of the operator as a canonical matrix in the computational basis (static method). @@ -552,6 +582,27 @@ def label( return super().label(decimals=decimals, base_label=base_label or "U", cache=cache) +def _diagonal_qubit_unitary_resource(base_class, base_params): + return {resource_rep(base_class, **base_params): 1} + + +@register_resources(_diagonal_qubit_unitary_resource) +def _adjoint_diagonal_unitary(U, wires, **_): + U = qml.math.conj(U) + DiagonalQubitUnitary(U, wires=wires) + + +add_decomps("Adjoint(DiagonalQubitUnitary)", _adjoint_diagonal_unitary) + + +@register_resources(_diagonal_qubit_unitary_resource) +def _pow_diagonal_unitary(U, wires, z, **_): + DiagonalQubitUnitary(qml.math.cast(U, np.complex128) ** z, wires=wires) + + +add_decomps("Pow(DiagonalQubitUnitary)", _pow_diagonal_unitary) + + class BlockEncode(Operation): r"""BlockEncode(A, wires) Construct a unitary :math:`U(A)` such that an arbitrary matrix :math:`A` @@ -651,8 +702,8 @@ def __init__(self, A: TensorLike, wires: WiresLike, id: Optional[str] = None): shape_a = qml.math.shape(A) normalization = qml.math.maximum( - norm(A @ qml.math.transpose(qml.math.conj(A)), ord=pnp.inf), - norm(qml.math.transpose(qml.math.conj(A)) @ A, ord=pnp.inf), + math.norm(A @ qml.math.transpose(qml.math.conj(A)), ord=pnp.inf), + math.norm(qml.math.transpose(qml.math.conj(A)) @ A, ord=pnp.inf), ) subspace = (*shape_a, 2 ** len(wires)) @@ -755,7 +806,7 @@ def _process_blockencode(A, subspace): n, m, k = subspace shape_a = qml.math.shape(A) - sqrtm = sqrt_matrix_sparse if sp.sparse.issparse(A) else sqrt_matrix + sqrtm = math.sqrt_matrix_sparse if sp.sparse.issparse(A) else math.sqrt_matrix def _stack(lst, h=False, like=None): if like == "tensorflow": @@ -766,19 +817,24 @@ def _stack(lst, h=False, like=None): interface = qml.math.get_interface(A) if qml.math.sum(shape_a) <= 2: - col1 = _stack([A, sqrt(1 - A * conj(A))], like=interface) - col2 = _stack([sqrt(1 - A * conj(A)), -conj(A)], like=interface) + col1 = _stack([A, math.sqrt(1 - A * math.conj(A))], like=interface) + col2 = _stack([math.sqrt(1 - A * math.conj(A)), -math.conj(A)], like=interface) u = _stack([col1, col2], h=True, like=interface) else: d1, d2 = shape_a col1 = _stack( - [A, sqrtm(cast(eye(d2, like=A), A.dtype) - qml.math.transpose(conj(A)) @ A)], + [ + A, + sqrtm( + math.cast(math.eye(d2, like=A), A.dtype) - qml.math.transpose(math.conj(A)) @ A + ), + ], like=interface, ) col2 = _stack( [ - sqrtm(cast(eye(d1, like=A), A.dtype) - A @ transpose(conj(A))), - -transpose(conj(A)), + sqrtm(math.cast(math.eye(d1, like=A), A.dtype) - A @ math.transpose(math.conj(A))), + -math.transpose(math.conj(A)), ], like=interface, ) @@ -786,8 +842,8 @@ def _stack(lst, h=False, like=None): if n + m < k: r = k - (n + m) - col1 = _stack([u, zeros((r, n + m), like=A)], like=interface) - col2 = _stack([zeros((n + m, r), like=A), eye(r, like=A)], like=interface) + col1 = _stack([u, math.zeros((r, n + m), like=A)], like=interface) + col2 = _stack([math.zeros((n + m, r), like=A), math.eye(r, like=A)], like=interface) u = _stack([col1, col2], h=True, like=interface) return u diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 9dbbe03513a..3331e08f5a2 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -25,7 +25,15 @@ from scipy import sparse import pennylane as qml -from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition import ( + DecompositionNotApplicable, + add_decomps, + adjoint_resource_rep, + pow_resource_rep, + register_resources, + resource_rep, +) +from pennylane.decomposition.symbolic_decomposition import pow_of_self_adjoint, self_adjoint from pennylane.operation import Observable, Operation from pennylane.typing import TensorLike from pennylane.wires import Wires, WiresLike @@ -230,6 +238,9 @@ def _hadamard_to_rz_ry(wires: WiresLike, **__): add_decomps(Hadamard, _hadamard_to_rz_rx, _hadamard_to_rz_ry) +add_decomps("Adjoint(Hadamard)", self_adjoint) +add_decomps("Pow(Hadamard)", pow_of_self_adjoint) + H = Hadamard r"""H(wires) @@ -449,17 +460,19 @@ def single_qubit_rot_angles(self) -> list[TensorLike]: """ -def _paulix_to_rx_gp_resources(): +def _paulix_to_rx_resources(): return {qml.GlobalPhase: 1, qml.RX: 1} -@register_resources(_paulix_to_rx_gp_resources) -def _paulix_to_rx_gp(wires: WiresLike, **__): +@register_resources(_paulix_to_rx_resources) +def _paulix_to_rx(wires: WiresLike, **__): qml.RX(np.pi, wires=wires) qml.GlobalPhase(-np.pi / 2, wires=wires) -add_decomps(PauliX, _paulix_to_rx_gp) +add_decomps(PauliX, _paulix_to_rx) +add_decomps("Adjoint(PauliX)", self_adjoint) +add_decomps("Pow(PauliX)", pow_of_self_adjoint) class PauliY(Observable, Operation): @@ -672,6 +685,8 @@ def _pauliy_to_ry_gp(wires: WiresLike, **__): add_decomps(PauliY, _pauliy_to_ry_gp) +add_decomps("Adjoint(PauliY)", self_adjoint) +add_decomps("Pow(PauliY)", pow_of_self_adjoint) class PauliZ(Observable, Operation): @@ -892,6 +907,8 @@ def _pauliz_to_ps(wires: WiresLike, **__): add_decomps(PauliZ, _pauliz_to_ps) +add_decomps("Adjoint(PauliZ)", self_adjoint) +add_decomps("Pow(PauliZ)", pow_of_self_adjoint) class S(Operation): @@ -1042,6 +1059,33 @@ def _s_phaseshift(wires, **__): add_decomps(S, _s_phaseshift) +def _pow_s_resource(base_class, base_params, z): # pylint: disable=unused_argument + z_mod4 = z % 4 + if z_mod4 == 0.5: + return {T: 1} + if z_mod4 == 2: + return {Z: 1} + if z != z_mod4: + return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} + raise DecompositionNotApplicable + + +@register_resources(_pow_s_resource) +def _pow_s(wires, z, **__): + z_mod4 = z % 4 + qml.cond( + z_mod4 == 0.5, + lambda: T(wires=wires), + lambda: qml.pow(S(wires), z_mod4), + elifs=[ + (z_mod4 == 2, lambda: Z(wires=wires)), + ], + )() + + +add_decomps("Pow(S)", _pow_s) + + class T(Operation): r"""T(wires) The single-qubit T gate @@ -1190,6 +1234,33 @@ def _t_phaseshift(wires, **__): add_decomps(T, _t_phaseshift) +def _pow_t_resource(base_class, base_params, z): # pylint: disable=unused-argument + z_mod8 = z % 8 + if z_mod8 == 2: + return {S: 1} + if z_mod8 == 4: + return {Z: 1} + if z != z_mod8: + return {pow_resource_rep(base_class, base_params, z=z_mod8): 1} + raise DecompositionNotApplicable + + +@register_resources(_pow_t_resource) +def _pow_t(wires, z, **__): + z_mod8 = z % 8 + qml.cond( + z_mod8 == 2, + lambda: S(wires=wires), + lambda: qml.pow(T(wires), z_mod8), + elifs=[ + (z_mod8 == 4, lambda: Z(wires=wires)), + ], + )() + + +add_decomps("Pow(T)", _pow_t) + + class SX(Operation): r"""SX(wires) The single-qubit Square-Root X operator. @@ -1327,19 +1398,39 @@ def single_qubit_rot_angles(self) -> list[TensorLike]: return [np.pi / 2, np.pi / 2, -np.pi / 2] -def _sx_to_rz_ry_rz_ps_resources(): - return {qml.PhaseShift: 1, qml.RZ: 2, qml.RY: 1} +def _sx_to_rx_resources(): + return {qml.RX: 1, qml.GlobalPhase: 1} -@register_resources(_sx_to_rz_ry_rz_ps_resources) -def _sx_to_rz_ry_rz_ps(wires: WiresLike, **__): - qml.RZ(np.pi / 2, wires=wires) - qml.RY(np.pi / 2, wires=wires) - qml.RZ(-np.pi, wires=wires) - qml.PhaseShift(np.pi / 2, wires=wires) +@register_resources(_sx_to_rx_resources) +def _sx_to_rx(wires: WiresLike, **__): + qml.RX(np.pi / 2, wires=wires) + qml.GlobalPhase(-np.pi / 4, wires=wires) + +add_decomps(SX, _sx_to_rx) -add_decomps(SX, _sx_to_rz_ry_rz_ps) + +def _pow_sx_to_x_resource(base_class, base_params, z): # pylint: disable=unused-argument + z_mod4 = z % 4 + if z_mod4 == 2: + return {X: 1} + if z != z_mod4: + return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} + raise DecompositionNotApplicable + + +@register_resources(_pow_sx_to_x_resource) +def _pow_sx_to_x(*params, wires, z, base, **__): # pylint: disable=unused-argument + z_mod4 = z % 4 + qml.cond( + z_mod4 == 2, + lambda: X(wires=wires), + lambda: qml.pow(SX(wires), z_mod4), + )() + + +add_decomps("Pow(SX)", _pow_sx_to_x) class SWAP(Operation): @@ -1494,6 +1585,8 @@ def _swap_to_cnot(wires, **__): add_decomps(SWAP, _swap_to_cnot) +add_decomps("Adjoint(SWAP)", self_adjoint) +add_decomps("Pow(SWAP)", pow_of_self_adjoint) class ECR(Operation): @@ -1657,6 +1750,8 @@ def _ecr_decomp(wires, **__): add_decomps(ECR, _ecr_decomp) +add_decomps("Adjoint(ECR)", self_adjoint) +add_decomps("Pow(ECR)", pow_of_self_adjoint) class ISWAP(Operation): @@ -1811,6 +1906,28 @@ def _iswap_decomp(wires, **__): add_decomps(ISWAP, _iswap_decomp) +def _pow_iswap_to_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument + z_mod2 = z % 2 + if z_mod2 == 0.5: + return {SISWAP: 1} + if z != z_mod2: + return {pow_resource_rep(base_class, base_params, z=z_mod2): 1} + raise DecompositionNotApplicable + + +@register_resources(_pow_iswap_to_siswap_resource) +def _pow_iswap_to_siswap(wires, z, **__): + z_mod2 = z % 2 + qml.cond( + z_mod2 == 0.5, + lambda: SISWAP(wires=wires), + lambda: qml.pow(ISWAP(wires=wires), z_mod2), + )() + + +add_decomps("Pow(ISWAP)", _pow_iswap_to_siswap) + + class SISWAP(Operation): r"""SISWAP(wires) The square root of i-swap operator. Can also be accessed as ``qml.SQISW`` @@ -1988,4 +2105,27 @@ def _siswap_decomp(wires, **__): add_decomps(SISWAP, _siswap_decomp) + +def _pow_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument + z_mod4 = z % 4 + if qml.math.allclose(z_mod4, 2): + return {ISWAP: 1} + if z != z_mod4: + return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} + raise DecompositionNotApplicable + + +@register_resources(_pow_siswap_resource) +def _pow_siswap(wires, z, **__): + z_mod4 = z % 4 + qml.cond( + qml.math.allclose(z_mod4, 2), + lambda: ISWAP(wires=wires), + lambda: qml.pow(SISWAP(wires=wires), z_mod4), + ) + + +add_decomps("Pow(SISWAP)", _pow_siswap) + + SQISW = SISWAP diff --git a/pennylane/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/ops/qubit/parametric_ops_multi_qubit.py index a7b06ce9a2f..b79beb88c6b 100644 --- a/pennylane/ops/qubit/parametric_ops_multi_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_multi_qubit.py @@ -25,6 +25,7 @@ import pennylane as qml from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition.symbolic_decomposition import adjoint_rotation, pow_rotation from pennylane.math import expand_matrix from pennylane.operation import AnyWires, FlatPytree, Operation from pennylane.typing import TensorLike @@ -240,6 +241,8 @@ def _post_cnot(i): add_decomps(MultiRZ, _multi_rz_decomposition) +add_decomps("Adjoint(MultiRZ)", adjoint_rotation) +add_decomps("Pow(MultiRZ)", pow_rotation) class PauliRot(Operation): @@ -569,6 +572,18 @@ def pow(self, z): return [PauliRot(self.data[0] * z, self.hyperparameters["pauli_word"], wires=self.wires)] +def _pauli_rot_resources(pauli_word): + if set(pauli_word) == {"I"}: + return {qml.GlobalPhase: 1} + num_active_wires = len(pauli_word.replace("I", "")) + return { + qml.Hadamard: 2 * pauli_word.count("X"), + qml.RX: 2 * pauli_word.count("Y"), + qml.resource_rep(qml.MultiRZ, num_wires=num_active_wires): 1, + } + + +@register_resources(_pauli_rot_resources) def _pauli_rot_decomposition(theta, pauli_word, wires, **__): if set(pauli_word) == {"I"}: qml.GlobalPhase(theta / 2) @@ -589,19 +604,9 @@ def _pauli_rot_decomposition(theta, pauli_word, wires, **__): qml.RX(-np.pi / 2, wires=[wire]) -def _pauli_rot_resources(pauli_word): - if set(pauli_word) == {"I"}: - return {qml.GlobalPhase: 1} - num_active_wires = len(pauli_word.replace("I", "")) - return { - qml.Hadamard: 2 * pauli_word.count("X"), - qml.RX: 2 * pauli_word.count("Y"), - qml.resource_rep(qml.MultiRZ, num_wires=num_active_wires): 1, - } - - -pauli_rot_decomposition = qml.register_resources(_pauli_rot_resources, _pauli_rot_decomposition) -add_decomps(PauliRot, pauli_rot_decomposition) +add_decomps(PauliRot, _pauli_rot_decomposition) +add_decomps("Adjoint(PauliRot)", adjoint_rotation) +add_decomps("Pow(PauliRot)", pow_rotation) class PCPhase(Operation): @@ -984,6 +989,8 @@ def _isingxx_to_cnot_rx_cnot(phi: TensorLike, wires: WiresLike, **__): add_decomps(IsingXX, _isingxx_to_cnot_rx_cnot) +add_decomps("Adjoint(IsingXX)", adjoint_rotation) +add_decomps("Pow(IsingXX)", pow_rotation) class IsingYY(Operation): @@ -1146,6 +1153,8 @@ def _isingyy_to_cy_ry_cy(phi: TensorLike, wires: WiresLike, **__): add_decomps(IsingYY, _isingyy_to_cy_ry_cy) +add_decomps("Adjoint(IsingYY)", adjoint_rotation) +add_decomps("Pow(IsingYY)", pow_rotation) class IsingZZ(Operation): @@ -1339,6 +1348,8 @@ def _isingzz_to_cnot_rz_cnot(phi: TensorLike, wires: WiresLike, **__): add_decomps(IsingZZ, _isingzz_to_cnot_rz_cnot) +add_decomps("Adjoint(IsingZZ)", adjoint_rotation) +add_decomps("Pow(IsingZZ)", pow_rotation) class IsingXY(Operation): @@ -1563,6 +1574,8 @@ def _isingxy_to_h_cy(phi: TensorLike, wires: WiresLike, **__): add_decomps(IsingXY, _isingxy_to_h_cy) +add_decomps("Adjoint(IsingXY)", adjoint_rotation) +add_decomps("Pow(IsingXY)", pow_rotation) class PSWAP(Operation): @@ -1739,6 +1752,8 @@ def _pswap_to_swap_cnot_phaseshift_cnot(phi: TensorLike, wires: WiresLike, **__) add_decomps(PSWAP, _pswap_to_swap_cnot_phaseshift_cnot) +add_decomps("Adjoint(PSWAP)", adjoint_rotation) +add_decomps("Pow(PSWAP)", pow_rotation) class CPhaseShift00(Operation): @@ -1957,6 +1972,8 @@ def _cphaseshift00(phi: TensorLike, wires: WiresLike, **__): add_decomps(CPhaseShift00, _cphaseshift00) +add_decomps("Adjoint(CPhaseShift00)", adjoint_rotation) +add_decomps("Pow(CPhaseShift00)", pow_rotation) class CPhaseShift01(Operation): @@ -2166,6 +2183,8 @@ def _cphaseshift01(phi: TensorLike, wires: WiresLike, **__): add_decomps(CPhaseShift01, _cphaseshift01) +add_decomps("Adjoint(CPhaseShift01)", adjoint_rotation) +add_decomps("Pow(CPhaseShift01)", pow_rotation) class CPhaseShift10(Operation): @@ -2369,3 +2388,5 @@ def _cphaseshift10(phi: TensorLike, wires: WiresLike, **__): add_decomps(CPhaseShift10, _cphaseshift10) +add_decomps("Adjoint(CPhaseShift10)", adjoint_rotation) +add_decomps("Pow(CPhaseShift10)", pow_rotation) diff --git a/pennylane/ops/qubit/parametric_ops_single_qubit.py b/pennylane/ops/qubit/parametric_ops_single_qubit.py index 6f636967df8..f194fd7ca92 100644 --- a/pennylane/ops/qubit/parametric_ops_single_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_single_qubit.py @@ -24,7 +24,13 @@ import scipy as sp import pennylane as qml -from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition import ( + DecompositionNotApplicable, + add_decomps, + register_resources, + resource_rep, +) +from pennylane.decomposition.symbolic_decomposition import adjoint_rotation, pow_rotation from pennylane.operation import Operation from pennylane.typing import TensorLike from pennylane.wires import WiresLike @@ -175,6 +181,8 @@ def _rx_to_rz_ry(phi, wires: WiresLike, **__): add_decomps(RX, _rx_to_rot, _rx_to_rz_ry) +add_decomps("Adjoint(RX)", adjoint_rotation) +add_decomps("Pow(RX)", pow_rotation) class RY(Operation): @@ -307,6 +315,8 @@ def _ry_to_rz_rx(phi, wires: WiresLike, **__): add_decomps(RY, _ry_to_rot, _ry_to_rz_rx) +add_decomps("Adjoint(RY)", adjoint_rotation) +add_decomps("Pow(RY)", pow_rotation) class RZ(Operation): @@ -478,6 +488,8 @@ def _rz_to_ry_rx(phi, wires: WiresLike, **__): add_decomps(RZ, _rz_to_rot, _rz_to_ry_rx) +add_decomps("Adjoint(RZ)", adjoint_rotation) +add_decomps("Pow(RZ)", pow_rotation) class PhaseShift(Operation): @@ -667,6 +679,8 @@ def _phaseshift_to_rz_gp(phi, wires: WiresLike, **__): add_decomps(PhaseShift, _phaseshift_to_rz_gp) +add_decomps("Adjoint(PhaseShift)", adjoint_rotation) +add_decomps("Pow(PhaseShift)", pow_rotation) class Rot(Operation): @@ -874,6 +888,14 @@ def _rot_to_rz_ry_rz(phi, theta, omega, wires: WiresLike, **__): add_decomps(Rot, _rot_to_rz_ry_rz) +@register_resources({Rot: 1}) +def _adjoint_rot(phi, theta, omega, wires, **__): + Rot(-omega, -theta, -phi, wires=wires) + + +add_decomps("Adjoint(Rot)", _adjoint_rot) + + class U1(Operation): r""" U1 gate. @@ -1008,6 +1030,8 @@ def _u1_phaseshift(phi, wires, **__): add_decomps(U1, _u1_phaseshift) +add_decomps("Adjoint(U1)", adjoint_rotation) +add_decomps("Pow(U1)", pow_rotation) class U2(Operation): @@ -1177,6 +1201,16 @@ def _u2_phaseshift_rot(phi, delta, wires, **__): add_decomps(U2, _u2_phaseshift_rot) +@register_resources({U2: 1}) +def _adjoint_u2(phi, delta, wires, **__): + new_delta = qml.math.mod((np.pi - phi), (2 * np.pi)) + new_phi = qml.math.mod((np.pi - delta), (2 * np.pi)) + U2(new_phi, new_delta, wires=wires) + + +add_decomps("Adjoint(U2)", _adjoint_u2) + + class U3(Operation): r""" Arbitrary single qubit unitary. @@ -1377,3 +1411,13 @@ def _u3_phaseshift_rot(theta, phi, delta, wires, **__): add_decomps(U3, _u3_phaseshift_rot) + + +@register_resources({U3: 1}) +def _adjoint_u3(theta, phi, delta, wires, **__): + new_delta = qml.math.mod((np.pi - phi), (2 * np.pi)) + new_phi = qml.math.mod((np.pi - delta), (2 * np.pi)) + U3(theta, new_phi, new_delta, wires=wires) + + +add_decomps("Adjoint(U3)", _adjoint_u3) diff --git a/pennylane/ops/qubit/qchem_ops.py b/pennylane/ops/qubit/qchem_ops.py index b06703b7cd4..3789c6030b6 100644 --- a/pennylane/ops/qubit/qchem_ops.py +++ b/pennylane/ops/qubit/qchem_ops.py @@ -24,6 +24,7 @@ import pennylane as qml from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition.symbolic_decomposition import adjoint_rotation, pow_rotation from pennylane.operation import Operation from pennylane.typing import TensorLike from pennylane.wires import WiresLike @@ -322,6 +323,8 @@ def _single_excit(phi, wires, **__): add_decomps(SingleExcitation, _single_excit) +add_decomps("Adjoint(SingleExcitation)", adjoint_rotation) +add_decomps("Pow(SingleExcitation)", pow_rotation) class SingleExcitationMinus(Operation): @@ -481,6 +484,8 @@ def _single_excitation_minus_decomp(phi, wires: WiresLike, **__): add_decomps(SingleExcitationMinus, _single_excitation_minus_decomp) +add_decomps("Adjoint(SingleExcitationMinus)", adjoint_rotation) +add_decomps("Pow(SingleExcitationMinus)", pow_rotation) class SingleExcitationPlus(Operation): @@ -640,6 +645,8 @@ def _single_excitation_plus_decomp(phi, wires: WiresLike, **__): add_decomps(SingleExcitationPlus, _single_excitation_plus_decomp) +add_decomps("Adjoint(SingleExcitationPlus)", adjoint_rotation) +add_decomps("Pow(SingleExcitationPlus)", pow_rotation) class DoubleExcitation(Operation): @@ -893,6 +900,8 @@ def _doublexcit(phi, wires, **__): add_decomps(DoubleExcitation, _doublexcit) +add_decomps("Adjoint(DoubleExcitation)", adjoint_rotation) +add_decomps("Pow(DoubleExcitation)", pow_rotation) class DoubleExcitationPlus(Operation): @@ -988,6 +997,10 @@ def label( return super().label(decimals=decimals, base_label=base_label or "G²₊", cache=cache) +add_decomps("Adjoint(DoubleExcitationPlus)", adjoint_rotation) +add_decomps("Pow(DoubleExcitationPlus)", pow_rotation) + + class DoubleExcitationMinus(Operation): r""" Double excitation rotation with negative phase-shift outside the rotation subspace. @@ -1079,6 +1092,10 @@ def label( return super().label(decimals=decimals, base_label=base_label or "G²₋", cache=cache) +add_decomps("Adjoint(DoubleExcitationMinus)", adjoint_rotation) +add_decomps("Pow(DoubleExcitationMinus)", pow_rotation) + + class OrbitalRotation(Operation): r""" Spin-adapted spatial orbital rotation. @@ -1291,6 +1308,8 @@ def _orbital_rotation_decomp(phi, wires: WiresLike, **__): add_decomps(OrbitalRotation, _orbital_rotation_decomp) +add_decomps("Adjoint(OrbitalRotation)", adjoint_rotation) +add_decomps("Pow(OrbitalRotation)", pow_rotation) class FermionicSWAP(Operation): @@ -1524,3 +1543,5 @@ def _fermionic_swap_decomp(phi, wires: WiresLike, **__): add_decomps(FermionicSWAP, _fermionic_swap_decomp) +add_decomps("Adjoint(FermionicSWAP)", adjoint_rotation) +add_decomps("Pow(FermionicSWAP)", pow_rotation) diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py index f57a0267eaf..ad586013bdb 100644 --- a/tests/decomposition/conftest.py +++ b/tests/decomposition/conftest.py @@ -21,6 +21,12 @@ import pennylane as qml from pennylane.decomposition import Resources from pennylane.decomposition.decomposition_rule import _auto_wrap +from pennylane.decomposition.symbolic_decomposition import ( + adjoint_rotation, + pow_of_self_adjoint, + pow_rotation, + self_adjoint, +) decompositions = defaultdict(list) @@ -126,22 +132,12 @@ def _t_ps(wires, **__): decompositions["T"] = [_t_ps] +################################################ +# Custom Decompositions For Symbolic Operators # +################################################ -@qml.register_resources({qml.H: 1}) -def _adjoint_hadamard(*_, wires, **__): - qml.H(wires) - - -decompositions["Adjoint(Hadamard)"] = [_adjoint_hadamard] - - -def _pow_hadamard_resource(z, **__): - return {qml.H: z % 2} - - -@qml.register_resources(_pow_hadamard_resource) -def _pow_hadamard(*_, wires, z, **__): - qml.cond(z % 2 == 1, qml.H)(wires=wires) - - -decompositions["Pow(Hadamard)"] = [_pow_hadamard] +decompositions["Adjoint(Hadamard)"] = [self_adjoint] +decompositions["Pow(Hadamard)"] = [pow_of_self_adjoint] +decompositions["Adjoint(RX)"] = [adjoint_rotation] +decompositions["Adjoint(CNOT)"] = [self_adjoint] +decompositions["Adjoint(PhaseShift)"] = [adjoint_rotation] diff --git a/tests/decomposition/test_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py index 956c17645ca..289be3a6cbe 100644 --- a/tests/decomposition/test_decomposition_graph.py +++ b/tests/decomposition/test_decomposition_graph.py @@ -449,7 +449,7 @@ def custom_controlled_decomp(wires): class TestSymbolicDecompositions: """Tests decompositions of symbolic ops.""" - def test_adjoint_adjoint(self, _): + def test_cancel_adjoint(self, _): """Tests that a nested adjoint operator is flattened properly.""" op = qml.adjoint(qml.adjoint(qml.RX(0.5, wires=[0]))) @@ -467,26 +467,6 @@ def test_adjoint_adjoint(self, _): assert q.queue == [qml.RX(0.5, wires=[0])] assert graph.resource_estimate(op) == to_resources({qml.RX: 1}) - def test_adjoint_pow(self, _): - """tests that an adjoint of a power of am operator that has adjoint is decomposed.""" - - op = qml.adjoint(qml.pow(qml.H(0), z=3)) - - graph = DecompositionGraph(operations=[op], gate_set={"H"}) - # 3 operator nodes: Adjoint(Pow(H)), Pow(H), and H, and 1 dummy starting node - # 1 decomposition nodes for Adjoint(Pow(H)) and two decomposition nodes for Pow(H) - assert len(graph._graph.nodes()) == 7 - # 3 edges from decompositions to ops and 3 edges from ops to decompositions - # and 1 edge from the dummy starting node to the target gate set. - assert len(graph._graph.edges()) == 7 - - graph.solve() - with qml.queuing.AnnotatedQueue() as q: - graph.decomposition(op)(*op.parameters, wires=op.wires, **op.hyperparameters) - - assert q.queue == [qml.pow(qml.H(0), z=3)] - assert graph.resource_estimate(op) == to_resources({qml.H: 1}) - def test_adjoint_custom(self, _): """Tests adjoint of an operator that defines its own adjoint.""" @@ -504,35 +484,6 @@ def test_adjoint_custom(self, _): assert q.queue == [qml.RX(-0.5, wires=[0])] assert graph.resource_estimate(op) == to_resources({qml.RX: 1}) - def test_adjoint_controlled(self, _): - """Tests that the adjoint of a controlled operator is decomposed properly.""" - - op = qml.adjoint(qml.ops.Controlled(qml.RX(0.5, wires=[0]), control_wires=1)) - op2 = qml.adjoint(qml.ctrl(qml.U1(0.5, wires=0), control=[1])) - - graph = DecompositionGraph(operations=[op, op2], gate_set={"ControlledPhaseShift", "CRX"}) - # 5 operator nodes: Adjoint(C(RX)), Adjoint(C(U1)), CRX, C(U1), ControlledPhaseShift - # 3 decomposition nodes leading into Adjoint(C(RX)), Adjoint(C(U1)), and C(U1) - # and the dummy starting node - assert len(graph._graph.nodes()) == 9 - # 3 edges from decompositions to ops and 3 edges from ops to decompositions, - # and 2 edges from the dummy starting node to the target gate set. - assert len(graph._graph.edges()) == 8 - - graph.solve() - with qml.queuing.AnnotatedQueue() as q: - graph.decomposition(op)(*op.parameters, wires=op.wires, **op.hyperparameters) - graph.decomposition(op2)(*op2.parameters, wires=op2.wires, **op2.hyperparameters) - - assert q.queue == [qml.CRX(-0.5, wires=[1, 0]), qml.ctrl(qml.U1(-0.5, wires=0), control=1)] - - op3 = qml.ctrl(qml.U1(-0.5, wires=0), control=1) - with qml.queuing.AnnotatedQueue() as q: - graph.decomposition(op3)(*op3.parameters, wires=op3.wires, **op3.hyperparameters) - - assert q.queue == [qml.ControlledPhaseShift(-0.5, wires=[1, 0])] - assert graph.resource_estimate(op2) == to_resources({qml.ControlledPhaseShift: 1}) - def test_adjoint_general(self, _): """Tests decomposition of a generalized adjoint operation.""" @@ -560,13 +511,12 @@ def custom_decomp(phi, wires): alt_decomps={CustomOp: [custom_decomp]}, ) # 10 operator nodes: A(CustomOp), A(H), A(CNOT), A(RX), A(T), H, CNOT, RX, A(PhaseShift), PhaseShift - # 5 decomposition nodes for: A(CustomOp), A(CNOT), A(RX), A(T), A(PhaseShift) - # 2 decomposition nodes for A(H) + # 6 decomposition nodes for: A(CustomOp), A(CNOT), A(RX), A(T), A(PhaseShift), A(H) # 1 dummy starting node - assert len(graph._graph.nodes()) == 18 - # 10 edges from ops to decompositions and 7 edges from decompositions to ops. + assert len(graph._graph.nodes()) == 17 + # 9 edges from ops to decompositions and 6 edges from decompositions to ops. # and 4 edges from the dummy starting node to the target gate set. - assert len(graph._graph.edges()) == 21 + assert len(graph._graph.edges()) == 19 graph.solve() with qml.queuing.AnnotatedQueue() as q: @@ -589,12 +539,13 @@ def test_nested_powers(self, _): op = qml.pow(qml.pow(qml.H(0), 3), 2) graph = DecompositionGraph(operations=[op], gate_set={"H"}) # 3 operator nodes: Pow(Pow(H)), Pow(H), and H - # 1 decomposition nodes for Pow(Pow(H)) and 2 nodes for Pow(H) + # 1 decomposition nodes for Pow(Pow(H)) and 1 nodes for Pow(H) # and the dummy starting node - assert len(graph._graph.nodes()) == 7 - # 3 edges from decompositions to ops and 3 edges from ops to decompositions - # and 1 edge from the dummy starting node to the target gate set. - assert len(graph._graph.edges()) == 7 + assert len(graph._graph.nodes()) == 5 + # 2 edges from decompositions to ops and 1 edge from ops to decompositions + # and 1 edge from the dummy starting node to the target gate set. Note that + # H**6 decomposes to nothing, so H isn't counted. + assert len(graph._graph.edges()) == 4 graph.solve() with qml.queuing.AnnotatedQueue() as q: diff --git a/tests/decomposition/test_resources.py b/tests/decomposition/test_resources.py index 7e9171a2f71..72ed3d77736 100644 --- a/tests/decomposition/test_resources.py +++ b/tests/decomposition/test_resources.py @@ -375,10 +375,10 @@ def test_nested_controlled_qubit_unitary(self): { "base_class": qml.ControlledQubitUnitary, "base_params": { + "num_target_wires": 1, "num_control_wires": 2, "num_zero_control_values": 1, "num_work_wires": 1, - "base": qml.QubitUnitary(U, wires=[0]), }, "num_control_wires": 1, "num_zero_control_values": 1, diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index b704fb0a6f1..01795492f58 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -17,23 +17,33 @@ import pytest import pennylane as qml -from pennylane.decomposition.resources import Resources, pow_resource_rep +from pennylane import queuing +from pennylane.decomposition import DecompositionNotApplicable +from pennylane.decomposition.resources import ( + Resources, + adjoint_resource_rep, + pow_resource_rep, + resource_rep, +) from pennylane.decomposition.symbolic_decomposition import ( - AdjointDecomp, - adjoint_controlled_decomp, - adjoint_pow_decomp, + adjoint_rotation, cancel_adjoint, + flip_pow_adjoint, + make_adjoint_decomp, merge_powers, + pow_of_self_adjoint, + pow_rotation, repeat_pow_base, - same_type_adjoint_decomp, + self_adjoint, ) from tests.decomposition.conftest import to_resources +@pytest.mark.unit class TestAdjointDecompositionRules: """Tests the decomposition rules defined for the adjoint of operations.""" - def test_adjoint_adjoint(self): + def test_cancel_adjoint(self): """Tests that the adjoint of an adjoint cancels out.""" op = qml.adjoint(qml.adjoint(qml.RX(0.5, wires=0))) @@ -45,7 +55,7 @@ def test_adjoint_adjoint(self): assert cancel_adjoint.compute_resources(**op.resource_params) == to_resources({qml.RX: 1}) @pytest.mark.jax - def test_adjoint_adjoint_capture(self): + def test_cancel_adjoint_capture(self): """Tests that the adjoint of an adjoint works with capture.""" from pennylane.tape.plxpr_conversion import CollectOpsandMeas @@ -66,82 +76,6 @@ def circuit(): if not capture_enabled: qml.capture.disable() - def test_adjoint_controlled(self): - """Tests that the adjoint of a controlled operation is correctly decomposed.""" - - op1 = qml.adjoint(qml.ctrl(qml.U1(0.5, wires=0), control=1)) - op2 = qml.adjoint(qml.ops.Controlled(qml.RX(0.5, wires=0), control_wires=[1])) - - with qml.queuing.AnnotatedQueue() as q: - adjoint_controlled_decomp(*op1.parameters, wires=op1.wires, **op1.hyperparameters) - adjoint_controlled_decomp(*op2.parameters, wires=op2.wires, **op2.hyperparameters) - - assert q.queue == [qml.ctrl(qml.U1(-0.5, wires=0), control=1), qml.CRX(-0.5, wires=[1, 0])] - assert adjoint_controlled_decomp.compute_resources(**op1.resource_params) == Resources( - { - qml.decomposition.controlled_resource_rep( - qml.U1, {}, num_control_wires=1, num_zero_control_values=0, num_work_wires=0 - ): 1, - } - ) - assert adjoint_controlled_decomp.compute_resources(**op2.resource_params) == Resources( - {qml.resource_rep(qml.CRX): 1} - ) - - def test_adjoint_controlled_x(self): - """Tests the adjoint of controlled X operations are correctly decomposed.""" - - op1 = qml.adjoint(qml.ops.Controlled(qml.X(1), control_wires=[0])) - op2 = qml.adjoint(qml.ops.Controlled(qml.X(2), control_wires=[0, 1])) - op3 = qml.adjoint(qml.ops.Controlled(qml.X(3), control_wires=[0, 1, 2])) - - with qml.queuing.AnnotatedQueue() as q: - adjoint_controlled_decomp(*op1.parameters, wires=op1.wires, **op1.hyperparameters) - adjoint_controlled_decomp(*op2.parameters, wires=op2.wires, **op2.hyperparameters) - adjoint_controlled_decomp(*op3.parameters, wires=op3.wires, **op3.hyperparameters) - - assert q.queue == [ - qml.CNOT(wires=[0, 1]), - qml.Toffoli(wires=[0, 1, 2]), - qml.MultiControlledX(wires=[0, 1, 2, 3]), - ] - - assert adjoint_controlled_decomp.compute_resources(**op1.resource_params) == Resources( - {qml.resource_rep(qml.CNOT): 1} - ) - assert adjoint_controlled_decomp.compute_resources(**op2.resource_params) == Resources( - {qml.resource_rep(qml.Toffoli): 1} - ) - assert adjoint_controlled_decomp.compute_resources(**op3.resource_params) == Resources( - { - qml.resource_rep( - qml.MultiControlledX, - num_control_wires=3, - num_zero_control_values=0, - num_work_wires=0, - ): 1 - } - ) - - def test_same_type_adjoint(self): - """Tests that the adjoint of an operation that has adjoint of same - type is correctly decomposed.""" - - op1 = qml.adjoint(qml.H(0)) - op2 = qml.adjoint(qml.RX(0.5, wires=0)) - - with qml.queuing.AnnotatedQueue() as q: - same_type_adjoint_decomp(*op1.parameters, wires=op1.wires, **op1.hyperparameters) - same_type_adjoint_decomp(*op2.parameters, wires=op2.wires, **op2.hyperparameters) - - assert q.queue == [qml.H(0), qml.RX(-0.5, wires=0)] - assert same_type_adjoint_decomp.compute_resources(**op1.resource_params) == to_resources( - {qml.H: 1} - ) - assert same_type_adjoint_decomp.compute_resources(**op2.resource_params) == to_resources( - {qml.RX: 1} - ) - def test_adjoint_general(self): """Tests the adjoint of a general operator can be correctly decomposed.""" @@ -162,7 +96,7 @@ def custom_decomp(phi, wires): qml.T(wires[2]) op = qml.adjoint(CustomOp(0.5, wires=[0, 1, 2])) - rule = AdjointDecomp(custom_decomp) + rule = make_adjoint_decomp(custom_decomp) with qml.queuing.AnnotatedQueue() as q: rule(*op.parameters, wires=op.wires, **op.hyperparameters) @@ -177,30 +111,59 @@ def custom_decomp(phi, wires): assert rule.compute_resources(**op.resource_params) == Resources( { - qml.decomposition.adjoint_resource_rep(qml.T): 1, - qml.decomposition.adjoint_resource_rep(qml.CNOT): 2, - qml.decomposition.adjoint_resource_rep(qml.RX): 1, - qml.decomposition.adjoint_resource_rep(qml.H): 1, + adjoint_resource_rep(qml.T): 1, + adjoint_resource_rep(qml.CNOT): 2, + adjoint_resource_rep(qml.RX): 1, + adjoint_resource_rep(qml.H): 1, } ) - def test_adjoint_pow(self): - """Tests decomposing the adjoint of a Pow of an operator that has adjoint.""" + def test_adjoint_rotation(self): + """Tests the adjoint_rotation decomposition.""" - op = qml.adjoint(qml.pow(qml.H(0), 3)) - with qml.queuing.AnnotatedQueue() as q: - adjoint_pow_decomp(*op.parameters, wires=op.wires, **op.hyperparameters) + class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + + resource_keys = set() + + @property + def resource_params(self): + return {} - assert q.queue == [qml.pow(qml.H(0), 3)] - assert adjoint_pow_decomp.compute_resources(**op.resource_params) == to_resources( - {pow_resource_rep(qml.H, {}, 3): 1} + op = qml.adjoint(CustomOp(0.5, wires=[0, 1, 2])) + with queuing.AnnotatedQueue() as q: + adjoint_rotation(*op.parameters, wires=op.wires, **op.hyperparameters) + + assert q.queue == [CustomOp(-0.5, wires=[0, 1, 2])] + assert adjoint_rotation.compute_resources(**op.resource_params) == Resources( + {resource_rep(CustomOp): 1} ) + def test_self_adjoint(self): + """Tests the self_adjoint decomposition.""" + + class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + + resource_keys = set() + @property + def resource_params(self): + return {} + + op = qml.adjoint(CustomOp(0.5, wires=[0, 1, 2])) + with queuing.AnnotatedQueue() as q: + self_adjoint(*op.parameters, wires=op.wires, **op.hyperparameters) + + assert q.queue == [CustomOp(0.5, wires=[0, 1, 2])] + assert self_adjoint.compute_resources(**op.resource_params) == Resources( + {resource_rep(CustomOp): 1} + ) + + +@pytest.mark.unit class TestPowDecomposition: """Tests the decomposition rule defined for Pow.""" - def test_pow_pow(self): + def test_merge_powers(self): """Test the decomposition rule for nested powers.""" op = qml.pow(qml.pow(qml.H(0), 3), 2) @@ -212,7 +175,7 @@ def test_pow_pow(self): {pow_resource_rep(qml.H, {}, 6): 1} ) - def test_pow_general(self): + def test_repeat_pow_base(self): """Tests repeating the same op z number of times.""" op = qml.pow(qml.H(0), 3) @@ -223,7 +186,7 @@ def test_pow_general(self): assert repeat_pow_base.compute_resources(**op.resource_params) == to_resources({qml.H: 3}) @pytest.mark.jax - def test_pow_general_capture(self): + def test_repeat_pow_base_capture(self): """Tests that the general pow decomposition works with capture.""" from pennylane.tape.plxpr_conversion import CollectOpsandMeas @@ -244,12 +207,88 @@ def circuit(): if not capture_enabled: qml.capture.disable() - def test_non_integer_pow_not_implemented(self): - """Tests that NotImplementedError is raised when z isn't a positive integer.""" + def test_non_integer_pow_not_applicable(self): + """Tests that DecompositionNotApplicable is raised when z isn't a positive integer.""" op = qml.pow(qml.H(0), 0.5) - with pytest.raises(NotImplementedError, match="Non-integer or negative powers"): + with pytest.raises(DecompositionNotApplicable): repeat_pow_base.compute_resources(**op.resource_params) op = qml.pow(qml.H(0), -1) - with pytest.raises(NotImplementedError, match="Non-integer or negative powers"): + with pytest.raises(DecompositionNotApplicable): repeat_pow_base.compute_resources(**op.resource_params) + + def test_flip_pow_adjoint(self): + """Tests the flip_pow_adjoint decomposition.""" + + class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + + resource_keys = set() + + @property + def resource_params(self): + return {} + + op = qml.pow(qml.adjoint(CustomOp(0.5, wires=[0, 1, 2])), 2) + with queuing.AnnotatedQueue() as q: + flip_pow_adjoint(*op.parameters, wires=op.wires, **op.hyperparameters) + + assert q.queue == [qml.adjoint(qml.pow(CustomOp(0.5, wires=[0, 1, 2]), 2))] + assert flip_pow_adjoint.compute_resources(**op.resource_params) == Resources( + { + adjoint_resource_rep( + qml.ops.Pow, {"base_class": CustomOp, "base_params": {}, "z": 2} + ): 1 + } + ) + + def test_pow_of_self_adjoint(self): + """Tests the pow_of_self_adjoint decomposition.""" + + class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + + resource_keys = set() + + @property + def resource_params(self): + return {} + + op1 = qml.pow(CustomOp(wires=[0, 1, 2]), 1) + op2 = qml.pow(CustomOp(wires=[0, 1, 2]), 2) + op3 = qml.pow(CustomOp(wires=[0, 1, 2]), 3) + op4 = qml.pow(CustomOp(wires=[0, 1, 2]), 4) + + with qml.queuing.AnnotatedQueue() as q: + pow_of_self_adjoint(*op1.parameters, wires=op1.wires, **op1.hyperparameters) + pow_of_self_adjoint(*op2.parameters, wires=op2.wires, **op2.hyperparameters) + pow_of_self_adjoint(*op3.parameters, wires=op3.wires, **op3.hyperparameters) + pow_of_self_adjoint(*op4.parameters, wires=op4.wires, **op4.hyperparameters) + + assert q.queue == [CustomOp(wires=[0, 1, 2]), CustomOp(wires=[0, 1, 2])] + assert pow_of_self_adjoint.compute_resources(**op1.resource_params) == Resources( + {resource_rep(CustomOp): 1} + ) + assert pow_of_self_adjoint.compute_resources(**op3.resource_params) == Resources( + {resource_rep(CustomOp): 1} + ) + assert pow_of_self_adjoint.compute_resources(**op2.resource_params) == Resources() + assert pow_of_self_adjoint.compute_resources(**op4.resource_params) == Resources() + + def test_pow_rotations(self): + """Tests the pow_rotations decomposition.""" + + class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + + resource_keys = set() + + @property + def resource_params(self): + return {} + + op = qml.pow(CustomOp(0.3, wires=[0, 1, 2]), 2.5) + with queuing.AnnotatedQueue() as q: + pow_rotation(*op.parameters, wires=op.wires, **op.hyperparameters) + + assert q.queue == [CustomOp(0.3 * 2.5, wires=[0, 1, 2])] + assert pow_rotation.compute_resources(**op.resource_params) == Resources( + {resource_rep(CustomOp): 1} + ) From 9483966e6ba27c4e94d16aa66802f194c555cecd Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 14:58:16 -0400 Subject: [PATCH 10/45] changelog --- doc/releases/changelog-dev.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 76a096fd64f..814b7f5054f 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -123,6 +123,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 @register_resources({qml.RY: 1}) def my_adjoint_ry(phi, wires, **_): From 9a141f760562d4ef3bcf772ebb87d29d6c65adfd Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 15:14:45 -0400 Subject: [PATCH 11/45] pylint and black --- pennylane/decomposition/decomposition_graph.py | 4 ++-- pennylane/decomposition/symbolic_decomposition.py | 2 +- pennylane/ops/qubit/non_parametric_ops.py | 2 -- pennylane/ops/qubit/parametric_ops_single_qubit.py | 7 +------ tests/decomposition/conftest.py | 1 + tests/decomposition/test_resources.py | 2 -- 6 files changed, 5 insertions(+), 13 deletions(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index 9947d0ebb7e..d12bb98027e 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -222,10 +222,10 @@ def _add_op_node(self, op_node: CompressedResourceOp) -> int: return op_node_idx - def _add_decomp(self, rule: DecompositionRule, op: CompressedResourceOp, op_idx: int): + 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.params) + decomp_resource = rule.compute_resources(**op_node.params) d_node = _DecompositionNode(rule, decomp_resource) d_node_idx = self._graph.add_node(d_node) if not decomp_resource.gate_counts: diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index 2b576e69373..bf84f33f61f 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -93,7 +93,7 @@ def _loop(i): new_struct = (wires, *struct[1:]) base._unflatten(params, new_struct) - _loop() + _loop() # pylint: disable=no-value-for-parameter def _merge_powers_resource(base_class, base_params, z): # pylint: disable=unused-argument diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 3331e08f5a2..d3aa1b20ab5 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -28,10 +28,8 @@ from pennylane.decomposition import ( DecompositionNotApplicable, add_decomps, - adjoint_resource_rep, pow_resource_rep, register_resources, - resource_rep, ) from pennylane.decomposition.symbolic_decomposition import pow_of_self_adjoint, self_adjoint from pennylane.operation import Observable, Operation diff --git a/pennylane/ops/qubit/parametric_ops_single_qubit.py b/pennylane/ops/qubit/parametric_ops_single_qubit.py index f194fd7ca92..0a5b8086dfb 100644 --- a/pennylane/ops/qubit/parametric_ops_single_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_single_qubit.py @@ -24,12 +24,7 @@ import scipy as sp import pennylane as qml -from pennylane.decomposition import ( - DecompositionNotApplicable, - add_decomps, - register_resources, - resource_rep, -) +from pennylane.decomposition import add_decomps, register_resources from pennylane.decomposition.symbolic_decomposition import adjoint_rotation, pow_rotation from pennylane.operation import Operation from pennylane.typing import TensorLike diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py index ad586013bdb..1b9cd94be91 100644 --- a/tests/decomposition/conftest.py +++ b/tests/decomposition/conftest.py @@ -139,5 +139,6 @@ def _t_ps(wires, **__): decompositions["Adjoint(Hadamard)"] = [self_adjoint] decompositions["Pow(Hadamard)"] = [pow_of_self_adjoint] decompositions["Adjoint(RX)"] = [adjoint_rotation] +decompositions["Pow(RX)"] = [pow_rotation] decompositions["Adjoint(CNOT)"] = [self_adjoint] decompositions["Adjoint(PhaseShift)"] = [adjoint_rotation] diff --git a/tests/decomposition/test_resources.py b/tests/decomposition/test_resources.py index 72ed3d77736..43c5f69e077 100644 --- a/tests/decomposition/test_resources.py +++ b/tests/decomposition/test_resources.py @@ -368,8 +368,6 @@ def test_controlled_resource_op_flatten_x(self): def test_nested_controlled_qubit_unitary(self): """Tests that a nested controlled qubit unitary is flattened.""" - U = qml.math.eye(2) - rep = controlled_resource_rep( qml.ops.Controlled, { From 855111315c342f7060426c85bc50a9eb4ca65667 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 15:19:31 -0400 Subject: [PATCH 12/45] ooops --- pennylane/decomposition/resources.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pennylane/decomposition/resources.py b/pennylane/decomposition/resources.py index 9c84258c7dd..ebd3995aaa5 100644 --- a/pennylane/decomposition/resources.py +++ b/pennylane/decomposition/resources.py @@ -356,11 +356,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. @@ -370,8 +365,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, From a3705e89c32373e4f61f2d82aa2ae163572d5c6c Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 15:20:57 -0400 Subject: [PATCH 13/45] unused import --- pennylane/decomposition/resources.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pennylane/decomposition/resources.py b/pennylane/decomposition/resources.py index ebd3995aaa5..c1771562cd4 100644 --- a/pennylane/decomposition/resources.py +++ b/pennylane/decomposition/resources.py @@ -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 From 12a52c12d0a5cab8f5ce0fe9a0d8c362995638e7 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 16:10:25 -0400 Subject: [PATCH 14/45] remove test --- tests/decomposition/test_resources.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/decomposition/test_resources.py b/tests/decomposition/test_resources.py index 43c5f69e077..2b87302ce4d 100644 --- a/tests/decomposition/test_resources.py +++ b/tests/decomposition/test_resources.py @@ -488,12 +488,3 @@ def test_pow_resource_rep(self): op = qml.pow(qml.MultiRZ(0.5, wires=[0, 1, 2]), 3) assert op.resource_params == rep.params - - def test_non_integer_pow_not_supported(self): - """Tests that non-integer power is not supported yet.""" - - with pytest.raises(NotImplementedError, match="Non-integer powers"): - qml.decomposition.pow_resource_rep(qml.MultiRZ, {"num_wires": 3}, 3.5) - - with pytest.raises(NotImplementedError, match="Non-integer powers"): - qml.decomposition.pow_resource_rep(qml.MultiRZ, {"num_wires": 3}, -1) From 7a0d0d1544f54fc8b3571c8c8b94bcb42a5992fa Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 17:03:41 -0400 Subject: [PATCH 15/45] add missing coverage --- .../decomposition/test_decomposition_graph.py | 74 +++++++++++++++++++ .../test_symbolic_decomposition.py | 3 + 2 files changed, 77 insertions(+) diff --git a/tests/decomposition/test_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py index 289be3a6cbe..a4efd9721a8 100644 --- a/tests/decomposition/test_decomposition_graph.py +++ b/tests/decomposition/test_decomposition_graph.py @@ -596,3 +596,77 @@ def my_adjoint_rx(theta, wires, **__): assert graph.resource_estimate(op2) == to_resources({qml.H: 1}) assert graph.resource_estimate(op3) == to_resources({qml.CH: 1}) assert graph.resource_estimate(op4) == to_resources({qml.RX: 1}) + + def test_special_pow_decomps(self, _): + """Tests special cases for decomposing a power.""" + + class CustomOp(qml.operation.Operation): # pylint: disable=too-few-public-methods + """A custom operation.""" + + resource_keys = set() + + @property + def resource_params(self): + return {} + + graph = DecompositionGraph( + operations=[qml.pow(CustomOp(0), 0), qml.pow(CustomOp(1), 1)], gate_set={"CustomOp"} + ) + # 3 operator nodes: Pow(CustomOp, 0), Pow(CustomOp, 1), and CustomOp + # 1 decomposition node for Pow(CustomOp, 0) and 1 node for Pow(CustomOp, 1) + # and the dummy starting node + assert len(graph._graph.nodes()) == 6 + # 2 edges from decompositions to ops and 1 edge from ops to decompositions + # and 1 edge from the dummy starting node to the target gate set. + # and 1 edge from the dummy starting node to the empty decomposition. + assert len(graph._graph.edges()) == 5 + + op1 = qml.pow(CustomOp(0), 0) + op2 = qml.pow(CustomOp(1), 1) + + graph.solve() + with qml.queuing.AnnotatedQueue() as q: + graph.decomposition(op1)(*op1.parameters, wires=op1.wires, **op1.hyperparameters) + graph.decomposition(op2)(*op2.parameters, wires=op2.wires, **op2.hyperparameters) + + assert q.queue == [CustomOp(1)] + assert graph.resource_estimate(op1) == to_resources({}) + assert graph.resource_estimate(op2) == to_resources({CustomOp: 1}) + + def test_general_pow_decomps(self, _): + """Tests the more general power decomposition rules.""" + + class CustomOp(qml.operation.Operation): # pylint: disable=too-few-public-methods + """A custom operation.""" + + resource_keys = set() + + @property + def resource_params(self): + return {} + + graph = DecompositionGraph( + operations=[qml.pow(CustomOp(0), 2), qml.pow(qml.adjoint(CustomOp(1)), 2)], + gate_set={"CustomOp", "Adjoint(CustomOp)"}, + ) + # 5 operator nodes: Pow(CustomOp), Pow(Adjoint(CustomOp)), Adjoint(Pow(CustomOp)), + # Adjoint(CustomOp), CustomOp, and the dummy starting node + # 3 decomposition nodes for each of Pow(CustomOp), Pow(Adjoint(CustomOp)), Adjoint(Pow(CustomOp)) + assert len(graph._graph.nodes()) == 9 + # 3 edges from decompositions to ops and 3 edges from ops to decompositions + # and 2 edges from the dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 8 + + op1 = qml.pow(CustomOp(0), 2) + op2 = qml.pow(qml.adjoint(CustomOp(1)), 2) + + graph.solve() + with qml.queuing.AnnotatedQueue() as q: + graph.decomposition(op1)(*op1.parameters, wires=op1.wires, **op1.hyperparameters) + graph.decomposition(op2)(*op2.parameters, wires=op2.wires, **op2.hyperparameters) + + assert q.queue == [ + CustomOp(0), + CustomOp(0), + qml.adjoint(qml.pow(CustomOp(1), 2)), + ] diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index 01795492f58..7fa87156f18 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -273,6 +273,9 @@ def resource_params(self): assert pow_of_self_adjoint.compute_resources(**op2.resource_params) == Resources() assert pow_of_self_adjoint.compute_resources(**op4.resource_params) == Resources() + with pytest.raises(DecompositionNotApplicable): + pow_of_self_adjoint.compute_resources(CustomOp, {}, z=0.5) + def test_pow_rotations(self): """Tests the pow_rotations decomposition.""" From 7188ceefe519fa42e0bafb7c011891e82e5e459b Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 17:41:23 -0400 Subject: [PATCH 16/45] tests and missing coverage --- pennylane/ops/functions/assert_valid.py | 5 +++++ pennylane/ops/qubit/matrix_ops.py | 4 ++-- pennylane/ops/qubit/non_parametric_ops.py | 22 +++++++++---------- .../ops/qubit/parametric_ops_multi_qubit.py | 1 - 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index 020fc1a87bc..a2f10019aa8 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -116,6 +116,11 @@ def _check_decomposition_new(op, heuristic_resources=False): adj_op = qml.ops.Adjoint(op) _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) + for rule in qml.list_decomps(f"Pow({type(op).__name__})"): + for z in [2, 3, 3.5]: + pow_op = qml.ops.Pow(op, z) + _test_decomposition_rule(pow_op, rule, heuristic_resources=heuristic_resources) + def _test_decomposition_rule(op, rule: DecompositionRule, heuristic_resources=False): """Tests that a decomposition rule is consistent with the operator.""" diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py index 5948832849c..4941f638c62 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -356,7 +356,7 @@ def label( ) -def _qubit_unitary_resource(base_class, base_params): +def _qubit_unitary_resource(base_class, base_params, **_): return {resource_rep(base_class, **base_params): 1} @@ -582,7 +582,7 @@ def label( return super().label(decimals=decimals, base_label=base_label or "U", cache=cache) -def _diagonal_qubit_unitary_resource(base_class, base_params): +def _diagonal_qubit_unitary_resource(base_class, base_params, **_): return {resource_rep(base_class, **base_params): 1} diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index d3aa1b20ab5..bf33b71e301 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -1881,10 +1881,10 @@ def compute_decomposition(wires: WiresLike) -> list[qml.operation.Operator]: ] def pow(self, z: Union[int, float]) -> list[qml.operation.Operator]: - z_mod2 = z % 2 - if abs(z_mod2 - 0.5) < 1e-6: + z_mod4 = z % 4 + if abs(z_mod4 - 0.5) < 1e-6: return [SISWAP(wires=self.wires)] - return super().pow(z_mod2) + return super().pow(z_mod4) def _iswap_decomp_resources(): @@ -1905,21 +1905,21 @@ def _iswap_decomp(wires, **__): def _pow_iswap_to_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument - z_mod2 = z % 2 - if z_mod2 == 0.5: + z_mod4 = z % 4 + if z_mod4 == 0.5: return {SISWAP: 1} - if z != z_mod2: - return {pow_resource_rep(base_class, base_params, z=z_mod2): 1} + if z != z_mod4: + return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} raise DecompositionNotApplicable @register_resources(_pow_iswap_to_siswap_resource) def _pow_iswap_to_siswap(wires, z, **__): - z_mod2 = z % 2 + z_mod4 = z % 4 qml.cond( - z_mod2 == 0.5, + z_mod4 == 0.5, lambda: SISWAP(wires=wires), - lambda: qml.pow(ISWAP(wires=wires), z_mod2), + lambda: qml.pow(ISWAP(wires=wires), z_mod4), )() @@ -2120,7 +2120,7 @@ def _pow_siswap(wires, z, **__): qml.math.allclose(z_mod4, 2), lambda: ISWAP(wires=wires), lambda: qml.pow(SISWAP(wires=wires), z_mod4), - ) + )() add_decomps("Pow(SISWAP)", _pow_siswap) diff --git a/pennylane/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/ops/qubit/parametric_ops_multi_qubit.py index b79beb88c6b..19560c87fdd 100644 --- a/pennylane/ops/qubit/parametric_ops_multi_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_multi_qubit.py @@ -1753,7 +1753,6 @@ def _pswap_to_swap_cnot_phaseshift_cnot(phi: TensorLike, wires: WiresLike, **__) add_decomps(PSWAP, _pswap_to_swap_cnot_phaseshift_cnot) add_decomps("Adjoint(PSWAP)", adjoint_rotation) -add_decomps("Pow(PSWAP)", pow_rotation) class CPhaseShift00(Operation): From 58a3b4dc3079889e2c32d22259072cb737c57a01 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 09:37:43 -0400 Subject: [PATCH 17/45] fix test --- tests/ops/qubit/test_non_parametric_ops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py index 7f9d929c3b5..8841cb28aa9 100644 --- a/tests/ops/qubit/test_non_parametric_ops.py +++ b/tests/ops/qubit/test_non_parametric_ops.py @@ -827,7 +827,6 @@ def test_repr(self): qml.PauliZ(0), qml.Hadamard("a"), qml.SWAP(wires=(0, 1)), - qml.ISWAP(wires=(0, 1)), qml.ECR(wires=(0, 1)), # Controlled operations qml.CNOT(wires=(0, 1)), @@ -842,6 +841,7 @@ def test_repr(self): class TestPowMethod: + @pytest.mark.parametrize("op", period_two_ops) @pytest.mark.parametrize("n", (1, 5, -1, -5)) def test_period_two_pow_odd(self, op, n): @@ -919,7 +919,7 @@ def test_pauliz_general_power(self, n): assert op_pow[0].__class__ is qml.PhaseShift assert qml.math.allclose(op_pow[0].data[0], np.pi * (n % 2)) - @pytest.mark.parametrize("n", (0.5, 2.5, -1.5)) + @pytest.mark.parametrize("n", (0.5, 4.5, -3.5)) def test_ISWAP_sqaure_root(self, n): """Test that SISWAP is the square root of ISWAP.""" op = qml.ISWAP(wires=(0, 1)) From 499cf5958a242ab2945fc67b03eda75dee3fb69a Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 10:00:50 -0400 Subject: [PATCH 18/45] fix siswap and tests --- doc/releases/changelog-dev.md | 3 ++ pennylane/ops/functions/assert_valid.py | 2 +- pennylane/ops/qubit/non_parametric_ops.py | 63 +++++++++++++++------- tests/ops/qubit/test_non_parametric_ops.py | 2 +- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 814b7f5054f..548b0fc14d5 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -302,6 +302,9 @@ * Fixed a bug where the phase is used as the wire label for a `qml.GlobalPhase` when capture is enabled. [(#7211)](https://github.com/PennyLaneAI/pennylane/pull/7211) +* Fixes a bug where the `qml.ISWAP` raised to a power of 2 incorrectly decomposes to `qml.SISWAP`. + [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347) +

Contributors ✍️

This release contains contributions from (in alphabetical order): diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index a2f10019aa8..fce07efb559 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -117,7 +117,7 @@ def _check_decomposition_new(op, heuristic_resources=False): _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) for rule in qml.list_decomps(f"Pow({type(op).__name__})"): - for z in [2, 3, 3.5]: + for z in [0.5, 2, 3, 4, 4.5]: pow_op = qml.ops.Pow(op, z) _test_decomposition_rule(pow_op, rule, heuristic_resources=heuristic_resources) diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index bf33b71e301..3dc27de0033 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -1904,26 +1904,36 @@ def _iswap_decomp(wires, **__): add_decomps(ISWAP, _iswap_decomp) -def _pow_iswap_to_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument +def _pow_iswap_resource(base_class, base_params, z): # pylint: disable=unused-argument z_mod4 = z % 4 if z_mod4 == 0.5: return {SISWAP: 1} + if z_mod4 == 2: + return {Z: 2} if z != z_mod4: return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} raise DecompositionNotApplicable -@register_resources(_pow_iswap_to_siswap_resource) -def _pow_iswap_to_siswap(wires, z, **__): +@register_resources(_pow_iswap_resource) +def _pow_iswap_decomp(wires, z, **__): + z_mod4 = z % 4 - qml.cond( - z_mod4 == 0.5, - lambda: SISWAP(wires=wires), - lambda: qml.pow(ISWAP(wires=wires), z_mod4), - )() + def _siswap(): + SISWAP(wires=wires) + + def _general_case(): + qml.pow(ISWAP(wires=wires), z_mod4) -add_decomps("Pow(ISWAP)", _pow_iswap_to_siswap) + def _zz(): + qml.Z(wires[0]) + qml.Z(wires[1]) + + qml.cond(z_mod4 == 0.5, _siswap, _general_case, elifs=[(z_mod4 == 2, _zz)])() + + +add_decomps("Pow(ISWAP)", _pow_iswap_decomp) class SISWAP(Operation): @@ -2077,8 +2087,8 @@ def compute_decomposition(wires: WiresLike) -> list[qml.operation.Operator]: ] def pow(self, z: Union[int, float]) -> list[qml.operation.Operator]: - z_mod4 = z % 4 - return [ISWAP(wires=self.wires)] if z_mod4 == 2 else super().pow(z_mod4) + z_mod8 = z % 8 + return [ISWAP(wires=self.wires)] if z_mod8 == 2 else super().pow(z_mod8) def _siswap_decomp_resources(): @@ -2105,21 +2115,36 @@ def _siswap_decomp(wires, **__): def _pow_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument - z_mod4 = z % 4 - if qml.math.allclose(z_mod4, 2): + z_mod8 = z % 8 + if qml.math.allclose(z_mod8, 2): return {ISWAP: 1} - if z != z_mod4: - return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} + if qml.math.allclose(z_mod8, 4): + return {Z: 2} + if z != z_mod8: + return {pow_resource_rep(base_class, base_params, z=z_mod8): 1} raise DecompositionNotApplicable @register_resources(_pow_siswap_resource) def _pow_siswap(wires, z, **__): - z_mod4 = z % 4 + + z_mod8 = z % 8 + + def _iswap(): + ISWAP(wires=wires) + + def _zz(): + qml.Z(wires[0]) + qml.Z(wires[1]) + + def _general_case(): + qml.pow(SISWAP(wires=wires), z_mod8) + qml.cond( - qml.math.allclose(z_mod4, 2), - lambda: ISWAP(wires=wires), - lambda: qml.pow(SISWAP(wires=wires), z_mod4), + qml.math.allclose(z_mod8, 2), + _iswap, + _general_case, + elifs=[(qml.math.allclose(z_mod8, 4), _zz)], )() diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py index 8841cb28aa9..8435dea13f7 100644 --- a/tests/ops/qubit/test_non_parametric_ops.py +++ b/tests/ops/qubit/test_non_parametric_ops.py @@ -972,7 +972,7 @@ def test_SX_pow(self, offset): with pytest.raises(qml.operation.PowUndefinedError): op.pow(2.43 + offset) - @pytest.mark.parametrize("offset", (0, 4, -4)) + @pytest.mark.parametrize("offset", (0, 8, -8)) def test_SISWAP_pow(self, offset): """Test powers of the SISWAP operator""" op = qml.SISWAP(wires=("b", "c")) From 82c87808d3c9bb241d54f13bcb6b3948e6f50fef Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 10:01:38 -0400 Subject: [PATCH 19/45] ooops --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 548b0fc14d5..5a287fce0c7 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -302,7 +302,7 @@ * Fixed a bug where the phase is used as the wire label for a `qml.GlobalPhase` when capture is enabled. [(#7211)](https://github.com/PennyLaneAI/pennylane/pull/7211) -* Fixes a bug where the `qml.ISWAP` raised to a power of 2 incorrectly decomposes to `qml.SISWAP`. +* Fixes a bug where the `qml.ISWAP` raised to a power of 2 incorrectly decomposes to nothing. [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347)

Contributors ✍️

From 85f04856da34a0f76c127bf63dc41e96a36db99d Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 10:49:56 -0400 Subject: [PATCH 20/45] add missing coverage --- pennylane/ops/functions/assert_valid.py | 2 +- .../decompositions/unitary_decompositions.py | 2 ++ tests/ops/qubit/test_matrix_ops.py | 26 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index fce07efb559..fb4bad395a3 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -117,7 +117,7 @@ def _check_decomposition_new(op, heuristic_resources=False): _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) for rule in qml.list_decomps(f"Pow({type(op).__name__})"): - for z in [0.5, 2, 3, 4, 4.5]: + for z in [0.5, 2, 4, 8, 9, 10]: pow_op = qml.ops.Pow(op, z) _test_decomposition_rule(pow_op, rule, heuristic_resources=heuristic_resources) diff --git a/pennylane/ops/op_math/decompositions/unitary_decompositions.py b/pennylane/ops/op_math/decompositions/unitary_decompositions.py index 7167e17e248..6676a44273f 100644 --- a/pennylane/ops/op_math/decompositions/unitary_decompositions.py +++ b/pennylane/ops/op_math/decompositions/unitary_decompositions.py @@ -258,6 +258,8 @@ def _get_impl(self): """The implementation of the decomposition rule.""" def _impl(U, wires, **__): + if sp.issparse(U): + U = U.todense() U, global_phase = math.convert_to_su2(U, return_global_phase=True) self._naive_rule(U, wires=wires) ops.GlobalPhase(-global_phase) diff --git a/tests/ops/qubit/test_matrix_ops.py b/tests/ops/qubit/test_matrix_ops.py index c8379033b7f..ec2ab3f53cd 100644 --- a/tests/ops/qubit/test_matrix_ops.py +++ b/tests/ops/qubit/test_matrix_ops.py @@ -191,6 +191,32 @@ def test_csr_matrix_decomposition(self): mat2 = qml.matrix(op.decomposition, wire_order=[0])() assert qml.math.allclose(mat2, mat.todense()) + def test_csr_matrix_decomposition_new(self): + """Tests that the QubitUnitary's decomposition works with csr_matrix.""" + + U = csr_matrix(unitary_group.rvs(2)) + op = qml.QubitUnitary(U, wires=[0]) + rule = qml.list_decomps(qml.QubitUnitary)[0] + with qml.queuing.AnnotatedQueue() as q: + rule(*op.parameters, wires=op.wires, **op.hyperparameters) + + tape = qml.tape.QuantumScript.from_queue(q) + actual_mat = qml.matrix(tape) + assert qml.math.allclose(actual_mat, U.todense()) + + def test_csr_matrix_pow_new(self): + """Tests the pow decomposition of a QubitUnitary works with csr_matrix.""" + + U = csr_matrix(unitary_group.rvs(2)) + op = qml.pow(qml.QubitUnitary(U, wires=[0]), 2) + rule = qml.list_decomps("Pow(QubitUnitary)")[0] + with qml.queuing.AnnotatedQueue() as q: + rule(*op.parameters, wires=op.wires, **op.hyperparameters) + + tape = qml.tape.QuantumScript.from_queue(q) + actual_mat = qml.matrix(tape) + assert qml.math.allclose(actual_mat, (U @ U).todense()) + class TestQubitUnitary: """Tests for the QubitUnitary class.""" From 46a18a04a83109ee1c268de6f82b0892e57e606c Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 11:11:26 -0400 Subject: [PATCH 21/45] missing coverage --- pennylane/ops/functions/assert_valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index fb4bad395a3..9f1655bf879 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -117,7 +117,7 @@ def _check_decomposition_new(op, heuristic_resources=False): _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) for rule in qml.list_decomps(f"Pow({type(op).__name__})"): - for z in [0.5, 2, 4, 8, 9, 10]: + for z in [0.5, 2, 3, 4, 8, 9]: pow_op = qml.ops.Pow(op, z) _test_decomposition_rule(pow_op, rule, heuristic_resources=heuristic_resources) From 92f0b8415ad9d6236ac75cd8bfe673120f3e02ec Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 11:22:24 -0400 Subject: [PATCH 22/45] changelog --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 5a287fce0c7..ee01a54253a 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -302,7 +302,7 @@ * Fixed a bug where the phase is used as the wire label for a `qml.GlobalPhase` when capture is enabled. [(#7211)](https://github.com/PennyLaneAI/pennylane/pull/7211) -* Fixes a bug where the `qml.ISWAP` raised to a power of 2 incorrectly decomposes to nothing. +* Fixes a bug where the `qml.ISWAP` raised to a power of 2 and `qml.SISWAP` raised to a power of 4 incorrectly decompose to nothing. [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347)

Contributors ✍️

From 2060ca1004d6f60d95e528b8309ca8d45bdde900 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 14:01:13 -0400 Subject: [PATCH 23/45] revert some changes --- doc/releases/changelog-dev.md | 3 --- pennylane/ops/qubit/non_parametric_ops.py | 10 +++++----- tests/ops/qubit/test_non_parametric_ops.py | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index ee01a54253a..814b7f5054f 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -302,9 +302,6 @@ * Fixed a bug where the phase is used as the wire label for a `qml.GlobalPhase` when capture is enabled. [(#7211)](https://github.com/PennyLaneAI/pennylane/pull/7211) -* Fixes a bug where the `qml.ISWAP` raised to a power of 2 and `qml.SISWAP` raised to a power of 4 incorrectly decompose to nothing. - [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347) -

Contributors ✍️

This release contains contributions from (in alphabetical order): diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 3dc27de0033..9f649245e7c 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -1881,10 +1881,10 @@ def compute_decomposition(wires: WiresLike) -> list[qml.operation.Operator]: ] def pow(self, z: Union[int, float]) -> list[qml.operation.Operator]: - z_mod4 = z % 4 - if abs(z_mod4 - 0.5) < 1e-6: + z_mod2 = z % 2 + if abs(z_mod2 - 0.5) < 1e-6: return [SISWAP(wires=self.wires)] - return super().pow(z_mod4) + return super().pow(z_mod2) def _iswap_decomp_resources(): @@ -2087,8 +2087,8 @@ def compute_decomposition(wires: WiresLike) -> list[qml.operation.Operator]: ] def pow(self, z: Union[int, float]) -> list[qml.operation.Operator]: - z_mod8 = z % 8 - return [ISWAP(wires=self.wires)] if z_mod8 == 2 else super().pow(z_mod8) + z_mod4 = z % 4 + return [ISWAP(wires=self.wires)] if z_mod4 == 2 else super().pow(z_mod4) def _siswap_decomp_resources(): diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py index 8435dea13f7..37dc16439e2 100644 --- a/tests/ops/qubit/test_non_parametric_ops.py +++ b/tests/ops/qubit/test_non_parametric_ops.py @@ -919,7 +919,7 @@ def test_pauliz_general_power(self, n): assert op_pow[0].__class__ is qml.PhaseShift assert qml.math.allclose(op_pow[0].data[0], np.pi * (n % 2)) - @pytest.mark.parametrize("n", (0.5, 4.5, -3.5)) + @pytest.mark.parametrize("n", (0.5, 2.5, -1.5)) def test_ISWAP_sqaure_root(self, n): """Test that SISWAP is the square root of ISWAP.""" op = qml.ISWAP(wires=(0, 1)) @@ -972,7 +972,7 @@ def test_SX_pow(self, offset): with pytest.raises(qml.operation.PowUndefinedError): op.pow(2.43 + offset) - @pytest.mark.parametrize("offset", (0, 8, -8)) + @pytest.mark.parametrize("offset", (0, 4, -4)) def test_SISWAP_pow(self, offset): """Test powers of the SISWAP operator""" op = qml.SISWAP(wires=("b", "c")) From 26a314d2abf8c37fe8d62546918cf4bd6c9efed4 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 1 May 2025 14:42:45 -0400 Subject: [PATCH 24/45] Apply suggestions from code review --- doc/releases/changelog-dev.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 76a096fd64f..f6f17f75b39 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -99,7 +99,7 @@ * Symbolic operator types (e.g., `Adjoint`, `Controlled`, and `Pow`) can now be specified as strings in various parts of the new graph-based decomposition system, specifically: * The `gate_set` argument of the :func:`~.transforms.decompose` transform now supports adding symbolic - operators to the target gate set. + operators in the target gate set. [(#7331)](https://github.com/PennyLaneAI/pennylane/pull/7331) ```python from functools import partial @@ -137,7 +137,7 @@ @partial( qml.transforms.decompose, - gate_set={"RX", "CNOT"}, + gate_set={"RX", "RY", "CNOT"}, fixed_decomps={"Adjoint(RX)": my_adjoint_rx} ) @qml.qnode(qml.device("default.qubit")) From 02f08f29b013640a3e33ed99f52f6e46fecfe1e9 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 1 May 2025 15:07:06 -0400 Subject: [PATCH 25/45] Apply suggestions from code review Co-authored-by: Pietropaolo Frisoni --- doc/releases/changelog-dev.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index f6f17f75b39..ed17d74b52a 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -124,7 +124,7 @@ registered for symbolic operators. [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347) ```python - @register_resources({qml.RY: 1}) + @qml.register_resources({qml.RY: 1}) def my_adjoint_ry(phi, wires, **_): qml.RY(-phi, wires=wires) @@ -142,9 +142,9 @@ ) @qml.qnode(qml.device("default.qubit")) def circuit(): - qml.adjoint(qml.RX(0.5), wires=[0]) + qml.adjoint(qml.RX(0.5, wires=[0])) qml.CNOT(wires=[0, 1]) - qml.adjoint(qml.RY(0.5), wires=[1]) + qml.adjoint(qml.RY(0.5, wires=[1])) return qml.expval(qml.Z(0)) ``` ```pycon From fa54ba23378fad326431ae70d7e1c9d94ce0994e Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 1 May 2025 15:18:37 -0400 Subject: [PATCH 26/45] lol --- pennylane/decomposition/decomposition_graph.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index 66a63a42b23..4813ff4cfa9 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -54,8 +54,6 @@ ) from .utils import DecompositionError, DecompositionNotApplicable, translate_op_alias -NULL = "null" # sentinel value for the start node in the graph - class DecompositionGraph: # pylint: disable=too-many-instance-attributes """A graph that models a decomposition problem. @@ -151,7 +149,7 @@ def __init__( self._visitor = None # Construct the decomposition graph - self._start = self._graph.add_node(NULL) + self._start = self._graph.add_node(None) self._construct_graph(operations) def _get_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: @@ -452,7 +450,7 @@ def examine_edge(self, edge): return # nothing is to be done for edges leading to an operator node if target_idx not in self.distances: self.distances[target_idx] = Resources() # initialize with empty resource - if src_node == NULL: + if src_node is None: return # special case for when the decomposition produces nothing self.distances[target_idx] += self.distances[src_idx] * target_node.count(src_node) if target_idx not in self._num_edges_examined: @@ -469,7 +467,7 @@ def edge_relaxed(self, edge): """Triggered when an edge is relaxed during the Dijkstra search.""" src_idx, target_idx, _ = edge target_node = self._graph[target_idx] - if self._graph[src_idx] == NULL and not isinstance(target_node, _DecompositionNode): + if self._graph[src_idx] is None and not isinstance(target_node, _DecompositionNode): self.distances[target_idx] = Resources({target_node: 1}) elif isinstance(target_node, CompressedResourceOp): self.predecessors[target_idx] = src_idx From 30c0c3c13903a89266a877b3fb99242582c09825 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Fri, 2 May 2025 10:00:11 -0400 Subject: [PATCH 27/45] ooops --- pennylane/ops/op_math/controlled.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index 6143871fb81..0d9eeb12ba7 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -33,6 +33,7 @@ from pennylane import math as qmlmath from pennylane import operation from pennylane.compiler import compiler +from pennylane.decomposition.controlled_decomposition import base_to_custom_ctrl_op from pennylane.operation import Operator from pennylane.wires import Wires, WiresLike @@ -1001,28 +1002,3 @@ def _(base, *control_wires, control_values=None, work_wires=None, id=None): # easier to just keep the same primitive for both versions # dispatch between the two types happens inside instance creation anyway ControlledOp._primitive = Controlled._primitive # pylint: disable=protected-access - - -@functools.lru_cache(maxsize=1) -def base_to_custom_ctrl_op(): - """A dictionary mapping base op types to their custom controlled versions. - - This dictionary is used under the assumption that all custom controlled operations do not - have resource params (which is why `ControlledQubitUnitary` is not included here). - - """ - - ops_with_custom_ctrl_ops = { - (qml.PauliZ, 1): qml.CZ, - (qml.PauliZ, 2): qml.CCZ, - (qml.PauliY, 1): qml.CY, - (qml.CZ, 1): qml.CCZ, - (qml.SWAP, 1): qml.CSWAP, - (qml.Hadamard, 1): qml.CH, - (qml.RX, 1): qml.CRX, - (qml.RY, 1): qml.CRY, - (qml.RZ, 1): qml.CRZ, - (qml.Rot, 1): qml.CRot, - (qml.PhaseShift, 1): qml.ControlledPhaseShift, - } - return ops_with_custom_ctrl_ops From a85a40fb666911c72041789cc7bc90adbda370e9 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Fri, 2 May 2025 16:08:36 -0400 Subject: [PATCH 28/45] minor fix --- pennylane/decomposition/symbolic_decomposition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index dc4acf40084..61ee6322fc6 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -76,12 +76,12 @@ def _adjoint_adjoint_resource(*_, base_params, **__): return {resource_rep(base_class, **base_params): 1} +# pylint: disable=protected-access @register_resources(_adjoint_adjoint_resource) def cancel_adjoint(*params, wires, base): # pylint: disable=unused-argument """Decompose the adjoint of the adjoint of a gate.""" - _, [_, metadata] = base.base._flatten() # pylint: disable=protected-access - new_struct = wires, metadata - base.base._unflatten(params, new_struct) # pylint: disable=protected-access + _, struct = base.base._flatten() + base.base._unflatten(params, struct) def _adjoint_controlled_resource(base_class, base_params): From 5db0fc29567c0bab832e701b3a369b34997f70ff Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Mon, 5 May 2025 10:53:09 -0400 Subject: [PATCH 29/45] get rid of funny business --- pennylane/decomposition/symbolic_decomposition.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index 61ee6322fc6..15f6fb94128 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -80,8 +80,7 @@ def _adjoint_adjoint_resource(*_, base_params, **__): @register_resources(_adjoint_adjoint_resource) def cancel_adjoint(*params, wires, base): # pylint: disable=unused-argument """Decompose the adjoint of the adjoint of a gate.""" - _, struct = base.base._flatten() - base.base._unflatten(params, struct) + base.base._unflatten(*base.base._flatten()) def _adjoint_controlled_resource(base_class, base_params): From 74604ab97f3d8f8e0929986c626e9cb8c081d270 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 7 May 2025 09:41:16 -0400 Subject: [PATCH 30/45] minor rename --- .../decomposition/decomposition_graph.py | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index 4813ff4cfa9..3d668424cb6 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -152,24 +152,24 @@ def __init__( self._start = self._graph.add_node(None) self._construct_graph(operations) - def _get_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: + def _get_decompositions(self, op_node: CompressedResourceOp) -> list[DecompositionRule]: """Helper function to get a list of decomposition rules.""" - op_name = _to_name(op) + op_name = _to_name(op_node) if op_name in self._fixed_decomps: return [self._fixed_decomps[op_name]] decomps = self._alt_decomps.get(op_name, []) + list_decomps(op_name) - if issubclass(op.op_type, qml.ops.Adjoint): - decomps.extend(self._get_adjoint_decompositions(op)) + if issubclass(op_node.op_type, qml.ops.Adjoint): + decomps.extend(self._get_adjoint_decompositions(op_node)) - elif issubclass(op.op_type, qml.ops.Pow): - decomps.extend(self._get_pow_decompositions(op)) + elif issubclass(op_node.op_type, qml.ops.Pow): + decomps.extend(self._get_pow_decompositions(op_node)) - elif op.op_type in (qml.ops.Controlled, qml.ops.ControlledOp): - decomps.extend(self._get_controlled_decompositions(op)) + elif op_node.op_type in (qml.ops.Controlled, qml.ops.ControlledOp): + decomps.extend(self._get_controlled_decompositions(op_node)) return decomps @@ -215,10 +215,10 @@ def _add_decomp_rule_to_op( except DecompositionNotApplicable: pass # ignore decompositions that are not applicable to the given op params. - def _get_adjoint_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: + def _get_adjoint_decompositions(self, op_node: CompressedResourceOp) -> list[DecompositionRule]: """Retrieves a list of decomposition rules for an adjoint operator.""" - base_class, base_params = op.params["base_class"], op.params["base_params"] + base_class, base_params = (op_node.params["base_class"], op_node.params["base_params"]) if issubclass(base_class, qml.ops.Adjoint): return [cancel_adjoint] @@ -242,21 +242,23 @@ def _get_adjoint_decompositions(self, op: CompressedResourceOp) -> list[Decompos return [AdjointDecomp(base_rule) for base_rule in self._get_decompositions(base_rep)] @staticmethod - def _get_pow_decompositions(op: CompressedResourceOp) -> list[DecompositionRule]: + def _get_pow_decompositions(op_node: CompressedResourceOp) -> list[DecompositionRule]: """Retrieves a list of decomposition rules for a power operator.""" - base_class = op.params["base_class"] + base_class = op_node.params["base_class"] if issubclass(base_class, qml.ops.Pow): return [merge_powers] return [repeat_pow_base] - def _get_controlled_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: + def _get_controlled_decompositions( + self, op_node: CompressedResourceOp + ) -> list[DecompositionRule]: """Adds a controlled decomposition node to the graph.""" - base_class = op.params["base_class"] - num_control_wires = op.params["num_control_wires"] + base_class = op_node.params["base_class"] + num_control_wires = op_node.params["num_control_wires"] # Handle controlled global phase if base_class is qml.GlobalPhase: @@ -272,7 +274,7 @@ def _get_controlled_decompositions(self, op: CompressedResourceOp) -> list[Decom return [CustomControlledDecomposition(custom_op_type)] # General case - base_rep = resource_rep(base_class, **op.params["base_params"]) + 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( From 3bce1fb60d2921423f6922dd1cc6af37b70ea84a Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Mon, 12 May 2025 14:05:24 -0400 Subject: [PATCH 31/45] rename pow_self_adjoint to pow_involutory --- .../decomposition/decomposition_graph.py | 4 ++-- .../decomposition/symbolic_decomposition.py | 8 ++++---- pennylane/ops/op_math/controlled_ops.py | 18 ++++++++--------- pennylane/ops/qubit/arithmetic_ops.py | 4 ++-- pennylane/ops/qubit/non_parametric_ops.py | 14 ++++++------- tests/decomposition/conftest.py | 4 ++-- .../test_symbolic_decomposition.py | 20 +++++++++---------- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index 5ff32cb8f8f..d711b545f22 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -49,7 +49,7 @@ flip_pow_adjoint, make_adjoint_decomp, merge_powers, - pow_of_self_adjoint, + pow_involutory, pow_rotation, repeat_pow_base, self_adjoint, @@ -177,7 +177,7 @@ def _get_decompositions(self, op_node: CompressedResourceOp) -> list[Decompositi elif ( issubclass(op_node.op_type, qml.ops.Pow) and pow_rotation not in decomps - and pow_of_self_adjoint not in decomps + and pow_involutory 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 diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index 0060a0e4c9e..6c871f6af4d 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -130,16 +130,16 @@ def flip_pow_adjoint(*params, wires, base, z, **__): qml.adjoint(qml.pow(base_op, z)) -def _pow_self_adjoint_resource(base_class, base_params, z): # pylint: disable=unused-argument +def _pow_involutory_resource(base_class, base_params, z): # pylint: disable=unused-argument if (not is_integer(z)) or z < 0: raise DecompositionNotApplicable return {resource_rep(base_class, **base_params): z % 2} # pylint: disable=protected-access,unused-argument -@register_resources(_pow_self_adjoint_resource) -def pow_of_self_adjoint(*params, wires, base, z, **__): - """Decompose the power of a self-adjoint operator, assumes z is an integer.""" +@register_resources(_pow_involutory_resource) +def pow_involutory(*params, wires, base, z, **__): + """Decompose the power of an involutory operator, assumes z is an integer.""" def f(): base._unflatten(*base._flatten()) diff --git a/pennylane/ops/op_math/controlled_ops.py b/pennylane/ops/op_math/controlled_ops.py index 78d7f5675c2..b51ba8e24cf 100644 --- a/pennylane/ops/op_math/controlled_ops.py +++ b/pennylane/ops/op_math/controlled_ops.py @@ -26,7 +26,7 @@ from pennylane.decomposition import add_decomps, register_resources from pennylane.decomposition.symbolic_decomposition import ( adjoint_rotation, - pow_of_self_adjoint, + pow_involutory, pow_rotation, self_adjoint, ) @@ -337,7 +337,7 @@ def _ch_to_ry_cz_ry(wires: WiresLike, **__): add_decomps(CH, _ch_to_ry_cz_ry) add_decomps("Adjoint(CH)", self_adjoint) -add_decomps("Pow(CH)", pow_of_self_adjoint) +add_decomps("Pow(CH)", pow_involutory) class CY(ControlledOp): @@ -472,7 +472,7 @@ def _cy(wires: WiresLike, **__): add_decomps(CY, _cy) add_decomps("Adjoint(CY)", self_adjoint) -add_decomps("Pow(CY)", pow_of_self_adjoint) +add_decomps("Pow(CY)", pow_involutory) class CZ(ControlledOp): @@ -590,7 +590,7 @@ def _cz_to_cnot(wires: WiresLike, **__): add_decomps(CZ, _cz_to_cps, _cz_to_cnot) add_decomps("Adjoint(CZ)", self_adjoint) -add_decomps("Pow(CZ)", pow_of_self_adjoint) +add_decomps("Pow(CZ)", pow_involutory) class CSWAP(ControlledOp): @@ -742,7 +742,7 @@ def _cswap(wires: WiresLike, **__): add_decomps(CSWAP, _cswap) add_decomps("Adjoint(CSWAP)", self_adjoint) -add_decomps("Pow(CSWAP)", pow_of_self_adjoint) +add_decomps("Pow(CSWAP)", pow_involutory) class CCZ(ControlledOp): @@ -940,7 +940,7 @@ def _ccz(wires: WiresLike, **__): add_decomps(CCZ, _ccz) add_decomps("Adjoint(CCZ)", self_adjoint) -add_decomps("Pow(CCZ)", pow_of_self_adjoint) +add_decomps("Pow(CCZ)", pow_involutory) class CNOT(ControlledOp): @@ -1072,7 +1072,7 @@ def _cnot_to_cz_h(wires: WiresLike, **__): add_decomps(CNOT, _cnot_to_cz_h) add_decomps("Adjoint(CNOT)", self_adjoint) -add_decomps("Pow(CNOT)", pow_of_self_adjoint) +add_decomps("Pow(CNOT)", pow_involutory) class Toffoli(ControlledOp): @@ -1286,7 +1286,7 @@ def _toffoli(wires: WiresLike, **__): add_decomps(Toffoli, _toffoli) add_decomps("Adjoint(Toffoli)", self_adjoint) -add_decomps("Pow(Toffoli)", pow_of_self_adjoint) +add_decomps("Pow(Toffoli)", pow_involutory) class MultiControlledX(ControlledOp): @@ -1566,7 +1566,7 @@ def decomposition(self): add_decomps("Adjoint(MultiControlledX)", self_adjoint) -add_decomps("Pow(MultiControlledX)", pow_of_self_adjoint) +add_decomps("Pow(MultiControlledX)", pow_involutory) class CRX(ControlledOp): diff --git a/pennylane/ops/qubit/arithmetic_ops.py b/pennylane/ops/qubit/arithmetic_ops.py index 67f429ed620..63539464021 100644 --- a/pennylane/ops/qubit/arithmetic_ops.py +++ b/pennylane/ops/qubit/arithmetic_ops.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane.decomposition import add_decomps, register_resources -from pennylane.decomposition.symbolic_decomposition import pow_of_self_adjoint, self_adjoint +from pennylane.decomposition.symbolic_decomposition import pow_involutory, self_adjoint from pennylane.operation import FlatPytree, Operation from pennylane.ops import Identity from pennylane.typing import TensorLike @@ -359,7 +359,7 @@ def _qubitsum_to_cnots(wires: WiresLike, **__): add_decomps(QubitSum, _qubitsum_to_cnots) add_decomps("Adjoint(QubitSum)", self_adjoint) -add_decomps("Pow(QubitSum)", pow_of_self_adjoint) +add_decomps("Pow(QubitSum)", pow_involutory) class IntegerComparator(Operation): diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 1dbd6828c1e..b2859aa422b 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -32,7 +32,7 @@ pow_resource_rep, register_resources, ) -from pennylane.decomposition.symbolic_decomposition import pow_of_self_adjoint, self_adjoint +from pennylane.decomposition.symbolic_decomposition import pow_involutory, self_adjoint from pennylane.operation import Operation from pennylane.typing import TensorLike from pennylane.wires import Wires, WiresLike @@ -238,7 +238,7 @@ def _hadamard_to_rz_ry(wires: WiresLike, **__): add_decomps(Hadamard, _hadamard_to_rz_rx, _hadamard_to_rz_ry) add_decomps("Adjoint(Hadamard)", self_adjoint) -add_decomps("Pow(Hadamard)", pow_of_self_adjoint) +add_decomps("Pow(Hadamard)", pow_involutory) H = Hadamard @@ -473,7 +473,7 @@ def _paulix_to_rx(wires: WiresLike, **__): add_decomps(PauliX, _paulix_to_rx) add_decomps("Adjoint(PauliX)", self_adjoint) -add_decomps("Pow(PauliX)", pow_of_self_adjoint) +add_decomps("Pow(PauliX)", pow_involutory) class PauliY(Observable, Operation): @@ -689,7 +689,7 @@ def _pauliy_to_ry_gp(wires: WiresLike, **__): add_decomps(PauliY, _pauliy_to_ry_gp) add_decomps("Adjoint(PauliY)", self_adjoint) -add_decomps("Pow(PauliY)", pow_of_self_adjoint) +add_decomps("Pow(PauliY)", pow_involutory) class PauliZ(Observable, Operation): @@ -910,7 +910,7 @@ def _pauliz_to_ps(wires: WiresLike, **__): add_decomps(PauliZ, _pauliz_to_ps) add_decomps("Adjoint(PauliZ)", self_adjoint) -add_decomps("Pow(PauliZ)", pow_of_self_adjoint) +add_decomps("Pow(PauliZ)", pow_involutory) class S(Operation): @@ -1588,7 +1588,7 @@ def _swap_to_cnot(wires, **__): add_decomps(SWAP, _swap_to_cnot) add_decomps("Adjoint(SWAP)", self_adjoint) -add_decomps("Pow(SWAP)", pow_of_self_adjoint) +add_decomps("Pow(SWAP)", pow_involutory) class ECR(Operation): @@ -1753,7 +1753,7 @@ def _ecr_decomp(wires, **__): add_decomps(ECR, _ecr_decomp) add_decomps("Adjoint(ECR)", self_adjoint) -add_decomps("Pow(ECR)", pow_of_self_adjoint) +add_decomps("Pow(ECR)", pow_involutory) class ISWAP(Operation): diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py index 1b9cd94be91..4f8fd924b03 100644 --- a/tests/decomposition/conftest.py +++ b/tests/decomposition/conftest.py @@ -23,7 +23,7 @@ from pennylane.decomposition.decomposition_rule import _auto_wrap from pennylane.decomposition.symbolic_decomposition import ( adjoint_rotation, - pow_of_self_adjoint, + pow_involutory, pow_rotation, self_adjoint, ) @@ -137,7 +137,7 @@ def _t_ps(wires, **__): ################################################ decompositions["Adjoint(Hadamard)"] = [self_adjoint] -decompositions["Pow(Hadamard)"] = [pow_of_self_adjoint] +decompositions["Pow(Hadamard)"] = [pow_involutory] decompositions["Adjoint(RX)"] = [adjoint_rotation] decompositions["Pow(RX)"] = [pow_rotation] decompositions["Adjoint(CNOT)"] = [self_adjoint] diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index 7fa87156f18..6a49a74ceff 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -31,7 +31,7 @@ flip_pow_adjoint, make_adjoint_decomp, merge_powers, - pow_of_self_adjoint, + pow_involutory, pow_rotation, repeat_pow_base, self_adjoint, @@ -258,23 +258,23 @@ def resource_params(self): op4 = qml.pow(CustomOp(wires=[0, 1, 2]), 4) with qml.queuing.AnnotatedQueue() as q: - pow_of_self_adjoint(*op1.parameters, wires=op1.wires, **op1.hyperparameters) - pow_of_self_adjoint(*op2.parameters, wires=op2.wires, **op2.hyperparameters) - pow_of_self_adjoint(*op3.parameters, wires=op3.wires, **op3.hyperparameters) - pow_of_self_adjoint(*op4.parameters, wires=op4.wires, **op4.hyperparameters) + pow_involutory(*op1.parameters, wires=op1.wires, **op1.hyperparameters) + pow_involutory(*op2.parameters, wires=op2.wires, **op2.hyperparameters) + pow_involutory(*op3.parameters, wires=op3.wires, **op3.hyperparameters) + pow_involutory(*op4.parameters, wires=op4.wires, **op4.hyperparameters) assert q.queue == [CustomOp(wires=[0, 1, 2]), CustomOp(wires=[0, 1, 2])] - assert pow_of_self_adjoint.compute_resources(**op1.resource_params) == Resources( + assert pow_involutory.compute_resources(**op1.resource_params) == Resources( {resource_rep(CustomOp): 1} ) - assert pow_of_self_adjoint.compute_resources(**op3.resource_params) == Resources( + assert pow_involutory.compute_resources(**op3.resource_params) == Resources( {resource_rep(CustomOp): 1} ) - assert pow_of_self_adjoint.compute_resources(**op2.resource_params) == Resources() - assert pow_of_self_adjoint.compute_resources(**op4.resource_params) == Resources() + assert pow_involutory.compute_resources(**op2.resource_params) == Resources() + assert pow_involutory.compute_resources(**op4.resource_params) == Resources() with pytest.raises(DecompositionNotApplicable): - pow_of_self_adjoint.compute_resources(CustomOp, {}, z=0.5) + pow_involutory.compute_resources(CustomOp, {}, z=0.5) def test_pow_rotations(self): """Tests the pow_rotations decomposition.""" From 9f776261a8ddf8cf86527d1be0671100673584c0 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Mon, 12 May 2025 14:07:24 -0400 Subject: [PATCH 32/45] apply suggestions from code review --- doc/releases/changelog-dev.md | 1 + pennylane/decomposition/symbolic_decomposition.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 69307aede42..f07e0ac2ff8 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -123,6 +123,7 @@ 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, **_): diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index 6c871f6af4d..daeef9f3b2b 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -64,7 +64,7 @@ def _adjoint_rotation(base_class, base_params, **__): # pylint: disable=protected-access,unused-argument @register_resources(_adjoint_rotation) def adjoint_rotation(phi, wires, base, **__): - """Decompose the adjoint of a rotation operator by negating the angle.""" + """Decompose the adjoint of a rotation operator by inverting the angle.""" _, struct = base._flatten() base._unflatten((-phi,), struct) From 9da982d4741ab84bb94b51ed71e3b509b0684d8b Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Mon, 12 May 2025 15:11:53 -0400 Subject: [PATCH 33/45] error message for when a symbolic operator name isn't recognized --- pennylane/decomposition/utils.py | 5 +++++ tests/decomposition/test_decomp_utils.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/pennylane/decomposition/utils.py b/pennylane/decomposition/utils.py index b911aacd90f..b7def5b1386 100644 --- a/pennylane/decomposition/utils.py +++ b/pennylane/decomposition/utils.py @@ -50,6 +50,11 @@ def translate_op_alias(op_alias): if match := re.match(r"Pow\((\w+)\)", op_alias): base_op_name = match.group(1) return f"Pow({translate_op_alias(base_op_name)})" + if match := re.match(r"(\w+)\(\w+\)", op_alias): + raise ValueError( + f"'{match.group(1)}' is not a valid name for a symbolic operator. Supported " + f'names include: "Adjoint", "C", "Controlled", "Pow".' + ) return op_alias diff --git a/tests/decomposition/test_decomp_utils.py b/tests/decomposition/test_decomp_utils.py index 48b02d7a908..abcb2769f93 100644 --- a/tests/decomposition/test_decomp_utils.py +++ b/tests/decomposition/test_decomp_utils.py @@ -66,3 +66,10 @@ def test_translate_op_alias(base_op_alias, expected_op_name): assert translate_op_alias(f"Controlled({base_op_alias})") == f"C({expected_op_name})" assert translate_op_alias(f"Adjoint({base_op_alias})") == f"Adjoint({expected_op_name})" assert translate_op_alias(f"Pow({base_op_alias})") == f"Pow({expected_op_name})" + + +def test_translate_op_error(): + """Tests that an error is raised when the symbolic operator name is not valid.""" + + with pytest.raises(ValueError, match="'Adj' is not a valid name for a symbolic operator"): + translate_op_alias("Adj(X)") From 2da187e43e1a99e72cabeb9bd523bcdd482f9d63 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 14 May 2025 14:35:29 -0400 Subject: [PATCH 34/45] update docs --- pennylane/decomposition/decomposition_graph.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index d711b545f22..aeefed83e60 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -169,9 +169,12 @@ def _get_decompositions(self, op_node: CompressedResourceOp) -> list[Decompositi 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. + # 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 ( @@ -179,9 +182,12 @@ def _get_decompositions(self, op_node: CompressedResourceOp) -> list[Decompositi and pow_rotation not in decomps and pow_involutory 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. + # 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 had 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): From 39358ab9fd87918b4024a9a3a93141647fd55513 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 15 May 2025 09:53:44 -0400 Subject: [PATCH 35/45] Update pennylane/decomposition/decomposition_graph.py Co-authored-by: Pietropaolo Frisoni --- pennylane/decomposition/decomposition_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index aeefed83e60..f1182597628 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -185,7 +185,7 @@ def _get_decompositions(self, op_node: CompressedResourceOp) -> list[Decompositi # 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 had retrieved `pow_involutory` or `pow_rotation` + # 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)) From dc939f9022345ffb74ccbbf9a0779d2efc88dc3a Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 14 May 2025 15:46:39 -0400 Subject: [PATCH 36/45] [Decomposition] Register conditions to decomposition rules --- doc/releases/changelog-dev.md | 25 ++-- pennylane/__init__.py | 1 + pennylane/decomposition/__init__.py | 4 +- .../decomposition/decomposition_graph.py | 13 +- pennylane/decomposition/decomposition_rule.py | 115 +++++++++++++++++- pennylane/decomposition/utils.py | 4 - pennylane/ops/functions/assert_valid.py | 9 +- .../decompositions/unitary_decompositions.py | 65 ++++------ .../decomposition/test_decomposition_graph.py | 14 +-- .../decomposition/test_decomposition_rule.py | 33 +++++ 10 files changed, 197 insertions(+), 86 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 83a01c32ab2..e7e0e4cbd13 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -98,33 +98,32 @@ 0: ──RX(0.00)──RY(1.57)──RX(3.14)──GlobalPhase(-1.57)─┤ ``` -* Decomposition rules can be marked as not-applicable with :class:`~.decomposition.DecompositionNotApplicable`, allowing for flexibility when creating conditional decomposition - rules based on parameters that affects the rule's resources. - [(#7211)](https://github.com/PennyLaneAI/pennylane/pull/7211) +* A :func:`~.register_condition` decorator is added that allows users to bind a condition to a + decomposition rule for when it is applicable. The condition should be a function that takes the + resource parameters of an operator as arguments and returns `True` or `False` based on whether + these parameters satisfy the condition for when this rule can be applied. ```python import pennylane as qml - from pennylane.decomposition import DecompositionNotApplicable from pennylane.math.decomposition import zyz_rotation_angles - def _zyz_resource(num_wires): - if num_wires != 1: - # This decomposition is only applicable when num_wires is 1 - raise DecompositionNotApplicable - return {qml.RZ: 2, qml.RY: 1, qml.GlobalPhase: 1} + # The parameters must be consistent with ``qml.QubitUnitary.resource_keys`` + def _zyz_condition(num_wires): + return num_wires == 1 - @qml.register_resources(_zyz_resource) + @qml.register_condition(_zyz_condition) + @qml.register_resources({qml.RZ: 2, qml.RY: 1, qml.GlobalPhase: 1}) def zyz_decomposition(U, wires, **__): + # Assumes that U is a 2x2 unitary matrix phi, theta, omega, phase = zyz_rotation_angles(U, return_global_phase=True) qml.RZ(phi, wires=wires[0]) qml.RY(theta, wires=wires[0]) qml.RZ(omega, wires=wires[0]) qml.GlobalPhase(-phase) - qml.add_decomps(QubitUnitary, zyz_decomposition) + # This decomposition will be ignored for `QubitUnitary` on more than one wire. + qml.add_decomps(qml.QubitUnitary, zyz_decomposition) ``` - - This decomposition will be ignored for `QubitUnitary` on more than one wire. * The :func:`~.transforms.decompose` transform now supports symbolic operators (e.g., `Adjoint` and `Controlled`) specified as strings in the `gate_set` argument when the new graph-based decomposition system is enabled. diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 4c47482fe38..28da4ed253a 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -33,6 +33,7 @@ import pennylane.decomposition from pennylane.decomposition import ( register_resources, + register_condition, add_decomps, list_decomps, resource_rep, diff --git a/pennylane/decomposition/__init__.py b/pennylane/decomposition/__init__.py index a0c4459d4e9..055e916603e 100644 --- a/pennylane/decomposition/__init__.py +++ b/pennylane/decomposition/__init__.py @@ -58,6 +58,7 @@ :toctree: api ~register_resources + ~register_condition ~resource_rep ~controlled_resource_rep ~adjoint_resource_rep @@ -243,13 +244,11 @@ def circuit(): :toctree: api ~DecompositionError - ~DecompositionNotApplicable """ from .utils import ( DecompositionError, - DecompositionNotApplicable, enable_graph, disable_graph, enabled_graph, @@ -265,6 +264,7 @@ def circuit(): ) from .decomposition_rule import ( register_resources, + register_condition, DecompositionRule, add_decomps, list_decomps, diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index cdbbba327f7..013da02d935 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -52,7 +52,7 @@ same_type_adjoint_decomp, same_type_adjoint_ops, ) -from .utils import DecompositionError, DecompositionNotApplicable, translate_op_alias +from .utils import DecompositionError, translate_op_alias class DecompositionGraph: # pylint: disable=too-many-instance-attributes @@ -201,12 +201,11 @@ def _add_decomp_rule_to_op( self, rule: DecompositionRule, op_node: CompressedResourceOp, op_node_idx: int ): """Adds a special 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) - except DecompositionNotApplicable: - pass # ignore decompositions that are not applicable to the given op params. + 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) def _add_adjoint_decomp_node(self, op_node: CompressedResourceOp, op_node_idx: int) -> int: """Adds an adjoint decomposition node.""" diff --git a/pennylane/decomposition/decomposition_rule.py b/pennylane/decomposition/decomposition_rule.py index 9d1f79b24a3..92ec6cffb2f 100644 --- a/pennylane/decomposition/decomposition_rule.py +++ b/pennylane/decomposition/decomposition_rule.py @@ -26,6 +26,74 @@ from .resources import CompressedResourceOp, Resources, resource_rep +@overload +def register_condition(condition: Callable) -> Callable[[Callable], DecompositionRule]: ... +@overload +def register_condition(condition: Callable, qfunc: Callable) -> DecompositionRule: ... +def register_condition( + condition: Callable[..., bool], qfunc: Optional[Callable] = None +) -> Callable[[Callable], DecompositionRule] | DecompositionRule: + """Binds a condition to a decomposition rule for when it is applicable. + + .. note:: + + This function is only relevant when the new experimental graph-based decomposition system + (introduced in v0.41) is enabled via :func:`~pennylane.decomposition.enable_graph`. This new way of + doing decompositions is generally more resource efficient and accommodates multiple alternative + decomposition rules for an operator. In this new system, custom decomposition rules are + defined as quantum functions, and it is currently required that every decomposition rule + declares its required resources using ``qml.register_resources``. + + Args: + condition (Callable): a function which takes the resource parameters of an operator as + arguments and returns ``True`` or ``False`` based on whether the decomposition rule + is applicable to an operator with the given resource parameters. + qfunc (Callable): the quantum function that implements the decomposition. If ``None``, + returns a decorator for acting on a function. + + Returns: + DecompositionRule: + a data structure that represents a decomposition rule, which contains a PennyLane + quantum function representing the decomposition, and its resource function. + + **Example** + + This function can be used as a decorator to bind a condition function to a quantum function + that implements a decomposition rule. + + .. code-block:: python + + import pennylane as qml + from pennylane.math.decomposition import zyz_rotation_angles + + # The parameters must be consistent with ``qml.QubitUnitary.resource_keys`` + def _zyz_condition(num_wires): + return num_wires == 1 + + @qml.register_condition(_zyz_condition) + @qml.register_resources({qml.RZ: 2, qml.RY: 1, qml.GlobalPhase: 1}) + def zyz_decomposition(U, wires, **__): + # Assumes that U is a 2x2 unitary matrix + phi, theta, omega, phase = zyz_rotation_angles(U, return_global_phase=True) + qml.RZ(phi, wires=wires[0]) + qml.RY(theta, wires=wires[0]) + qml.RZ(omega, wires=wires[0]) + qml.GlobalPhase(-phase) + + # This decomposition will be ignored for `QubitUnitary` on more than one wire. + qml.add_decomps(qml.QubitUnitary, zyz_decomposition) + + """ + + def _decorator(_qfunc) -> DecompositionRule: + if isinstance(_qfunc, DecompositionRule): + _qfunc.set_condition(condition) + return _qfunc + return DecompositionRule(_qfunc, condition=condition) + + return _decorator(qfunc) if qfunc else _decorator + + @overload def register_resources(resources: Callable | dict) -> Callable[[Callable], DecompositionRule]: ... @overload @@ -178,7 +246,10 @@ def my_resources(num_wires): """ def _decorator(_qfunc) -> DecompositionRule: - return DecompositionRule(_qfunc, resources) + if isinstance(_qfunc, DecompositionRule): + _qfunc.set_resources(resources) + return _qfunc + return DecompositionRule(_qfunc, resources=resources) return _decorator(qfunc) if qfunc else _decorator @@ -186,18 +257,32 @@ def _decorator(_qfunc) -> DecompositionRule: class DecompositionRule: # pylint: disable=too-few-public-methods """Represents a decomposition rule for an operator.""" - def __init__(self, func: Callable, resources: Callable | dict): + def __init__( + self, + func: Callable, + resources: Optional[Callable | dict] = None, + condition: Optional[Callable[..., bool]] = None, + ): + self._impl = func - self._source = inspect.getsource(func) + + try: + self._source = inspect.getsource(func) + except OSError: + # OSError is raised if the source code cannot be retrieved + self._source = "" # pragma: no cover + if isinstance(resources, dict): - def resource_fn(): + def resource_fn(*_, **__): return resources self._compute_resources = resource_fn else: self._compute_resources = resources + self._condition = condition + def __call__(self, *args, **kwargs): return self._impl(*args, **kwargs) @@ -213,6 +298,28 @@ def compute_resources(self, *args, **kwargs) -> Resources: gate_counts = {_auto_wrap(op): count for op, count in gate_counts.items() if count > 0} return Resources(gate_counts) + def is_applicable(self, *args, **kwargs) -> bool: + """Checks whether this decomposition rule is applicable.""" + if self._condition is None: + return True + return self._condition(*args, **kwargs) + + def set_condition(self, condition: Callable[..., bool]) -> None: + """Sets the condition for this decomposition rule.""" + self._condition = condition + + def set_resources(self, resources: Callable | dict) -> None: + """Sets the resources for this decomposition rule.""" + + if isinstance(resources, dict): + + def resource_fn(*_, **__): + return resources + + self._compute_resources = resource_fn + else: + self._compute_resources = resources + def _auto_wrap(op_type): """Conveniently wrap an operator type in a resource representation.""" diff --git a/pennylane/decomposition/utils.py b/pennylane/decomposition/utils.py index 3203ee217d2..12c7f7ff416 100644 --- a/pennylane/decomposition/utils.py +++ b/pennylane/decomposition/utils.py @@ -24,10 +24,6 @@ class DecompositionError(Exception): """Base class for decomposition errors.""" -class DecompositionNotApplicable(Exception): - """Exception raised when a decomposition is not applicable to the given operator.""" - - OP_NAME_ALIASES = { "X": "PauliX", "Y": "PauliY", diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index 2cdad355b93..7a18e89ef4e 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -25,7 +25,7 @@ import scipy.sparse import pennylane as qml -from pennylane.decomposition import DecompositionNotApplicable, DecompositionRule +from pennylane.decomposition import DecompositionRule from pennylane.operation import EigvalsUndefinedError @@ -116,12 +116,11 @@ def _check_decomposition_new(op, heuristic_resources=False): def _test_decomposition_rule(op, rule: DecompositionRule, heuristic_resources=False): """Tests that a decomposition rule is consistent with the operator.""" - # Test that the resource function is correct - try: - resources = rule.compute_resources(**op.resource_params) - except DecompositionNotApplicable: + if not rule.is_applicable(**op.resource_params): return + # Test that the resource function is correct + resources = rule.compute_resources(**op.resource_params) gate_counts = resources.gate_counts with qml.queuing.AnnotatedQueue() as q: diff --git a/pennylane/ops/op_math/decompositions/unitary_decompositions.py b/pennylane/ops/op_math/decompositions/unitary_decompositions.py index ee23839b38f..9c7fbd4c67c 100644 --- a/pennylane/ops/op_math/decompositions/unitary_decompositions.py +++ b/pennylane/ops/op_math/decompositions/unitary_decompositions.py @@ -20,9 +20,8 @@ import scipy.sparse as sp from pennylane import capture, compiler, math, ops, queuing -from pennylane.decomposition.decomposition_rule import DecompositionRule, register_resources +from pennylane.decomposition.decomposition_rule import register_condition, register_resources from pennylane.decomposition.resources import resource_rep -from pennylane.decomposition.utils import DecompositionNotApplicable from pennylane.math.decomposition import ( xyx_rotation_angles, xzx_rotation_angles, @@ -238,41 +237,22 @@ def two_qubit_decomposition(U, wires): return q.queue -class OneQubitUnitaryDecomposition(DecompositionRule): # pylint: disable=too-few-public-methods - """Wrapper around naive one-qubit decomposition rules that adds a global phase. +def make_one_qubit_unitary_decomposition(su2_rule, su2_resource): + """Wrapper around a naive one-qubit decomposition rule that adds a global phase.""" - Args: - su2_rule (callable): A function that implements the naive decomposition rule which - assumes that the unitary is SU(2) - su2_resource (callable): A function that returns the resources required by the naive - decomposition rule, without the GlobalPhase. - - """ + def _resource_fn(num_wires): # pylint: disable=unused-argument + return su2_resource() | {ops.GlobalPhase: 1} - def __init__(self, su2_rule, su2_resource): - self._naive_rule = su2_rule - self._naive_resources = su2_resource - super().__init__(self._get_impl(), self._get_resource_fn()) - - def _get_impl(self): - """The implementation of the decomposition rule.""" - - def _impl(U, wires, **__): - U, global_phase = math.convert_to_su2(U, return_global_phase=True) - self._naive_rule(U, wires=wires) - ops.GlobalPhase(-global_phase) + @register_condition(lambda num_wires: num_wires == 1) + @register_resources(_resource_fn) + def _impl(U, wires, **__): + if sp.issparse(U): + U = U.todense() + U, global_phase = math.convert_to_su2(U, return_global_phase=True) + su2_rule(U, wires=wires) + ops.cond(math.logical_not(math.allclose(global_phase, 0)), _global_phase)(global_phase) - return _impl - - def _get_resource_fn(self): - """The resource function of the decomposition rule.""" - - def _resource_fn(num_wires): - if num_wires != 1: - raise DecompositionNotApplicable - return self._naive_resources() | {ops.GlobalPhase: 1} - - return _resource_fn + return _impl def _su2_rot_resource(): @@ -336,11 +316,11 @@ def _su2_zxz_decomp(U, wires, **__): ops.RZ(omega, wires=wires[0]) -rot_decomp_rule = OneQubitUnitaryDecomposition(_su2_rot_decomp, _su2_rot_resource) -zyz_decomp_rule = OneQubitUnitaryDecomposition(_su2_zyz_decomp, _su2_zyz_resource) -xyx_decomp_rule = OneQubitUnitaryDecomposition(_su2_xyx_decomp, _su2_xyx_resource) -xzx_decomp_rule = OneQubitUnitaryDecomposition(_su2_xzx_decomp, _su2_xzx_resource) -zxz_decomp_rule = OneQubitUnitaryDecomposition(_su2_zxz_decomp, _su2_zxz_resource) +rot_decomp_rule = make_one_qubit_unitary_decomposition(_su2_rot_decomp, _su2_rot_resource) +zyz_decomp_rule = make_one_qubit_unitary_decomposition(_su2_zyz_decomp, _su2_zyz_resource) +xyx_decomp_rule = make_one_qubit_unitary_decomposition(_su2_xyx_decomp, _su2_xyx_resource) +xzx_decomp_rule = make_one_qubit_unitary_decomposition(_su2_xzx_decomp, _su2_xzx_resource) +zxz_decomp_rule = make_one_qubit_unitary_decomposition(_su2_zxz_decomp, _su2_zxz_resource) ################################################################################### # Developer notes: @@ -728,8 +708,6 @@ def _decompose_3_cnots(U, wires, initial_phase): def _two_qubit_resource(num_wires): """A worst-case over-estimate for the resources of two-qubit unitary decomposition.""" - if num_wires != 2: - raise DecompositionNotApplicable # Assume the 3-CNOT case. return { resource_rep(ops.QubitUnitary, num_wires=1): 4, @@ -743,6 +721,7 @@ def _two_qubit_resource(num_wires): } +@register_condition(lambda num_wires: num_wires == 2) @register_resources(_two_qubit_resource) def two_qubit_decomp_rule(U, wires, **__): """The decomposition rule for a two-qubit unitary.""" @@ -760,3 +739,7 @@ def two_qubit_decomp_rule(U, wires, **__): )(U, wires, initial_phase) total_phase = initial_phase + additional_phase ops.GlobalPhase(-total_phase) + + +def _global_phase(phase): + ops.GlobalPhase(-phase) diff --git a/tests/decomposition/test_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py index 67c777464df..63b9488329b 100644 --- a/tests/decomposition/test_decomposition_graph.py +++ b/tests/decomposition/test_decomposition_graph.py @@ -26,7 +26,6 @@ from pennylane.decomposition import ( DecompositionError, DecompositionGraph, - DecompositionNotApplicable, adjoint_resource_rep, controlled_resource_rep, pow_resource_rep, @@ -99,7 +98,7 @@ def test_graph_construction(self, _): assert len(graph2._graph.edges()) == 8 def test_graph_construction_non_applicable_rules(self, _): - """Tests rules that raise DecompositionNotApplicable are skipped.""" + """Tests rules which are not applicable are skipped.""" class CustomOp(qml.operation.Operation): # pylint: disable=too-few-public-methods """A custom op""" @@ -110,20 +109,15 @@ class CustomOp(qml.operation.Operation): # pylint: disable=too-few-public-metho def resource_params(self): return {"num_wires": len(self.wires)} - def _some_resource(num_wires): - if num_wires > 1: - raise DecompositionNotApplicable - return {qml.RZ: 1, qml.CNOT: 1} - - @qml.register_resources(_some_resource) + @qml.register_condition(lambda num_wires: num_wires == 1) + @qml.register_resources({qml.RZ: 1, qml.CNOT: 1}) def some_rule(*_, **__): raise NotImplementedError def _some_other_resource(num_wires): - if num_wires < 2: - raise DecompositionNotApplicable return {qml.RZ: 1, qml.CNOT: num_wires - 1} + @qml.register_condition(lambda num_wires: num_wires >= 2) @qml.register_resources(_some_other_resource) def some_other_rule(*_, **__): raise NotImplementedError diff --git a/tests/decomposition/test_decomposition_rule.py b/tests/decomposition/test_decomposition_rule.py index 22b251fe097..6599bd88594 100644 --- a/tests/decomposition/test_decomposition_rule.py +++ b/tests/decomposition/test_decomposition_rule.py @@ -100,6 +100,39 @@ def multi_rz_decomposition(theta, wires, **__): gate_counts={CompressedResourceOp(qml.RZ): 1, CompressedResourceOp(qml.CNOT): 4} ) + def test_decomposition_condition(self): + """Tests that the register_condition works.""" + + @qml.register_resources({qml.H: 2, qml.Toffoli: 1}) + @qml.register_condition(lambda num_wires: num_wires == 3) + def rule_1(wires, **__): + raise NotImplementedError + + assert isinstance(rule_1, DecompositionRule) + assert rule_1.is_applicable(num_wires=3) + assert not rule_1.is_applicable(num_wires=2) + assert rule_1.compute_resources(num_wires=3) == Resources( + { + CompressedResourceOp(qml.H): 2, + CompressedResourceOp(qml.Toffoli): 1, + } + ) + + @qml.register_condition(lambda num_wires: num_wires == 3) + @qml.register_resources({qml.H: 2, qml.Toffoli: 1}) + def rule_2(wires, **__): + raise NotImplementedError + + assert isinstance(rule_2, DecompositionRule) + assert rule_2.is_applicable(num_wires=3) + assert not rule_2.is_applicable(num_wires=2) + assert rule_2.compute_resources(num_wires=3) == Resources( + { + CompressedResourceOp(qml.H): 2, + CompressedResourceOp(qml.Toffoli): 1, + } + ) + def test_inspect_decomposition_rule(self): """Tests that the source code for a decomposition rule can be inspected.""" From db98d92a98589d594289a8bd58e8df4d0adb2817 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 15 May 2025 12:48:28 -0400 Subject: [PATCH 37/45] clean up adjoint and pow conditional decomposition rules --- .../decomposition/symbolic_decomposition.py | 43 ++-- pennylane/ops/qubit/non_parametric_ops.py | 217 ++++++++---------- .../test_symbolic_decomposition.py | 12 +- 3 files changed, 121 insertions(+), 151 deletions(-) diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index daeef9f3b2b..fb044d7628b 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -20,9 +20,8 @@ import pennylane as qml -from .decomposition_rule import DecompositionRule, register_resources +from .decomposition_rule import DecompositionRule, register_condition, register_resources from .resources import adjoint_resource_rep, pow_resource_rep, resource_rep -from .utils import DecompositionNotApplicable def make_adjoint_decomp(base_decomposition: DecompositionRule): @@ -74,14 +73,9 @@ def is_integer(x): return isinstance(x, int) or np.issubdtype(getattr(x, "dtype", None), np.integer) -def _repeat_pow_base_resource(base_class, base_params, z): - if (not is_integer(z)) or z < 0: - raise DecompositionNotApplicable - return {resource_rep(base_class, **base_params): z} - - # pylint: disable=protected-access,unused-argument -@register_resources(_repeat_pow_base_resource) +@register_condition(lambda z, **__: is_integer(z) and z >= 0) +@register_resources(lambda base_class, base_params, z: {resource_rep(base_class, **base_params): z}) def repeat_pow_base(*params, wires, base, z, **__): """Decompose the power of an operator by repeating the base operator. Assumes z is a non-negative integer.""" @@ -130,21 +124,30 @@ def flip_pow_adjoint(*params, wires, base, z, **__): qml.adjoint(qml.pow(base_op, z)) -def _pow_involutory_resource(base_class, base_params, z): # pylint: disable=unused-argument - if (not is_integer(z)) or z < 0: - raise DecompositionNotApplicable - return {resource_rep(base_class, **base_params): z % 2} +def make_pow_decomp_with_period(period) -> DecompositionRule: + """Make a decomposition rule for the power of an op that has a period.""" + def _resource_fn(base_class, base_params, z): + z_mod_period = z % period + if z_mod_period == 0: + return {} + if z_mod_period == 1: + return {resource_rep(base_class, **base_params): 1} + return {pow_resource_rep(base_class, base_params, z_mod_period): 1} -# pylint: disable=protected-access,unused-argument -@register_resources(_pow_involutory_resource) -def pow_involutory(*params, wires, base, z, **__): - """Decompose the power of an involutory operator, assumes z is an integer.""" + @register_condition(lambda z, **_: z % period != z) + @register_resources(_resource_fn) + def _impl(*params, wires, base, z, **__): # pylint: disable=unused-argument + z_mod_period = z % period + if z_mod_period == 1: + base._unflatten(*base._flatten()) + elif z_mod_period > 0 and z_mod_period != period: + qml.pow(base, z_mod_period) + + return _impl - def f(): - base._unflatten(*base._flatten()) - qml.cond(z % 2 == 1, f)() +pow_involutory = make_pow_decomp_with_period(2) def _pow_rotation_resource(base_class, base_params, z): # pylint: disable=unused-argument diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index b2859aa422b..f775608ddd3 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -25,14 +25,14 @@ from scipy import sparse import pennylane as qml +from pennylane import math from pennylane._deprecated_observable import Observable -from pennylane.decomposition import ( - DecompositionNotApplicable, - add_decomps, - pow_resource_rep, - register_resources, +from pennylane.decomposition import add_decomps, register_condition, register_resources +from pennylane.decomposition.symbolic_decomposition import ( + make_pow_decomp_with_period, + pow_involutory, + self_adjoint, ) -from pennylane.decomposition.symbolic_decomposition import pow_involutory, self_adjoint from pennylane.operation import Operation from pennylane.typing import TensorLike from pennylane.wires import Wires, WiresLike @@ -471,9 +471,22 @@ def _paulix_to_rx(wires: WiresLike, **__): qml.GlobalPhase(-np.pi / 2, wires=wires) +@register_condition(lambda z, **_: math.allclose(z % 2, 0.5)) +@register_resources(lambda **_: {qml.SX: 1}) +def _pow_x_to_sx(wires, **_): + qml.SX(wires=wires) + + +@register_resources(lambda **_: {qml.RX: 1, qml.GlobalPhase: 1}) +def _pow_x_to_rx(wires, z, **_): + z_mod2 = z % 2 + qml.RX(np.pi * z_mod2, wires=wires) + qml.GlobalPhase(-np.pi / 2 * z_mod2, wires=wires) + + add_decomps(PauliX, _paulix_to_rx) add_decomps("Adjoint(PauliX)", self_adjoint) -add_decomps("Pow(PauliX)", pow_involutory) +add_decomps("Pow(PauliX)", pow_involutory, _pow_x_to_rx, _pow_x_to_sx) class PauliY(Observable, Operation): @@ -687,9 +700,16 @@ def _pauliy_to_ry_gp(wires: WiresLike, **__): qml.GlobalPhase(-np.pi / 2, wires=wires) +@register_resources(lambda **_: {qml.RY: 1, qml.GlobalPhase: 1}) +def _pow_y(wires, z, **_): + z_mod2 = z % 2 + qml.RY(np.pi * z_mod2, wires=wires) + qml.GlobalPhase(-np.pi / 2 * z_mod2, wires=wires) + + add_decomps(PauliY, _pauliy_to_ry_gp) add_decomps("Adjoint(PauliY)", self_adjoint) -add_decomps("Pow(PauliY)", pow_involutory) +add_decomps("Pow(PauliY)", pow_involutory, _pow_y) class PauliZ(Observable, Operation): @@ -908,9 +928,27 @@ def _pauliz_to_ps(wires: WiresLike, **__): qml.PhaseShift(np.pi, wires=wires) +@register_condition(lambda z, **_: math.allclose(z % 2, 0.5)) +@register_resources(lambda **_: {qml.S: 1}) +def _pow_z_to_s(wires, **_): + qml.S(wires=wires) + + +@register_condition(lambda z, **_: math.allclose(z % 2, 0.25)) +@register_resources(lambda **_: {qml.T: 1}) +def _pow_z_to_t(wires, **_): + qml.T(wires=wires) + + +@register_resources(lambda **_: {qml.PhaseShift: 1}) +def _pow_z(wires, z, **_): + z_mod2 = z % 2 + qml.PhaseShift(np.pi * z_mod2, wires=wires) + + add_decomps(PauliZ, _pauliz_to_ps) add_decomps("Adjoint(PauliZ)", self_adjoint) -add_decomps("Pow(PauliZ)", pow_involutory) +add_decomps("Pow(PauliZ)", pow_involutory, _pow_z, _pow_z_to_s, _pow_z_to_t) class S(Operation): @@ -1061,31 +1099,25 @@ def _s_phaseshift(wires, **__): add_decomps(S, _s_phaseshift) -def _pow_s_resource(base_class, base_params, z): # pylint: disable=unused_argument - z_mod4 = z % 4 - if z_mod4 == 0.5: - return {T: 1} - if z_mod4 == 2: - return {Z: 1} - if z != z_mod4: - return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} - raise DecompositionNotApplicable +@register_condition(lambda z, **_: math.allclose(z % 4, 0.5)) +@register_resources(lambda **_: {qml.T: 1}) +def _pow_s_to_t(wires, **_): + qml.T(wires=wires) -@register_resources(_pow_s_resource) -def _pow_s(wires, z, **__): +@register_condition(lambda z, **_: math.allclose(z % 4, 2)) +@register_resources(lambda **_: {qml.Z: 1}) +def _pow_s_to_z(wires, **_): + qml.Z(wires=wires) + + +@register_resources(lambda **_: {qml.PhaseShift: 1}) +def _pow_s(wires, z, **_): z_mod4 = z % 4 - qml.cond( - z_mod4 == 0.5, - lambda: T(wires=wires), - lambda: qml.pow(S(wires), z_mod4), - elifs=[ - (z_mod4 == 2, lambda: Z(wires=wires)), - ], - )() + qml.PhaseShift(np.pi * z_mod4 / 2, wires=wires) -add_decomps("Pow(S)", _pow_s) +add_decomps("Pow(S)", make_pow_decomp_with_period(4), _pow_s, _pow_s_to_t, _pow_s_to_z) class T(Operation): @@ -1236,31 +1268,13 @@ def _t_phaseshift(wires, **__): add_decomps(T, _t_phaseshift) -def _pow_t_resource(base_class, base_params, z): # pylint: disable=unused-argument +@register_resources(lambda **_: {qml.PhaseShift: 1}) +def _pow_t(wires, z, **_): z_mod8 = z % 8 - if z_mod8 == 2: - return {S: 1} - if z_mod8 == 4: - return {Z: 1} - if z != z_mod8: - return {pow_resource_rep(base_class, base_params, z=z_mod8): 1} - raise DecompositionNotApplicable + qml.PhaseShift(np.pi * z_mod8 / 4, wires=wires) -@register_resources(_pow_t_resource) -def _pow_t(wires, z, **__): - z_mod8 = z % 8 - qml.cond( - z_mod8 == 2, - lambda: S(wires=wires), - lambda: qml.pow(T(wires), z_mod8), - elifs=[ - (z_mod8 == 4, lambda: Z(wires=wires)), - ], - )() - - -add_decomps("Pow(T)", _pow_t) +add_decomps("Pow(T)", make_pow_decomp_with_period(8), _pow_t) class SX(Operation): @@ -1413,26 +1427,20 @@ def _sx_to_rx(wires: WiresLike, **__): add_decomps(SX, _sx_to_rx) -def _pow_sx_to_x_resource(base_class, base_params, z): # pylint: disable=unused-argument - z_mod4 = z % 4 - if z_mod4 == 2: - return {X: 1} - if z != z_mod4: - return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} - raise DecompositionNotApplicable +@register_condition(lambda z, **_: z % 4 == 2) +@register_resources(lambda **_: {qml.X: 1}) +def _pow_sx_to_x(wires, **__): + qml.X(wires) -@register_resources(_pow_sx_to_x_resource) -def _pow_sx_to_x(*params, wires, z, base, **__): # pylint: disable=unused-argument +@register_resources(lambda **_: {qml.RX: 1, qml.GlobalPhase: 1}) +def _pow_sx(wires, z, **_): z_mod4 = z % 4 - qml.cond( - z_mod4 == 2, - lambda: X(wires=wires), - lambda: qml.pow(SX(wires), z_mod4), - )() + qml.RX(np.pi / 2 * z_mod4, wires=wires) + qml.GlobalPhase(-np.pi / 4 * z_mod4, wires=wires) -add_decomps("Pow(SX)", _pow_sx_to_x) +add_decomps("Pow(SX)", make_pow_decomp_with_period(4), _pow_sx_to_x, _pow_sx) class SWAP(Operation): @@ -1910,36 +1918,20 @@ def _iswap_decomp(wires, **__): add_decomps(ISWAP, _iswap_decomp) -def _pow_iswap_resource(base_class, base_params, z): # pylint: disable=unused-argument - z_mod4 = z % 4 - if z_mod4 == 0.5: - return {SISWAP: 1} - if z_mod4 == 2: - return {Z: 2} - if z != z_mod4: - return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} - raise DecompositionNotApplicable - - -@register_resources(_pow_iswap_resource) -def _pow_iswap_decomp(wires, z, **__): - - z_mod4 = z % 4 - - def _siswap(): - SISWAP(wires=wires) - - def _general_case(): - qml.pow(ISWAP(wires=wires), z_mod4) +@register_condition(lambda z, **_: math.allclose(z % 4, 0.5)) +@register_resources(lambda **_: {qml.SISWAP: 1}) +def _pow_iswap_to_siswap(wires, **__): + qml.SISWAP(wires=wires) - def _zz(): - qml.Z(wires[0]) - qml.Z(wires[1]) - qml.cond(z_mod4 == 0.5, _siswap, _general_case, elifs=[(z_mod4 == 2, _zz)])() +@register_condition(lambda z, **_: math.allclose(z % 4, 2)) +@register_resources(lambda **_: {qml.Z: 2}) +def _pow_iswap_to_zz(wires, **__): + qml.Z(wires=wires[0]) + qml.Z(wires=wires[1]) -add_decomps("Pow(ISWAP)", _pow_iswap_decomp) +add_decomps("Pow(ISWAP)", make_pow_decomp_with_period(4), _pow_iswap_to_zz, _pow_iswap_to_siswap) class SISWAP(Operation): @@ -2124,41 +2116,20 @@ def _siswap_decomp(wires, **__): add_decomps(SISWAP, _siswap_decomp) -def _pow_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument - z_mod8 = z % 8 - if qml.math.allclose(z_mod8, 2): - return {ISWAP: 1} - if qml.math.allclose(z_mod8, 4): - return {Z: 2} - if z != z_mod8: - return {pow_resource_rep(base_class, base_params, z=z_mod8): 1} - raise DecompositionNotApplicable - - -@register_resources(_pow_siswap_resource) -def _pow_siswap(wires, z, **__): - - z_mod8 = z % 8 - - def _iswap(): - ISWAP(wires=wires) - - def _zz(): - qml.Z(wires[0]) - qml.Z(wires[1]) +@register_condition(lambda z, **_: math.allclose(z % 8, 2)) +@register_resources(lambda **_: {qml.ISWAP: 1}) +def _pow_siswap_to_iswap(wires, z, **_): + qml.ISWAP(wires) - def _general_case(): - qml.pow(SISWAP(wires=wires), z_mod8) - qml.cond( - qml.math.allclose(z_mod8, 2), - _iswap, - _general_case, - elifs=[(qml.math.allclose(z_mod8, 4), _zz)], - )() +@register_condition(lambda z, **_: math.allclose(z % 8, 4)) +@register_resources(lambda **_: {qml.Z: 2}) +def _pow_siswap_to_zz(wires, z, **_): + qml.Z(wires=wires[0]) + qml.Z(wires=wires[1]) -add_decomps("Pow(SISWAP)", _pow_siswap) +add_decomps("Pow(SISWAP)", make_pow_decomp_with_period(8), _pow_siswap_to_zz, _pow_siswap_to_iswap) SQISW = SISWAP diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index 6a49a74ceff..91a0185d367 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -18,7 +18,6 @@ import pennylane as qml from pennylane import queuing -from pennylane.decomposition import DecompositionNotApplicable from pennylane.decomposition.resources import ( Resources, adjoint_resource_rep, @@ -208,14 +207,12 @@ def circuit(): qml.capture.disable() def test_non_integer_pow_not_applicable(self): - """Tests that DecompositionNotApplicable is raised when z isn't a positive integer.""" + """Tests that is_applicable returns False when z isn't a positive integer.""" op = qml.pow(qml.H(0), 0.5) - with pytest.raises(DecompositionNotApplicable): - repeat_pow_base.compute_resources(**op.resource_params) + assert not repeat_pow_base.is_applicable(**op.resource_params) op = qml.pow(qml.H(0), -1) - with pytest.raises(DecompositionNotApplicable): - repeat_pow_base.compute_resources(**op.resource_params) + assert not repeat_pow_base.is_applicable(**op.resource_params) def test_flip_pow_adjoint(self): """Tests the flip_pow_adjoint decomposition.""" @@ -273,8 +270,7 @@ def resource_params(self): assert pow_involutory.compute_resources(**op2.resource_params) == Resources() assert pow_involutory.compute_resources(**op4.resource_params) == Resources() - with pytest.raises(DecompositionNotApplicable): - pow_involutory.compute_resources(CustomOp, {}, z=0.5) + assert not pow_involutory.is_applicable(CustomOp, {}, z=0.5) def test_pow_rotations(self): """Tests the pow_rotations decomposition.""" From 316d4c5603f4cb48b4658e957fb1bcc2fb2cc835 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 15 May 2025 13:01:07 -0400 Subject: [PATCH 38/45] pylint --- pennylane/ops/op_math/decompositions/unitary_decompositions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pennylane/ops/op_math/decompositions/unitary_decompositions.py b/pennylane/ops/op_math/decompositions/unitary_decompositions.py index 9c7fbd4c67c..6ca088965e2 100644 --- a/pennylane/ops/op_math/decompositions/unitary_decompositions.py +++ b/pennylane/ops/op_math/decompositions/unitary_decompositions.py @@ -706,9 +706,8 @@ def _decompose_3_cnots(U, wires, initial_phase): return math.cast_like(-np.pi / 4, initial_phase) -def _two_qubit_resource(num_wires): +def _two_qubit_resource(**_): """A worst-case over-estimate for the resources of two-qubit unitary decomposition.""" - # Assume the 3-CNOT case. return { resource_rep(ops.QubitUnitary, num_wires=1): 4, ops.CNOT: 3, From 026715713a22a99d9b431d9300b81e104d0965b9 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 15 May 2025 13:26:00 -0400 Subject: [PATCH 39/45] sphinx --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 250936fa896..31aa6bfa605 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -98,7 +98,7 @@ 0: ──RX(0.00)──RY(1.57)──RX(3.14)──GlobalPhase(-1.57)─┤ ``` -* A :func:`~.register_condition` decorator is added that allows users to bind a condition to a +* A :func:`~.decomposition.register_condition` decorator is added that allows users to bind a condition to a decomposition rule for when it is applicable. The condition should be a function that takes the resource parameters of an operator as arguments and returns `True` or `False` based on whether these parameters satisfy the condition for when this rule can be applied. From 69c35c7debcbd64c9d245b799791d1704d766219 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 15 May 2025 13:29:07 -0400 Subject: [PATCH 40/45] add missing coverage --- pennylane/decomposition/decomposition_rule.py | 2 +- .../decompositions/unitary_decompositions.py | 2 -- tests/decomposition/test_decomposition_rule.py | 18 ++++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pennylane/decomposition/decomposition_rule.py b/pennylane/decomposition/decomposition_rule.py index 92ec6cffb2f..f62226e28c0 100644 --- a/pennylane/decomposition/decomposition_rule.py +++ b/pennylane/decomposition/decomposition_rule.py @@ -268,7 +268,7 @@ def __init__( try: self._source = inspect.getsource(func) - except OSError: + except OSError: # pragma: no cover # OSError is raised if the source code cannot be retrieved self._source = "" # pragma: no cover diff --git a/pennylane/ops/op_math/decompositions/unitary_decompositions.py b/pennylane/ops/op_math/decompositions/unitary_decompositions.py index 6ca088965e2..42ea3b81c06 100644 --- a/pennylane/ops/op_math/decompositions/unitary_decompositions.py +++ b/pennylane/ops/op_math/decompositions/unitary_decompositions.py @@ -246,8 +246,6 @@ def _resource_fn(num_wires): # pylint: disable=unused-argument @register_condition(lambda num_wires: num_wires == 1) @register_resources(_resource_fn) def _impl(U, wires, **__): - if sp.issparse(U): - U = U.todense() U, global_phase = math.convert_to_su2(U, return_global_phase=True) su2_rule(U, wires=wires) ops.cond(math.logical_not(math.allclose(global_phase, 0)), _global_phase)(global_phase) diff --git a/tests/decomposition/test_decomposition_rule.py b/tests/decomposition/test_decomposition_rule.py index 6599bd88594..6865313b3e2 100644 --- a/tests/decomposition/test_decomposition_rule.py +++ b/tests/decomposition/test_decomposition_rule.py @@ -133,6 +133,24 @@ def rule_2(wires, **__): } ) + def _resource_fn(**_): + return {qml.H: 2, qml.Toffoli: 1} + + @qml.register_resources(_resource_fn) + @qml.register_condition(lambda num_wires: num_wires == 3) + def rule_3(wires, **__): + raise NotImplementedError + + assert isinstance(rule_3, DecompositionRule) + assert rule_3.is_applicable(num_wires=3) + assert not rule_3.is_applicable(num_wires=2) + assert rule_3.compute_resources(num_wires=3) == Resources( + { + CompressedResourceOp(qml.H): 2, + CompressedResourceOp(qml.Toffoli): 1, + } + ) + def test_inspect_decomposition_rule(self): """Tests that the source code for a decomposition rule can be inspected.""" From ea4af0d6090112471bf63adf87ba9891ea0c812c Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 15 May 2025 13:45:35 -0400 Subject: [PATCH 41/45] something I missed --- pennylane/decomposition/symbolic_decomposition.py | 4 ++++ pennylane/ops/qubit/non_parametric_ops.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index fb044d7628b..050b0c3b807 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -27,6 +27,9 @@ def make_adjoint_decomp(base_decomposition: DecompositionRule): """Create a decomposition rule for the adjoint of a decomposition rule.""" + def _condition_fn(base_class, base_params): # pylint: disable=unused-argument + return base_decomposition.is_applicable(**base_params) + def _resource_fn(base_class, base_params): # pylint: disable=unused-argument base_resources = base_decomposition.compute_resources(**base_params) return { @@ -34,6 +37,7 @@ def _resource_fn(base_class, base_params): # pylint: disable=unused-argument for decomp_op, count in base_resources.gate_counts.items() } + @register_condition(_condition_fn) @register_resources(_resource_fn) def _impl(*params, wires, base, **__): # pylint: disable=protected-access diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index f775608ddd3..563706ddde9 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -2118,13 +2118,13 @@ def _siswap_decomp(wires, **__): @register_condition(lambda z, **_: math.allclose(z % 8, 2)) @register_resources(lambda **_: {qml.ISWAP: 1}) -def _pow_siswap_to_iswap(wires, z, **_): +def _pow_siswap_to_iswap(wires, **_): qml.ISWAP(wires) @register_condition(lambda z, **_: math.allclose(z % 8, 4)) @register_resources(lambda **_: {qml.Z: 2}) -def _pow_siswap_to_zz(wires, z, **_): +def _pow_siswap_to_zz(wires, **_): qml.Z(wires=wires[0]) qml.Z(wires=wires[1]) From d3a9075ca810b288dfb99c2fcb344024234564a5 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 15 May 2025 14:49:10 -0400 Subject: [PATCH 42/45] fix something --- pennylane/decomposition/symbolic_decomposition.py | 5 ++++- tests/decomposition/test_symbolic_decomposition.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index 050b0c3b807..c5ed8c156ce 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -131,6 +131,9 @@ def flip_pow_adjoint(*params, wires, base, z, **__): def make_pow_decomp_with_period(period) -> DecompositionRule: """Make a decomposition rule for the power of an op that has a period.""" + def _condition_fn(base_class, base_params, z): # pylint: disable=unused-argument + return z % period != z + def _resource_fn(base_class, base_params, z): z_mod_period = z % period if z_mod_period == 0: @@ -139,7 +142,7 @@ def _resource_fn(base_class, base_params, z): return {resource_rep(base_class, **base_params): 1} return {pow_resource_rep(base_class, base_params, z_mod_period): 1} - @register_condition(lambda z, **_: z % period != z) + @register_condition(_condition_fn) @register_resources(_resource_fn) def _impl(*params, wires, base, z, **__): # pylint: disable=unused-argument z_mod_period = z % period diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index 91a0185d367..0e5aaf15860 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -238,8 +238,8 @@ def resource_params(self): } ) - def test_pow_of_self_adjoint(self): - """Tests the pow_of_self_adjoint decomposition.""" + def test_pow_involutory(self): + """Tests the pow_involutory decomposition.""" class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods From 1645b6aab0ab5217043099bcc854b93c080c566b Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 15 May 2025 15:12:43 -0400 Subject: [PATCH 43/45] ooops --- .../ops/op_math/decompositions/unitary_decompositions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pennylane/ops/op_math/decompositions/unitary_decompositions.py b/pennylane/ops/op_math/decompositions/unitary_decompositions.py index 42ea3b81c06..fed0de2c9ce 100644 --- a/pennylane/ops/op_math/decompositions/unitary_decompositions.py +++ b/pennylane/ops/op_math/decompositions/unitary_decompositions.py @@ -17,7 +17,7 @@ import warnings import numpy as np -import scipy.sparse as sp +from scipy import sparse from pennylane import capture, compiler, math, ops, queuing from pennylane.decomposition.decomposition_rule import register_condition, register_resources @@ -81,7 +81,7 @@ def one_qubit_decomposition(U, wire, rotations="ZYZ", return_global_phase=False) # It's fine to convert to dense here because the matrix is 2x2, and the decomposition # only consists of single-qubit rotation gates with a scalar rotation angle. - if sp.issparse(U): + if sparse.issparse(U): U = U.todense() U, global_phase = math.convert_to_su2(U, return_global_phase=True) @@ -200,7 +200,7 @@ def two_qubit_decomposition(U, wires): stacklevel=2, ) - if sp.issparse(U): + if sparse.issparse(U): raise DecompositionUndefinedError( "two_qubit_decomposition does not accept sparse matrices." ) @@ -246,6 +246,8 @@ def _resource_fn(num_wires): # pylint: disable=unused-argument @register_condition(lambda num_wires: num_wires == 1) @register_resources(_resource_fn) def _impl(U, wires, **__): + if sparse.issparse(U): + U = U.todense() U, global_phase = math.convert_to_su2(U, return_global_phase=True) su2_rule(U, wires=wires) ops.cond(math.logical_not(math.allclose(global_phase, 0)), _global_phase)(global_phase) From e209a58248ebb14d4d6d0a8f8a420c7af8c2c78f Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 15 May 2025 16:01:17 -0400 Subject: [PATCH 44/45] missing coverage --- pennylane/ops/functions/assert_valid.py | 2 +- tests/decomposition/test_symbolic_decomposition.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index bceee674257..0191f4cc4b1 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -117,7 +117,7 @@ def _check_decomposition_new(op, heuristic_resources=False): _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) for rule in qml.list_decomps(f"Pow({type(op).__name__})"): - for z in [0.5, 2, 3, 4, 8, 9]: + for z in [0.25, 0.5, 2, 3, 4, 8, 9]: pow_op = qml.ops.Pow(op, z) _test_decomposition_rule(pow_op, rule, heuristic_resources=heuristic_resources) diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index 0e5aaf15860..4f076ae8f24 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -253,14 +253,20 @@ def resource_params(self): op2 = qml.pow(CustomOp(wires=[0, 1, 2]), 2) op3 = qml.pow(CustomOp(wires=[0, 1, 2]), 3) op4 = qml.pow(CustomOp(wires=[0, 1, 2]), 4) + op5 = qml.pow(CustomOp(wires=[0, 1, 2]), 4.5) with qml.queuing.AnnotatedQueue() as q: pow_involutory(*op1.parameters, wires=op1.wires, **op1.hyperparameters) pow_involutory(*op2.parameters, wires=op2.wires, **op2.hyperparameters) pow_involutory(*op3.parameters, wires=op3.wires, **op3.hyperparameters) pow_involutory(*op4.parameters, wires=op4.wires, **op4.hyperparameters) + pow_involutory(*op5.parameters, wires=op5.wires, **op5.hyperparameters) - assert q.queue == [CustomOp(wires=[0, 1, 2]), CustomOp(wires=[0, 1, 2])] + assert q.queue == [ + CustomOp(wires=[0, 1, 2]), + CustomOp(wires=[0, 1, 2]), + qml.pow(CustomOp(wires=[0, 1, 2]), 0.5), + ] assert pow_involutory.compute_resources(**op1.resource_params) == Resources( {resource_rep(CustomOp): 1} ) @@ -269,6 +275,9 @@ def resource_params(self): ) assert pow_involutory.compute_resources(**op2.resource_params) == Resources() assert pow_involutory.compute_resources(**op4.resource_params) == Resources() + assert pow_involutory.compute_resources(**op5.resource_params) == Resources( + {pow_resource_rep(CustomOp, {}, 0.5): 1} + ) assert not pow_involutory.is_applicable(CustomOp, {}, z=0.5) From 9b06b7c7baed21f8a70147a9fcc39eda4494eae8 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 15 May 2025 16:46:07 -0400 Subject: [PATCH 45/45] Apply suggestions from code review Co-authored-by: Pietropaolo Frisoni --- doc/releases/changelog-dev.md | 1 + pennylane/decomposition/decomposition_rule.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 31aa6bfa605..efb12248cc5 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -102,6 +102,7 @@ decomposition rule for when it is applicable. The condition should be a function that takes the resource parameters of an operator as arguments and returns `True` or `False` based on whether these parameters satisfy the condition for when this rule can be applied. + [(#7439)](https://github.com/PennyLaneAI/pennylane/pull/7439) ```python import pennylane as qml diff --git a/pennylane/decomposition/decomposition_rule.py b/pennylane/decomposition/decomposition_rule.py index f62226e28c0..73bbadc81d4 100644 --- a/pennylane/decomposition/decomposition_rule.py +++ b/pennylane/decomposition/decomposition_rule.py @@ -39,7 +39,7 @@ def register_condition( This function is only relevant when the new experimental graph-based decomposition system (introduced in v0.41) is enabled via :func:`~pennylane.decomposition.enable_graph`. This new way of - doing decompositions is generally more resource efficient and accommodates multiple alternative + performing decompositions is generally more resource-efficient and accommodates multiple alternative decomposition rules for an operator. In this new system, custom decomposition rules are defined as quantum functions, and it is currently required that every decomposition rule declares its required resources using ``qml.register_resources``.