Repository: PennyLaneAI/pennylane
Pull request #7352 (2025-04-29 18:57:49)

[Decomposition] Clean up custom logic for adjoint and pow
**Context:**

This is a follow-up PR to https://github.com/PennyLaneAI/pennylane/pull/7347

**Description of the Change:**

There was previously a lot of custom logic for retrieving decomposition rules of symbolic operators. With the infrastructure added in https://github.com/PennyLaneAI/pennylane/pull/7347, this PR is able to clean up most of it.

- We no longer hard-code a list of operators whose adjoint is an instance of the same type. Instead, each operator with a custom adjoint decomposition registers a decomposition rule for the adjoint of itself. For example, `Hadamard` was previously hardcoded as an operator that is its own adjoint, now there is a decomposition rule for `"Adjoint(Hadamard)"` which produces an instance of the base operator.
- We removed the constraint that powers of an operator must be integers. Non-integer powers can be supported by adding custom decomposition rules for `"Pow(SomeOp)"`. All operators in PL which overrides `pow` now has a custom decomposition rule registered under the power of itself.
- Defined some general stock decomposition rules that can be registered under any operator. For example, a `self_adjoint` decomposition rule is defined, and it is registered with all operators that are self-adjoint (to avoid having duplicate code)

**Related Shortcut Stories:**

