Repository: PennyLaneAI/pennylane
Pull request #3566 (2022-12-19 15:25:24)

Remove qutrit ops use of in-place inversion
We are starting the process of getting rid of in-place inversion.  See PR #3518 .

Given the complexity of trying to remove such an intangled feature, I'm breaking removal of in-place inversion up into multiple smaller pieces. This first piece removes in-place inversion from the qutrit operations.

Instead of `TShift(0).inv()`, we should instead use `Adjoint(TShift(0))` or `Pow(TShift(0), -1)`.

@mudit2812 

codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3566?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3566](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3566?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (787afa1) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/c45ea5a24907b46bbc1ec7bf8e6e40c2b1a40d40?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (c45ea5a) will **decrease** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master    #3566      +/-   ##
==========================================
- Coverage   99.87%   99.87%   -0.01%     
==========================================
  Files         311      311              
  Lines       27903    27870      -33     
==========================================
- Hits        27867    27834      -33     
  Misses         36       36              
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3566?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/qutrit/matrix\_ops.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3566/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdXRyaXQvbWF0cml4X29wcy5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/qutrit/non\_parametric\_ops.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3566/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdXRyaXQvbm9uX3BhcmFtZXRyaWNfb3BzLnB5) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 4dcaa6a9a..fec75fc28 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -150,6 +150,11 @@
 * Limit the `numpy` version to `<1.24`.
   [(#3563)](https://github.com/PennyLaneAI/pennylane/pull/3563)
 
+
+* Removes qutrit operations use of in-place inversion in preparation for the
+  removal of in-place inversion.
+  [(#3566)](https://github.com/PennyLaneAI/pennylane/pull/3566)
+
 * Validation has been added on the `gradient_kwargs` when initializing a QNode, and if unexpected kwargs are passed,
   a `UserWarning` is raised. A list of the current expected gradient function kwargs has been added as
   `qml.gradients.SUPPORTED_GRADIENT_KWARGS`.
diff --git a/pennylane/ops/qutrit/matrix_ops.py b/pennylane/ops/qutrit/matrix_ops.py
index 705f02c8f..f9592fac4 100644
--- a/pennylane/ops/qutrit/matrix_ops.py
+++ b/pennylane/ops/qutrit/matrix_ops.py
@@ -132,8 +132,7 @@ class QutritUnitary(Operation):
         return super().pow(z)
 
     def _controlled(self, wire):
-        new_op = ControlledQutritUnitary(*self.parameters, control_wires=wire, wires=self.wires)
-        return new_op.inv() if self.inverse else new_op
+        return ControlledQutritUnitary(*self.parameters, control_wires=wire, wires=self.wires)
 
     def label(self, decimals=None, base_label=None, cache=None):
         return super().label(decimals=decimals, base_label=base_label or "U", cache=cache)
@@ -333,10 +332,9 @@ class ControlledQutritUnitary(QutritUnitary):
         ctrl_wires = self.control_wires + wire
         old_control_values = self.hyperparameters["control_values"]
         values = None if old_control_values is None else f"{old_control_values}2"
-        new_op = ControlledQutritUnitary(
+        return ControlledQutritUnitary(
             *self.parameters,
             control_wires=ctrl_wires,
             wires=self.hyperparameters["u_wires"],
             control_values=values,
         )
-        return new_op.inv() if self.inverse else new_op
diff --git a/pennylane/ops/qutrit/non_parametric_ops.py b/pennylane/ops/qutrit/non_parametric_ops.py
index da9722517..9474e10aa 100644
--- a/pennylane/ops/qutrit/non_parametric_ops.py
+++ b/pennylane/ops/qutrit/non_parametric_ops.py
@@ -102,17 +102,7 @@ class TShift(Operation):
     # TODO: Add compute_decomposition once parametric ops are added.
 
     def pow(self, z):
-        if isinstance(z, int):
-            z_mod3 = z % 3
-            if z_mod3 < 2:
-                return super().pow(z_mod3)
-            return [self.adjoint()]
-        return super().pow(z)
-
-    def adjoint(self):
-        op = TShift(wires=self.wires)
-        op.inverse = not self.inverse
-        return op
+        return super().pow(z % 3)
 
 
 class TClock(Operation):
@@ -192,17 +182,7 @@ class TClock(Operation):
     # TODO: Add compute_decomposition() once parametric ops are added.
 
     def pow(self, z):
-        if isinstance(z, int):
-            z_mod3 = z % 3
-            if z_mod3 < 2:
-                return super().pow(z_mod3)
-            return [self.adjoint()]
-        return super().pow(z)
-
-    def adjoint(self):
-        op = TClock(wires=self.wires)
-        op.inverse = not self.inverse
-        return op
+        return super().pow(z % 3)
 
 
 class TAdd(Operation):
@@ -309,17 +289,7 @@ class TAdd(Operation):
     # TODO: Add compute_decomposition() once parametric ops are added.
 
     def pow(self, z):
-        if isinstance(z, int):
-            z_mod3 = z % 3
-            if z_mod3 < 2:
-                return super().pow(z_mod3)
-            return [self.adjoint()]
-        return super().pow(z)
-
-    def adjoint(self):
-        op = TAdd(self.wires)
-        op.inverse = not self.inverse
-        return op
+        return super().pow(z % 3)
 
     @property
     def control_wires(self):
@@ -495,11 +465,7 @@ class THadamard(Operation):
     """int: Number of trainable parameters that the operator depends on."""
 
     def label(self, decimals=None, base_label=None, cache=None):
-        label = base_label or "TH"
-        if self.subspace is None and self.inverse:
-            label += "⁻¹"
-
-        return label
+        return base_label or "TH"
 
     def __init__(self, wires, subspace=None, do_queue=True):
         self._subspace = self.validate_subspace(subspace) if subspace is not None else None
@@ -565,19 +531,15 @@ class THadamard(Operation):
 
         return mat / np.sqrt(2)
 
-    def adjoint(self):
-        op = THadamard(wires=self.wires, subspace=self.subspace)
+    @property
+    def has_adjoint(self):  # pylint: disable=arguments-renamed, invalid-overridden-method
+        return self.subspace is not None
 
+    def adjoint(self):
         if self.subspace is None:
-            op.inverse = not self.inverse
-
-        return op
+            raise qml.operation.AdjointUndefinedError
+        return THadamard(wires=self.wires, subspace=self.subspace)
 
     def pow(self, z):
-        if self.subspace is not None:
-            if not isinstance(z, int):
-                return super().pow(z)
-
-            return super().pow(z % 2)
-
-        return super().pow(z)
+        new_exp = z % 4 if self.subspace is None else z % 2
+        return super().pow(new_exp)


Test patch:
diff --git a/tests/ops/qutrit/test_qutrit_matrix_ops.py b/tests/ops/qutrit/test_qutrit_matrix_ops.py
index 10466d1e7..44b126bf9 100644
--- a/tests/ops/qutrit/test_qutrit_matrix_ops.py
+++ b/tests/ops/qutrit/test_qutrit_matrix_ops.py
@@ -295,15 +295,12 @@ class TestQutritUnitary:
         assert np.allclose(res_static, expected)
         assert np.allclose(res_dynamic, expected)
 
-    @pytest.mark.parametrize("inverse", (True, False))
-    def test_controlled(self, inverse):
+    def test_controlled(self):
         """Test QutritUnitary's controlled method."""
         U = U_thadamard_01
         base = qml.QutritUnitary(U, wires=0)
-        base.inverse = inverse
 
         expected = qml.ControlledQutritUnitary(U, control_wires="a", wires=0)
-        expected.inverse = inverse
 
         out = base._controlled("a")
         assert qml.equal(out, expected)
@@ -580,16 +577,13 @@ class TestControlledQutritUnitary:
         with pytest.raises(qml.operation.PowUndefinedError):
             op.pow(0.12)
 
-    @pytest.mark.parametrize("inverse", (True, False))
-    def test_controlled(self, inverse):
+    def test_controlled(self):
         """Test the _controlled method for ControlledQutritUnitary."""
 
         U = TSWAP
 
         original = qml.ControlledQutritUnitary(U, control_wires=(0, 1), wires=[4, 2])
-        original.inverse = inverse
         expected = qml.ControlledQutritUnitary(U, control_wires=(0, 1, "a"), wires=[4, 2])
-        expected.inverse = inverse
 
         out = original._controlled("a")
         assert qml.equal(out, expected)
@@ -597,11 +591,9 @@ class TestControlledQutritUnitary:
         original = qml.ControlledQutritUnitary(
             U, control_wires=(0, 1), wires=[4, 2], control_values="01"
         )
-        original.inverse = inverse
         expected = qml.ControlledQutritUnitary(
             U, control_wires=(0, 1, "a"), wires=[4, 2], control_values="012"
         )
-        expected.inverse = inverse
 
         out = original._controlled("a")
         assert qml.equal(out, expected)
diff --git a/tests/ops/qutrit/test_qutrit_non_parametric_ops.py b/tests/ops/qutrit/test_qutrit_non_parametric_ops.py
index 8e9ccfda3..b8c10d8f9 100644
--- a/tests/ops/qutrit/test_qutrit_non_parametric_ops.py
+++ b/tests/ops/qutrit/test_qutrit_non_parametric_ops.py
@@ -141,26 +141,38 @@ no_pow_method_ops = [
 
 
 class TestPowMethod:
+    """Tests for the pow method of non-parametric qutrit operations."""
+
     @pytest.mark.parametrize("op", period_three_ops)
     @pytest.mark.parametrize("offset", (-6, -3, 0, 3, 6))
-    def test_period_three_pow(self, op, offset):
-        """Tests that ops with period == 3 behave correctly when raised to various
-        integer powers"""
+    def test_period_three_ops_pow_multiple_of_3(self, op, offset):
+        """Tests that ops with period == 3 return an empty list when raised to a
+        power that's a multiple of 3."""
 
-        # When raising to power == 0 mod 3
         assert len(op.pow(0 + offset)) == 0
 
+    @pytest.mark.parametrize("op", period_three_ops)
+    @pytest.mark.parametrize("offset", (-6, -3, 0, 3, 6))
+    def test_period_three_ops_pow_offset_1(self, op, offset):
+        """Tests that ops with a period == 3 return a queued copy of themselves when
+        raised to a power that is 1+multiple of three.
+        """
         # When raising to power == 1 mod 3
-        op_pow_1 = op.pow(1 + offset)[0]
-        assert op_pow_1.__class__ is op.__class__
-        assert np.allclose(op_pow_1.matrix(), op.matrix())
-        assert op_pow_1.inverse == False
+        with qml.queuing.AnnotatedQueue() as q:
+            op_pow_1 = op.pow(1 + offset)[0]
+
+        assert q.queue[0] is op_pow_1
+        assert qml.equal(op_pow_1, op)
+
+    @pytest.mark.parametrize("op", period_three_ops)
+    @pytest.mark.parametrize("offset", (0, 3))
+    def test_period_three_ops_pow_offset_2(self, op, offset):
+        """Tests that ops with a period ==3 raise a PowUndefinedError when raised
+        to a power that is 2+multiple of three."""
 
         # When raising to power == 2 mod 3
-        op_pow_2 = op.pow(2 + offset)[0]
-        assert op_pow_2.__class__ is op.__class__
-        assert np.allclose(op.matrix().conj().T, op_pow_2.matrix())
-        assert op_pow_2.inverse == True
+        with pytest.raises(qml.operation.PowUndefinedError):
+            op_pow_2 = op.pow(2 + offset)
 
     @pytest.mark.parametrize("op", period_three_ops + period_two_ops)
     def test_period_two_three_noninteger_power(self, op):
@@ -175,7 +187,7 @@ class TestPowMethod:
         integer powers"""
 
         assert len(op.pow(0 + offset)) == 0
-        assert op.pow(1 + offset)[0].__class__ is op.__class__
+        assert qml.equal(op.pow(1 + offset)[0], op)
 
     @pytest.mark.parametrize("op", no_pow_method_ops)
     def test_no_pow_ops(self, op):
@@ -193,23 +205,20 @@ class TestPowMethod:
 
 
 label_data = [
-    (qml.TShift(0), "TShift", "TShift⁻¹"),
-    (qml.TClock(0), "TClock", "TClock⁻¹"),
-    (qml.TAdd([0, 1]), "TAdd", "TAdd⁻¹"),
-    (qml.TSWAP([0, 1]), "TSWAP", "TSWAP"),
-    (qml.THadamard(0), "TH", "TH⁻¹"),
-    (qml.THadamard(0, subspace=[0, 1]), "TH", "TH"),
+    (qml.TShift(0), "TShift"),
+    (qml.TClock(0), "TClock"),
+    (qml.TAdd([0, 1]), "TAdd"),
+    (qml.TSWAP([0, 1]), "TSWAP"),
+    (qml.THadamard(0), "TH"),
+    (qml.THadamard(0, subspace=[0, 1]), "TH"),
 ]
 
 
-@pytest.mark.parametrize("op, label1, label2", label_data)
-def test_label_method(op, label1, label2):
+@pytest.mark.parametrize("op, label1", label_data)
+def test_label_method(op, label1):
     assert op.label() == label1
     assert op.label(decimals=2) == label1
 
-    op.inv()
-    assert op.label() == label2
-
 
 control_data = [
     (qml.TShift(0), Wires([])),
@@ -227,7 +236,7 @@ def test_control_wires(op, control_wires):
     assert op.control_wires == control_wires
 
 
-adjoint_ops = [  # ops that are not their own inverses
+no_adjoint_ops = [  # ops that are not their own inverses
     qml.TShift(wires=0),
     qml.TClock(wires=0),
     qml.TAdd(wires=[0, 1]),
@@ -242,19 +251,20 @@ involution_ops = [  # ops that are their own inverses
 ]
 
 
-@pytest.mark.parametrize("op", adjoint_ops)
+@pytest.mark.parametrize("op", no_adjoint_ops)
 def test_adjoint_method(op, tol):
-    adj_op = copy.copy(op)
-    adj_op = adj_op.adjoint()
+    """Assert that ops that are not their own inverses do not have a defined adjoint."""
+    assert not op.has_adjoint
 
-    assert adj_op.name == op.name + ".inv"
-    assert np.allclose(adj_op.matrix(), op.matrix().conj().T)
+    with pytest.raises(qml.operation.AdjointUndefinedError):
+        op.adjoint()
 
 
 @pytest.mark.parametrize("op", involution_ops)
 def test_adjoint_method_involution(op, tol):
-    adj_op = copy.copy(op)
-    adj_op = adj_op.adjoint()
+    """Assert that involution ops are their own adjoint."""
+    assert op.has_adjoint
 
-    assert adj_op.name == op.name
-    assert np.allclose(adj_op.matrix(), op.matrix())
+    adj_op = op.adjoint()
+    assert qml.equal(adj_op, op)
+    assert adj_op is not op