Skip to content

Commit

Permalink
Disallow taking the adjoint of fractional power operations (#5835)
Browse files Browse the repository at this point in the history
**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]
  • Loading branch information
dwierichs committed Jun 12, 2024
1 parent 45f8d4a commit 3771f24
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 6 deletions.
4 changes: 4 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion pennylane/ops/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]


Expand Down
12 changes: 11 additions & 1 deletion pennylane/ops/op_math/pow.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import pennylane as qml
from pennylane import math as qmlmath
from pennylane.operation import (
AdjointUndefinedError,
DecompositionUndefinedError,
Observable,
Operation,
Expand Down Expand Up @@ -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:
Expand Down
49 changes: 45 additions & 4 deletions tests/ops/op_math/test_pow_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand All @@ -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()

Expand Down

0 comments on commit 3771f24

Please sign in to comment.