[sc-90114]

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: Patch coverage is `84.57300%` with `56 lines` in your changes missing coverage. Please review.
> Project coverage is 99.54%. Comparing base [(`edff8d4`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/edff8d4925156de1215afc825053175822cb0785?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`12a52c1`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/12a52c12d0a5cab8f5ce0fe9a0d8c362995638e7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

| [Files with missing lines](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/ops/qubit/non\_parametric\_ops.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?src=pr&el=tree&filepath=pennylane%2Fops%2Fqubit%2Fnon_parametric_ops.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9ub25fcGFyYW1ldHJpY19vcHMucHk=) | 50.56% | [44 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/ops/qubit/matrix\_ops.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?src=pr&el=tree&filepath=pennylane%2Fops%2Fqubit%2Fmatrix_ops.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9tYXRyaXhfb3BzLnB5) | 82.50% | [7 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/decomposition/decomposition\_graph.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?src=pr&el=tree&filepath=pennylane%2Fdecomposition%2Fdecomposition_graph.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RlY29tcG9zaXRpb24vZGVjb21wb3NpdGlvbl9ncmFwaC5weQ==) | 84.00% | [4 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/decomposition/symbolic\_decomposition.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?src=pr&el=tree&filepath=pennylane%2Fdecomposition%2Fsymbolic_decomposition.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RlY29tcG9zaXRpb24vc3ltYm9saWNfZGVjb21wb3NpdGlvbi5weQ==) | 98.76% | [1 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |

<details><summary>Additional details and impacted files</summary>


```diff
@@                  Coverage Diff                  @@
##           symbolic-rules-01    #7352      +/-   ##
=====================================================
- Coverage              99.65%   99.54%   -0.12%     
=====================================================
  Files                    526      526              
  Lines                  50308    50540     +232     
=====================================================
+ Hits                   50133    50308     +175     
- Misses                   175      232      +57     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7352?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 7574aff80..9bde96dfb 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -156,6 +156,8 @@
     :func:`~.transforms.decompose` transform, allowing custom decomposition rules to be defined and
     registered for symbolic operators.
     [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347)
+    [(#7352)](https://github.com/PennyLaneAI/pennylane/pull/7352)
+
     ```python
     @qml.register_resources({qml.RY: 1})
     def my_adjoint_ry(phi, wires, **_):
diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py
index 393e14f26..99ee9448a 100644
--- a/pennylane/decomposition/decomposition_graph.py
+++ b/pennylane/decomposition/decomposition_graph.py
@@ -40,17 +40,19 @@ from .controlled_decomposition import (
     controlled_global_phase_decomp,
     controlled_x_decomp,
 )
-from .decomposition_rule import DecompositionRule, list_decomps
+from .decomposition_rule import DecompositionRule, list_decomps, null_decomp
 from .resources import CompressedResourceOp, Resources, resource_rep
 from .symbolic_decomposition import (
-    AdjointDecomp,
-    adjoint_controlled_decomp,
-    adjoint_pow_decomp,
+    adjoint_rotation,
     cancel_adjoint,
+    decompose_to_base,
+    flip_pow_adjoint,
+    make_adjoint_decomp,
     merge_powers,
+    pow_involutory,
+    pow_rotation,
     repeat_pow_base,
-    same_type_adjoint_decomp,
-    same_type_adjoint_ops,
+    self_adjoint,
 )
 from .utils import DecompositionError, translate_op_alias
 
@@ -162,10 +164,30 @@ class DecompositionGraph:  # pylint: disable=too-many-instance-attributes
 
         decomps = self._alt_decomps.get(op_name, []) + list_decomps(op_name)
 
-        if issubclass(op_node.op_type, qml.ops.Adjoint):
+        if (
+            issubclass(op_node.op_type, qml.ops.Adjoint)
+            and self_adjoint not in decomps
+            and adjoint_rotation not in decomps
+        ):
+            # In general, we decompose the adjoint of an operator by applying adjoint to the
+            # decompositions of the operator. However, this is not necessary if the operator
+            # is self-adjoint or if it has a single rotation angle which can be trivially
+            # inverted to obtain its adjoint. In this case, `self_adjoint` or `adjoint_rotation`
+            # would've already been retrieved as a potential decomposition rule for this
+            # operator, so there is no need to consider the general case.
             decomps.extend(self._get_adjoint_decompositions(op_node))
 
-        elif issubclass(op_node.op_type, qml.ops.Pow):
+        elif (
+            issubclass(op_node.op_type, qml.ops.Pow)
+            and pow_rotation not in decomps
+            and pow_involutory not in decomps
+        ):
+            # Similar to the adjoint case, the `_get_pow_decompositions` contains the general
+            # approach we take to decompose powers of operators. However, if the operator is
+            # involutory or if it has a single rotation angle that can be trivially multiplied
+            # with the power, we would've already retrieved `pow_involutory` or `pow_rotation`
+            # as a potential decomposition rule for this operator, so there is no need to consider
+            # the general case.
             decomps.extend(self._get_pow_decompositions(op_node))
 
         elif op_node.op_type in (qml.ops.Controlled, qml.ops.ControlledOp):
@@ -178,10 +200,10 @@ class DecompositionGraph:  # pylint: disable=too-many-instance-attributes
         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
@@ -200,55 +222,62 @@ class DecompositionGraph:  # pylint: disable=too-many-instance-attributes
             return op_node_idx
 
         for decomposition in self._get_decompositions(op_node):
-            self._add_decomp_rule_to_op(decomposition, op_node, op_node_idx)
+            self._add_decomp(decomposition, op_node, op_node_idx)
 
         return op_node_idx
 
-    def _add_decomp_rule_to_op(
-        self, rule: DecompositionRule, op_node: CompressedResourceOp, op_node_idx: int
-    ):
-        """Adds a special decomposition rule to the graph."""
+    def _add_decomp(self, rule: DecompositionRule, op_node: CompressedResourceOp, op_idx: int):
+        """Adds a decomposition rule to the graph."""
         if not rule.is_applicable(**op_node.params):
             return  # skip the decomposition rule if it is not applicable
         decomp_resource = rule.compute_resources(**op_node.params)
-        d_node_idx = self._recursively_add_decomposition_node(rule, decomp_resource)
-        self._graph.add_edge(d_node_idx, op_node_idx, 0)
+        d_node = _DecompositionNode(rule, decomp_resource)
+        d_node_idx = self._graph.add_node(d_node)
+        if not decomp_resource.gate_counts:
+            # If an operator decomposes to nothing (e.g., a Hadamard raised to a
+            # power of 2), we must still connect something to this decomposition
+            # node so that it is accounted for.
+            self._graph.add_edge(self._start, d_node_idx, 0)
+        for op in decomp_resource.gate_counts:
+            op_node_idx = self._add_op_node(op)
+            self._graph.add_edge(op_node_idx, d_node_idx, (op_node_idx, d_node_idx))
+        self._graph.add_edge(d_node_idx, op_idx, 0)
 
     def _get_adjoint_decompositions(self, op_node: CompressedResourceOp) -> list[DecompositionRule]:
-        """Retrieves a list of decomposition rules for an adjoint operator."""
+        """Gets the decomposition rules for the adjoint of an operator."""
 
         base_class, base_params = (op_node.params["base_class"], op_node.params["base_params"])
 
+        # Special case: adjoint of an adjoint cancels out
         if issubclass(base_class, qml.ops.Adjoint):
             return [cancel_adjoint]
 
-        if (
-            issubclass(base_class, qml.ops.Pow)
-            and base_params["base_class"] in same_type_adjoint_ops()
-        ):
-            return [adjoint_pow_decomp]
-
-        if base_class in same_type_adjoint_ops():
-            return [same_type_adjoint_decomp]
-
-        if (
-            issubclass(base_class, qml.ops.Controlled)
-            and base_params["base_class"] in same_type_adjoint_ops()
-        ):
-            return [adjoint_controlled_decomp]
-
-        base_rep = resource_rep(base_class, **base_params)
-        return [AdjointDecomp(base_rule) for base_rule in self._get_decompositions(base_rep)]
+        # General case: apply adjoint to each of the base op's decomposition rules.
+        base = resource_rep(base_class, **base_params)
+        return [make_adjoint_decomp(base_decomp) for base_decomp in self._get_decompositions(base)]
 
     @staticmethod
     def _get_pow_decompositions(op_node: CompressedResourceOp) -> list[DecompositionRule]:
-        """Retrieves a list of decomposition rules for a power operator."""
+        """Gets the decomposition rules for the power of an operator."""
 
         base_class = op_node.params["base_class"]
 
+        # Special case: power of zero
+        if op_node.params["z"] == 0:
+            return [null_decomp]
+
+        if op_node.params["z"] == 1:
+            return [decompose_to_base]
+
+        # Special case: power of a power
         if issubclass(base_class, qml.ops.Pow):
             return [merge_powers]
 
+        # Special case: power of an adjoint
+        if issubclass(base_class, qml.ops.Adjoint):
+            return [flip_pow_adjoint]
+
+        # General case: repeat the operator z times
         return [repeat_pow_base]
 
     def _get_controlled_decompositions(
@@ -276,28 +305,6 @@ class DecompositionGraph:  # pylint: disable=too-many-instance-attributes
         base_rep = resource_rep(base_class, **op_node.params["base_params"])
         return [ControlledBaseDecomposition(rule) for rule in self._get_decompositions(base_rep)]
 
-    def _recursively_add_decomposition_node(
-        self, rule: DecompositionRule, decomp_resource: Resources
-    ) -> int:
-        """Recursively adds a decomposition node to the graph.
-
-        A decomposition node is defined by a decomposition rule and a first-order resource estimate
-        of this decomposition as computed with resource params passed from the operator node.
-
-        """
-
-        d_node = _DecompositionNode(rule, decomp_resource)
-        d_node_idx = self._graph.add_node(d_node)
-        if not decomp_resource.gate_counts:
-            # If an operator decomposes to nothing (e.g., a Hadamard raised to a
-            # power of 2), we must still connect something to this decomposition
-            # node so that it is accounted for.
-            self._graph.add_edge(self._start, d_node_idx, 0)
-        for op in decomp_resource.gate_counts:
-            op_node_idx = self._recursively_add_op_node(op)
-            self._graph.add_edge(op_node_idx, d_node_idx, (op_node_idx, d_node_idx))
-        return d_node_idx
-
     def solve(self, lazy=True):
         """Solves the graph using the Dijkstra search algorithm.
 
diff --git a/pennylane/decomposition/decomposition_rule.py b/pennylane/decomposition/decomposition_rule.py
index d7b6e67f7..bf9cec7bb 100644
--- a/pennylane/decomposition/decomposition_rule.py
+++ b/pennylane/decomposition/decomposition_rule.py
@@ -488,3 +488,9 @@ def has_decomp(op_type: Type[Operator] | str) -> bool:
         op_type = op_type.__name__
     op_type = translate_op_alias(op_type)
     return op_type in _decompositions and len(_decompositions[op_type]) > 0
+
+
+@register_resources({})
+def null_decomp(*_, **__):
+    """A decomposition rule that does nothing."""
+    return
diff --git a/pennylane/decomposition/resources.py b/pennylane/decomposition/resources.py
index d4f71c06a..c1771562c 100644
--- a/pennylane/decomposition/resources.py
+++ b/pennylane/decomposition/resources.py
@@ -21,8 +21,6 @@ from dataclasses import dataclass, field
 from functools import cached_property
 from typing import Optional, Type
 
-import numpy as np
-
 import pennylane as qml
 from pennylane.operation import Operator
 
@@ -326,7 +324,7 @@ def controlled_resource_rep(
         num_control_wires += base_params["num_control_wires"]
         num_zero_control_values += base_params["num_zero_control_values"]
         num_work_wires += base_params["num_work_wires"]
-        base_params = base_params["base"].resource_params
+        base_params = {"num_wires": base_params["num_target_wires"]}
 
     return CompressedResourceOp(
         qml.ops.Controlled,
@@ -356,11 +354,6 @@ def adjoint_resource_rep(base_class: Type[Operator], base_params: dict = None):
     )
 
 
-def _is_integer(x):
-    """Checks if x is an integer."""
-    return isinstance(x, int) or np.issubdtype(getattr(x, "dtype", None), np.integer)
-
-
 def pow_resource_rep(base_class, base_params, z):
     """Creates a ``CompressedResourceOp`` representation of the power of an operator.
 
@@ -370,8 +363,6 @@ def pow_resource_rep(base_class, base_params, z):
         z (int or float): the power
 
     """
-    if (not qml.math.is_abstract(z)) and (not _is_integer(z) or z < 0):
-        raise NotImplementedError("Non-integer powers or negative powers are not supported yet.")
     base_resource_rep = resource_rep(base_class, **base_params)
     return CompressedResourceOp(
         qml.ops.Pow,
diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py
index 15f6fb941..c5ed8c156 100644
--- a/pennylane/decomposition/symbolic_decomposition.py
+++ b/pennylane/decomposition/symbolic_decomposition.py
@@ -16,237 +16,168 @@
 
 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 .decomposition_rule import DecompositionRule, register_condition, register_resources
 from .resources import adjoint_resource_rep, pow_resource_rep, resource_rep
 
 
-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 _condition_fn(base_class, base_params):  # pylint: disable=unused-argument
+        return base_decomposition.is_applicable(**base_params)
 
-    def _get_impl(self):
-        """The implementation of the adjoint of a gate."""
+    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 _impl(*params, wires, base, **__):
-            qml.adjoint(self._base_decomposition._impl)(  # pylint: disable=protected-access
-                *params, wires, **base.hyperparameters
-            )
+    @register_condition(_condition_fn)
+    @register_resources(_resource_fn)
+    def _impl(*params, wires, base, **__):
+        # pylint: disable=protected-access
+        qml.adjoint(base_decomposition._impl)(*params, wires=wires, **base.hyperparameters)
 
-        return _impl
+    return _impl
 
-    def _get_resource_fn(self):
-        """The resource function of the adjoint of a gate."""
 
-        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()
-            }
+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}
 
-        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."""
+    base.base._unflatten(*base.base._flatten())
 
-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()
+# pylint: disable=protected-access,unused-argument
+@register_resources(_adjoint_rotation)
+def adjoint_rotation(phi, wires, base, **__):
+    """Decompose the adjoint of a rotation operator by inverting the angle."""
+    _, struct = base._flatten()
+    base._unflatten((-phi,), 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)
 
 
-# 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."""
-    base.base._unflatten(*base.base._flatten())
+# pylint: disable=protected-access,unused-argument
+@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."""
 
+    @qml.for_loop(0, z)
+    def _loop(i):
+        base._unflatten(*base._flatten())
 
-def _adjoint_controlled_resource(base_class, base_params):
-    """Resources of the adjoint of a controlled gate whose base has adjoint."""
+    _loop()  # pylint: disable=no-value-for-parameter
 
-    num_control_wires = base_params["num_control_wires"]
-    controlled_base_class = base_params["base_class"]
 
-    # 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
-        }
+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
+    }
+
+
+# pylint: disable=protected-access,unused-argument
+@register_resources(_merge_powers_resource)
+def merge_powers(*params, wires, base, z, **__):
+    """Decompose nested powers by combining them."""
+    base_op = base.base._unflatten(*base.base._flatten())
+    qml.pow(base_op, z * base.z)
+
+
+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
+    }
+
+
+# pylint: disable=protected-access,unused-argument
+@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."""
+    base_op = base.base._unflatten(*base.base._flatten())
+    qml.adjoint(qml.pow(base_op, z))
 
-    # 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.
+
+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:
+            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}
+
+    @register_condition(_condition_fn)
+    @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
+
+
+pow_involutory = make_pow_decomp_with_period(2)
+
+
+def _pow_rotation_resource(base_class, base_params, z):  # pylint: disable=unused-argument
     return {resource_rep(base_class, **base_params): 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
-
-    """
-    qml.ctrl(
-        base.base.adjoint(),
-        control=base.control_wires,
-        control_values=base.control_values,
-        work_wires=base.work_wires,
-    )
-
-
-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(_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_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_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,
-        }
-    )
+# pylint: disable=protected-access,unused-argument
+@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()
+    base._unflatten((phi * z,), struct)
+
+
+def _decompose_to_base_resource(base_class, base_params, **__):
+    return {resource_rep(base_class, **base_params): 1}
+
+
+# pylint: disable=protected-access,unused-argument
+@register_resources(_decompose_to_base_resource)
+def decompose_to_base(*params, wires, base, **__):
+    """Decompose a symbolic operator to its base."""
+    base._unflatten(*base._flatten())
+
+
+self_adjoint: DecompositionRule = decompose_to_base
diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py
index 7a18e89ef..0191f4cc4 100644
--- a/pennylane/ops/functions/assert_valid.py
+++ b/pennylane/ops/functions/assert_valid.py
@@ -112,6 +112,15 @@ 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)
+
+    for rule in qml.list_decomps(f"Pow({type(op).__name__})"):
+        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)
+
 
 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 25285b9fc..b45aecefe 100644
--- a/pennylane/ops/identity.py
+++ b/pennylane/ops/identity.py
@@ -21,6 +21,9 @@ from typing import Sequence
 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 CVObservable, Operation, SparseMatrixUndefinedError
 from pennylane.wires import WiresLike
 
@@ -54,6 +57,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
@@ -231,6 +240,10 @@ Corresponds to the trace of the quantum state, which in exact
 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}`.
@@ -469,3 +482,7 @@ class GlobalPhase(Operation):
         # 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_ops.py b/pennylane/ops/op_math/controlled_ops.py
index c8ffe4bf1..b51ba8e24 100644
--- a/pennylane/ops/op_math/controlled_ops.py
+++ b/pennylane/ops/op_math/controlled_ops.py
@@ -24,6 +24,12 @@ from scipy.linalg import block_diag
 
 import pennylane as qml
 from pennylane.decomposition import add_decomps, register_resources
+from pennylane.decomposition.symbolic_decomposition import (
+    adjoint_rotation,
+    pow_involutory,
+    pow_rotation,
+    self_adjoint,
+)
 from pennylane.ops.qubit.parametric_ops_single_qubit import stack_last
 from pennylane.typing import TensorLike
 from pennylane.wires import Wires, WiresLike
@@ -99,7 +105,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."""
@@ -167,7 +178,7 @@ class ControlledQubitUnitary(ControlledOp):
     @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),
@@ -325,6 +336,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_involutory)
 
 
 class CY(ControlledOp):
@@ -458,6 +471,8 @@ def _cy(wires: WiresLike, **__):
 
 
 add_decomps(CY, _cy)
+add_decomps("Adjoint(CY)", self_adjoint)
+add_decomps("Pow(CY)", pow_involutory)
 
 
 class CZ(ControlledOp):
@@ -574,6 +589,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_involutory)
 
 
 class CSWAP(ControlledOp):
@@ -724,6 +741,8 @@ def _cswap(wires: WiresLike, **__):
 
 
 add_decomps(CSWAP, _cswap)
+add_decomps("Adjoint(CSWAP)", self_adjoint)
+add_decomps("Pow(CSWAP)", pow_involutory)
 
 
 class CCZ(ControlledOp):
@@ -920,6 +939,8 @@ def _ccz(wires: WiresLike, **__):
 
 
 add_decomps(CCZ, _ccz)
+add_decomps("Adjoint(CCZ)", self_adjoint)
+add_decomps("Pow(CCZ)", pow_involutory)
 
 
 class CNOT(ControlledOp):
@@ -1050,6 +1071,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_involutory)
 
 
 class Toffoli(ControlledOp):
