diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 17acd824fe7..6c89bfe1c45 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -323,6 +323,10 @@

Bug fixes 🐛

+* 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 dababf87e93..bdb2d917ad8 100644 --- a/pennylane/ops/identity.py +++ b/pennylane/ops/identity.py @@ -195,7 +195,8 @@ def identity_op(*params): 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 98a724b0603..a6daafb9150 100644 --- a/pennylane/ops/op_math/pow.py +++ b/pennylane/ops/op_math/pow.py @@ -22,6 +22,7 @@ import pennylane as qml from pennylane import math as qmlmath from pennylane.operation import ( + AdjointUndefinedError, DecompositionUndefinedError, Observable, Operation, @@ -341,8 +342,17 @@ def generator(self): 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: diff --git a/tests/ops/op_math/test_pow_op.py b/tests/ops/op_math/test_pow_op.py index c1bbc07dc57..f41bf6083f6 100644 --- a/tests/ops/op_math/test_pow_op.py +++ b/tests/ops/op_math/test_pow_op.py @@ -20,7 +20,7 @@ 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 CustomOp(qml.operation.Operation): 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 CustomObs(qml.operation.Observable): @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 @@ def test_hamiltonian_base(self, power_method): 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 @@ def test_has_matrix_false(self, power_method): 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 @@ def test_pauli_rep_none_if_base_pauli_rep_none(self, power_method): 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 @@ def test_depth_property(self): 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()