Repository: PennyLaneAI/pennylane (python)

Issue:
[BUG] `qml.Adjoint` and `qml.Pow` do not commute when exponent is fractional
### Expected behavior

Taking the `qml.adjoint()` of a powered-operator, i.e. an operator resulting from `qml.pow`, returns the same result whether we use `lazy=True` or `lazy=False` in `qml.adjoint()`.

### Actual behavior

Setting the flag `lazy=False` when using a fractional power (`z`) in `qml.pow` produces a matrix with the imaginary component having the opposite sign as with `lazy=True`. The correct matrix is the one with `lazy=True`

### Additional information

_No response_

### Source code

```shell
import pennylane as qml 

power = 0.5
op = qml.Hadamard(0)
powered_op = qml.pow(op, power, lazy=True)

powered_op_adj_true = qml.adjoint(powered_op, lazy=True)
matrix_true = qml.matrix(powered_op_adj_true)

powered_op_adj_false = qml.adjoint(powered_op, lazy=False)
matrix_false = qml.matrix(powered_op_adj_false)

np.allclose(matrix_true, matrix_false)
>> False
```


### Tracebacks

_No response_

### System information

```shell
Name: PennyLane
Version: 0.36.0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /Users/emiliano.godinez/Documents/software_development/pennylane-iqm/venv/lib/python3.9/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: pennylane_iqm, PennyLane_Lightning

Platform info:           macOS-14.5-arm64-arm-64bit
Python version:          3.9.19
Numpy version:           1.26.4
Scipy version:           1.13.1
Installed devices:
- iqm.remote (pennylane-iqm-0.0.1)
- default.clifford (PennyLane-0.36.0)
- default.gaussian (PennyLane-0.36.0)
- default.mixed (PennyLane-0.36.0)
- default.qubit (PennyLane-0.36.0)
- default.qubit.autograd (PennyLane-0.36.0)
- default.qubit.jax (PennyLane-0.36.0)
- default.qubit.legacy (PennyLane-0.36.0)
- default.qubit.tf (PennyLane-0.36.0)
- default.qubit.torch (PennyLane-0.36.0)
- default.qutrit (PennyLane-0.36.0)
- default.qutrit.mixed (PennyLane-0.36.0)
- null.qubit (PennyLane-0.36.0)
- lightning.qubit (PennyLane-Lightning-0.36.0)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.

Pull request description:
Disallow taking the adjoint of fractional power operations
**Context:**
The adjoint of an integer power of an operator is the same power of the adjoint of the operator.
For fractional powers, this does not hold because of branch cuts in the power function.

**Description of the Change:**
This PR explicitly disallows computing the (eager) adjoint of a `Pow` operator with fractional power, and raises an `AdjointUndefinedError` in this case.

As a tiny side effect, this PR changes the signature of `Identity.pow` to be compatible with generalized simplification workflows where the keyword argument `z` for the power is passed around explicitly (this popped up in a simplification test of `Pow`)

NB: As usual, a lazy `Adjoint(Pow(base, z=0.2))` is still supported, just can't be evaluated/simplified if that would lead to calling the method `adjoint` of `Pow`.

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**
Fixes #5812 

[sc-65297]


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 17acd824f..6c89bfe1c 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -323,6 +323,10 @@
 
 <h3>Bug fixes 🐛</h3>
 
+* Fixes a bug where fractional powers and adjoints of operators were commuted, which is
+  not well-defined/correct in general. Adjoints of fractional powers can no longer be evaluated.
+  [(#5835)](https://github.com/PennyLaneAI/pennylane/pull/5835)
+
 * `qml.qnn.TorchLayer` now works with tuple returns.
   [(#5816)](https://github.com/PennyLaneAI/pennylane/pull/5816)
 
diff --git a/pennylane/ops/identity.py b/pennylane/ops/identity.py
index dababf87e..bdb2d917a 100644
--- a/pennylane/ops/identity.py
+++ b/pennylane/ops/identity.py
@@ -195,7 +195,8 @@ class Identity(CVObservable, Operation):
     def adjoint(self):
         return I(wires=self.wires)
 
-    def pow(self, _):
+    # pylint: disable=unused-argument
+    def pow(self, z):
         return [I(wires=self.wires)]
 
 
diff --git a/pennylane/ops/op_math/pow.py b/pennylane/ops/op_math/pow.py
index 98a724b06..a6daafb91 100644
--- a/pennylane/ops/op_math/pow.py
+++ b/pennylane/ops/op_math/pow.py
@@ -22,6 +22,7 @@ from scipy.linalg import fractional_matrix_power
 import pennylane as qml
 from pennylane import math as qmlmath
 from pennylane.operation import (
+    AdjointUndefinedError,
     DecompositionUndefinedError,
     Observable,
     Operation,
@@ -341,8 +342,17 @@ class Pow(ScalarSymbolicOp):
     def pow(self, z):
         return [Pow(base=self.base, z=self.z * z)]
 
+    # pylint: disable=arguments-renamed, invalid-overridden-method
+    @property
+    def has_adjoint(self):
+        return isinstance(self.z, int)
+
     def adjoint(self):
-        return Pow(base=qml.adjoint(self.base), z=self.z)
+        if isinstance(self.z, int):
+            return Pow(base=qml.adjoint(self.base), z=self.z)
+        raise AdjointUndefinedError(
+            "The adjoint of Pow operators only is well-defined for integer powers."
+        )
 
     def simplify(self) -> Union["Pow", Identity]:
         # try using pauli_rep:


Test patch:
diff --git a/tests/ops/op_math/test_pow_op.py b/tests/ops/op_math/test_pow_op.py
index c1bbc07dc..f41bf6083 100644
--- a/tests/ops/op_math/test_pow_op.py
+++ b/tests/ops/op_math/test_pow_op.py
@@ -20,7 +20,7 @@ import pytest
 
 import pennylane as qml
 from pennylane import numpy as np
-from pennylane.operation import DecompositionUndefinedError
+from pennylane.operation import AdjointUndefinedError, DecompositionUndefinedError
 from pennylane.ops.op_math.controlled import ControlledOp
 from pennylane.ops.op_math.pow import Pow, PowOperation
 
@@ -144,7 +144,7 @@ class TestInheritanceMixins:
         assert "control_wires" in dir(op)
 
     def test_observable(self, power_method):
-        """Test that when the base is an Observable, Adjoint will also inherit from Observable."""
+        """Test that when the base is an Observable, Pow will also inherit from Observable."""
 
         class CustomObs(qml.operation.Observable):
             num_wires = 1
@@ -166,7 +166,7 @@ class TestInheritanceMixins:
 
     @pytest.mark.usefixtures("use_legacy_opmath")
     def test_observable_legacy_opmath(self, power_method):
-        """Test that when the base is an Observable, Adjoint will also inherit from Observable."""
+        """Test that when the base is an Observable, Pow will also inherit from Observable."""
 
         class CustomObs(qml.operation.Observable):
             num_wires = 1
@@ -279,6 +279,7 @@ class TestInitialization:
         assert op.num_wires == 2
 
 
+# pylint: disable=too-many-public-methods
 @pytest.mark.parametrize("power_method", [Pow, pow_using_dunder_method, qml.pow])
 class TestProperties:
     """Test Pow properties."""
@@ -317,6 +318,26 @@ class TestProperties:
 
         assert op.has_matrix is False
 
+    @pytest.mark.parametrize("z", [-2, 3, 2])
+    def test_has_adjoint_true(self, z, power_method):
+        """Test `has_adjoint` property is true for integer powers."""
+        # Note that even if the base would have `base.has_adjoint=False`, `qml.adjoint`
+        # would succeed because it would create an `Adjoint(base)` operator.
+        base = qml.PauliX(0)
+        op: Pow = power_method(base=base, z=z)
+
+        assert op.has_adjoint is True
+
+    @pytest.mark.parametrize("z", [-2.0, 1.0, 0.32])
+    def test_has_adjoint_false(self, z, power_method):
+        """Test `has_adjoint` property is false for non-integer powers."""
+        # Note that the integer power check is a type check, so that floats like 2.
+        # are not considered to be integers.
+
+        op: Pow = power_method(base=TempOperator(wires=0), z=z)
+
+        assert op.has_adjoint is False
+
     @pytest.mark.parametrize("z", [1, 3])
     def test_has_decomposition_true_via_int(self, power_method, z):
         """Test `has_decomposition` property is true if the power is an interger."""
@@ -465,6 +486,26 @@ class TestProperties:
         op = power_method(base, z=2)
         assert op.pauli_rep is None
 
+    @pytest.mark.parametrize("z", [-2, 3, 2])
+    def test_adjoint_integer_power(self, z, power_method):
+        """Test the `adjoint` method for integer powers."""
+        base = qml.PauliX(0)
+        op: Pow = power_method(base=base, z=z)
+        adj_op = op.adjoint()
+
+        assert isinstance(adj_op, Pow)
+        assert adj_op.z is op.z
+        assert qml.equal(adj_op.base, qml.ops.Adjoint(qml.X(0)))
+
+    @pytest.mark.parametrize("z", [-2.0, 1.0, 0.32])
+    def test_adjoint_non_integer_power_raises(self, z, power_method):
+        """Test that the `adjoint` method raises and error for non-integer powers."""
+
+        base = qml.PauliX(0)
+        op: Pow = power_method(base=base, z=z)
+        with pytest.raises(AdjointUndefinedError, match="The adjoint of Pow operators"):
+            _ = op.adjoint()
+
 
 class TestSimplify:
     """Test Pow simplify method and depth property."""
@@ -476,7 +517,7 @@ class TestSimplify:
 
     def test_simplify_nested_pow_ops(self):
         """Test the simplify method with nested pow operations."""
-        pow_op = Pow(base=Pow(base=qml.adjoint(Pow(base=qml.CNOT([1, 0]), z=1.2)), z=2), z=5)
+        pow_op = Pow(base=Pow(base=qml.adjoint(Pow(base=qml.CNOT([1, 0]), z=2)), z=1.2), z=5)
         final_op = qml.Identity([1, 0])
         simplified_op = pow_op.simplify()