@@ -1262,6 +1285,8 @@ def _toffoli(wires: WiresLike, **__):
 
 
 add_decomps(Toffoli, _toffoli)
+add_decomps("Adjoint(Toffoli)", self_adjoint)
+add_decomps("Pow(Toffoli)", pow_involutory)
 
 
 class MultiControlledX(ControlledOp):
@@ -1540,6 +1565,10 @@ class MultiControlledX(ControlledOp):
         )
 
 
+add_decomps("Adjoint(MultiControlledX)", self_adjoint)
+add_decomps("Pow(MultiControlledX)", pow_involutory)
+
+
 class CRX(ControlledOp):
     r"""The controlled-RX operator
 
@@ -1745,6 +1774,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):
@@ -1922,6 +1953,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):
@@ -2140,6 +2173,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):
@@ -2358,6 +2393,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.
 
@@ -2558,5 +2601,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/op_math/decompositions/unitary_decompositions.py b/pennylane/ops/op_math/decompositions/unitary_decompositions.py
index 42ea3b81c..fed0de2c9 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 make_one_qubit_unitary_decomposition(su2_rule, su2_resource):
     @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)
diff --git a/pennylane/ops/qubit/arithmetic_ops.py b/pennylane/ops/qubit/arithmetic_ops.py
index b872a5a4f..635394640 100644
--- a/pennylane/ops/qubit/arithmetic_ops.py
+++ b/pennylane/ops/qubit/arithmetic_ops.py
@@ -23,6 +23,7 @@ import numpy as np
 
 import pennylane as qml
 from pennylane.decomposition import add_decomps, register_resources
+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
@@ -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_involutory)
 
 
 class IntegerComparator(Operation):
diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py
index 23961299c..5e137d582 100644
--- a/pennylane/ops/qubit/matrix_ops.py
+++ b/pennylane/ops/qubit/matrix_ops.py
@@ -25,19 +25,10 @@ from scipy.linalg import fractional_matrix_power
 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 DecompositionUndefinedError, FlatPytree, Operation
 from pennylane.ops.op_math.decompositions.unitary_decompositions import (
     rot_decomp_rule,
@@ -361,6 +352,39 @@ add_decomps(
 )
 
 
+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.
@@ -386,6 +410,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).
@@ -582,6 +612,27 @@ class DiagonalQubitUnitary(Operation):
         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`
@@ -678,8 +729,8 @@ class BlockEncode(Operation):
                 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))
 
@@ -782,7 +833,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":
@@ -793,19 +844,24 @@ def _process_blockencode(A, subspace):
     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,
         )
@@ -813,8 +869,8 @@ def _process_blockencode(A, subspace):
 
     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 b61023404..563706ddd 100644
--- a/pennylane/ops/qubit/non_parametric_ops.py
+++ b/pennylane/ops/qubit/non_parametric_ops.py
@@ -25,8 +25,14 @@ import numpy as np
 from scipy import sparse
 
 import pennylane as qml
+from pennylane import math
 from pennylane._deprecated_observable import Observable
-from pennylane.decomposition import add_decomps, 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.operation import Operation
 from pennylane.typing import TensorLike
 from pennylane.wires import Wires, WiresLike
@@ -231,6 +237,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_involutory)
+
 
 H = Hadamard
 r"""H(wires)
@@ -452,17 +461,32 @@ Args:
 """
 
 
-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)
+@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, _pow_x_to_rx, _pow_x_to_sx)
 
 
 class PauliY(Observable, Operation):
@@ -676,7 +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, _pow_y)
 
 
 class PauliZ(Observable, Operation):
@@ -895,7 +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, _pow_z, _pow_z_to_s, _pow_z_to_t)
 
 
 class S(Operation):
@@ -1046,6 +1099,27 @@ def _s_phaseshift(wires, **__):
 add_decomps(S, _s_phaseshift)
 
 
+@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_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.PhaseShift(np.pi * z_mod4 / 2, wires=wires)
+
+
+add_decomps("Pow(S)", make_pow_decomp_with_period(4), _pow_s, _pow_s_to_t, _pow_s_to_z)
+
+
 class T(Operation):
     r"""T(wires)
     The single-qubit T gate
@@ -1194,6 +1268,15 @@ def _t_phaseshift(wires, **__):
 add_decomps(T, _t_phaseshift)
 
 
+@register_resources(lambda **_: {qml.PhaseShift: 1})
+def _pow_t(wires, z, **_):
+    z_mod8 = z % 8
+    qml.PhaseShift(np.pi * z_mod8 / 4, wires=wires)
+
+
+add_decomps("Pow(T)", make_pow_decomp_with_period(8), _pow_t)
+
+
 class SX(Operation):
     r"""SX(wires)
     The single-qubit Square-Root X operator.
@@ -1331,19 +1414,33 @@ class SX(Operation):
         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)
+
+@register_condition(lambda z, **_: z % 4 == 2)
+@register_resources(lambda **_: {qml.X: 1})
+def _pow_sx_to_x(wires, **__):
+    qml.X(wires)
+
+
+@register_resources(lambda **_: {qml.RX: 1, qml.GlobalPhase: 1})
+def _pow_sx(wires, z, **_):
+    z_mod4 = z % 4
+    qml.RX(np.pi / 2 * z_mod4, wires=wires)
+    qml.GlobalPhase(-np.pi / 4 * z_mod4, wires=wires)
+
+
+add_decomps("Pow(SX)", make_pow_decomp_with_period(4), _pow_sx_to_x, _pow_sx)
 
 
 class SWAP(Operation):
@@ -1498,6 +1595,8 @@ def _swap_to_cnot(wires, **__):
 
 
 add_decomps(SWAP, _swap_to_cnot)
+add_decomps("Adjoint(SWAP)", self_adjoint)
+add_decomps("Pow(SWAP)", pow_involutory)
 
 
 class ECR(Operation):
@@ -1661,6 +1760,8 @@ def _ecr_decomp(wires, **__):
 
 
 add_decomps(ECR, _ecr_decomp)
+add_decomps("Adjoint(ECR)", self_adjoint)
+add_decomps("Pow(ECR)", pow_involutory)
 
 
 class ISWAP(Operation):
@@ -1817,6 +1918,22 @@ def _iswap_decomp(wires, **__):
 add_decomps(ISWAP, _iswap_decomp)
 
 
+@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)
+
+
+@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)", make_pow_decomp_with_period(4), _pow_iswap_to_zz, _pow_iswap_to_siswap)
+
+
 class SISWAP(Operation):
     r"""SISWAP(wires)
     The square root of i-swap operator. Can also be accessed as ``qml.SQISW``
@@ -1998,4 +2115,21 @@ def _siswap_decomp(wires, **__):
 
 add_decomps(SISWAP, _siswap_decomp)
 
+
+@register_condition(lambda z, **_: math.allclose(z % 8, 2))
+@register_resources(lambda **_: {qml.ISWAP: 1})
+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, **_):
+    qml.Z(wires=wires[0])
+    qml.Z(wires=wires[1])
+
+
+add_decomps("Pow(SISWAP)", make_pow_decomp_with_period(8), _pow_siswap_to_zz, _pow_siswap_to_iswap)
+
+
 SQISW = SISWAP
diff --git a/pennylane/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/ops/qubit/parametric_ops_multi_qubit.py
index 602eda8b5..6b2c770cd 100644
--- a/pennylane/ops/qubit/parametric_ops_multi_qubit.py
+++ b/pennylane/ops/qubit/parametric_ops_multi_qubit.py
@@ -25,6 +25,7 @@ import numpy as np
 
 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.math.decomposition import decomp_int_to_powers_of_two
 from pennylane.operation import FlatPytree, Operation
@@ -240,6 +241,8 @@ def _multi_rz_decomposition(theta: TensorLike, wires: WiresLike, **__):
 
 
 add_decomps(MultiRZ, _multi_rz_decomposition)
+add_decomps("Adjoint(MultiRZ)", adjoint_rotation)
+add_decomps("Pow(MultiRZ)", pow_rotation)
 
 
 class PauliRot(Operation):
@@ -568,6 +571,18 @@ class PauliRot(Operation):
         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)
@@ -588,19 +603,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)
 
 
 def _ctrl_phase_shift(phi, wire, subspace, control_data):
@@ -1177,6 +1182,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):
@@ -1339,6 +1346,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):
@@ -1532,6 +1541,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):
@@ -1756,6 +1767,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):
@@ -1932,6 +1945,7 @@ 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)
 
 
 class CPhaseShift00(Operation):
@@ -2150,6 +2164,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):
@@ -2359,6 +2375,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):
@@ -2562,3 +2580,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 6f636967d..0a5b8086d 100644
--- a/pennylane/ops/qubit/parametric_ops_single_qubit.py
+++ b/pennylane/ops/qubit/parametric_ops_single_qubit.py
@@ -25,6 +25,7 @@ import scipy as sp
 
 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
@@ -175,6 +176,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 +310,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 +483,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 +674,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 +883,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 +1025,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 +1196,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 +1406,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 b06703b7c..3789c6030 100644
--- a/pennylane/ops/qubit/qchem_ops.py
+++ b/pennylane/ops/qubit/qchem_ops.py
@@ -24,6 +24,7 @@ from scipy.sparse import csr_matrix
 
 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 @@ class DoubleExcitationPlus(Operation):
         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 @@ class DoubleExcitationMinus(Operation):
         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)


Test patch:
diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py
index f57a0267e..4f8fd924b 100644
--- a/tests/decomposition/conftest.py
+++ b/tests/decomposition/conftest.py
@@ -21,6 +21,12 @@ from collections import defaultdict
 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_involutory,
+    pow_rotation,
+    self_adjoint,
+)
 
 decompositions = defaultdict(list)
 
@@ -126,22 +132,13 @@ 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_involutory]
+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_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py
index c623b8cad..96e434567 100644
--- a/tests/decomposition/test_decomposition_graph.py
+++ b/tests/decomposition/test_decomposition_graph.py
@@ -443,7 +443,7 @@ class TestControlledDecompositions:
 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])))
@@ -461,26 +461,6 @@ class TestSymbolicDecompositions:
         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."""
 
@@ -498,35 +478,6 @@ class TestSymbolicDecompositions:
         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."""
 
@@ -554,13 +505,12 @@ class TestSymbolicDecompositions:
             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:
@@ -583,12 +533,13 @@ class TestSymbolicDecompositions:
         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:
@@ -639,3 +590,77 @@ class TestSymbolicDecompositions:
         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_resources.py b/tests/decomposition/test_resources.py
index 7e9171a2f..2b87302ce 100644
--- a/tests/decomposition/test_resources.py
+++ b/tests/decomposition/test_resources.py
@@ -368,17 +368,15 @@ class TestControlledResourceRep:
     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,
             {
                 "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,
@@ -490,12 +488,3 @@ class TestSymbolicResourceRep:
 
         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)
diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py
index b704fb0a6..4f076ae8f 100644
--- a/tests/decomposition/test_symbolic_decomposition.py
+++ b/tests/decomposition/test_symbolic_decomposition.py
@@ -17,23 +17,32 @@
 import pytest
 
 import pennylane as qml
-from pennylane.decomposition.resources import Resources, pow_resource_rep
+from pennylane import queuing
+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_involutory,
+    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 +54,7 @@ class TestAdjointDecompositionRules:
         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 +75,6 @@ class TestAdjointDecompositionRules:
         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 +95,7 @@ class TestAdjointDecompositionRules:
             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 +110,59 @@ class TestAdjointDecompositionRules:
 
         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 {}
+
+        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 == [qml.pow(qml.H(0), 3)]
-        assert adjoint_pow_decomp.compute_resources(**op.resource_params) == to_resources(
-            {pow_resource_rep(qml.H, {}, 3): 1}
+        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 +174,7 @@ class TestPowDecomposition:
             {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 +185,7 @@ class TestPowDecomposition:
         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 +206,97 @@ class TestPowDecomposition:
         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 is_applicable returns False 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"):
-            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(NotImplementedError, match="Non-integer or negative powers"):
-            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."""
+
+        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_involutory(self):
+        """Tests the pow_involutory 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)
+        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]),
+            qml.pow(CustomOp(wires=[0, 1, 2]), 0.5),
+        ]
+        assert pow_involutory.compute_resources(**op1.resource_params) == Resources(
+            {resource_rep(CustomOp): 1}
+        )
+        assert pow_involutory.compute_resources(**op3.resource_params) == Resources(
+            {resource_rep(CustomOp): 1}
+        )
+        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)
+
+    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}
+        )
diff --git a/tests/ops/qubit/test_matrix_ops.py b/tests/ops/qubit/test_matrix_ops.py
index 81104fa41..8b570c53d 100644
--- a/tests/ops/qubit/test_matrix_ops.py
+++ b/tests/ops/qubit/test_matrix_ops.py
@@ -191,6 +191,32 @@ class TestQubitUnitaryCSR:
         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."""
diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py
index 772f21777..19d7d2e0c 100644
--- a/tests/ops/qubit/test_non_parametric_ops.py
+++ b/tests/ops/qubit/test_non_parametric_ops.py
@@ -841,6 +841,7 @@ period_two_ops = (
 
 
 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):