Repository: PennyLaneAI/pennylane
Pull request #1985 (2021-12-06 12:00:55)

[OpRefactor] Move diagonalizing_gates method to Operator
**Context:**

This will allow all operators a representation by diagonalizing gates + evals

**Description of the Change:**

Move method, update docstring of eigval methods, which allows us to delete them in the `Observable` class.


codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1985](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (a85e8b2) into [op-refactor](https://codecov.io/gh/PennyLaneAI/pennylane/commit/622c6a148b6c0655f4410c9262cddd1613967d38?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (622c6a1) will **decrease** coverage by `1.90%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@               Coverage Diff               @@
##           op-refactor    #1985      +/-   ##
===============================================
- Coverage        98.81%   96.90%   -1.91%     
===============================================
  Files              227      227              
  Lines            17299    17302       +3     
===============================================
- Hits             17094    16767     -327     
- Misses             205      535     +330     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/measure.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmUucHk=) | `99.03% <100.00%> (ø)` | |
| [pennylane/operation.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wZXJhdGlvbi5weQ==) | `96.16% <100.00%> (+0.39%)` | :arrow_up: |
| [pennylane/tape/tape.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RhcGUvdGFwZS5weQ==) | `98.94% <100.00%> (-0.01%)` | :arrow_down: |
| [pennylane/interfaces/batch/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvYmF0Y2gvamF4LnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvamF4LnB5) | `0.00% <0.00%> (-97.44%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF9qYXgucHk=) | `0.00% <0.00%> (-95.00%)` | :arrow_down: |
| [pennylane/qnn/cost.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3Fubi9jb3N0LnB5) | `42.85% <0.00%> (-57.15%)` | :arrow_down: |
| [pennylane/transforms/classical\_jacobian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvY2xhc3NpY2FsX2phY29iaWFuLnB5) | `80.00% <0.00%> (-20.00%)` | :arrow_down: |
| [pennylane/math/utils.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvdXRpbHMucHk=) | `81.81% <0.00%> (-18.19%)` | :arrow_down: |
| [pennylane/math/multi\_dispatch.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvbXVsdGlfZGlzcGF0Y2gucHk=) | `86.84% <0.00%> (-13.16%)` | :arrow_down: |
| ... and [17 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [622c6a1...a85e8b2](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1985?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

mariaschuld: @josh146 I agree, the boolean check with stopping the tape and potentially computing something is not good. Would the register/decorator idea solve this issue down the line?
mariaschuld: I don't know what I did to have it only show 4 of the 13 tests??
josh146: > Would the register/decorator idea solve this issue down the line?

Yes, but it would be a significant change 🤔 Might need some prototyping
mariaschuld: @albi3ro You happy if I merge this?

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 486294ecb..c376ec705 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -26,6 +26,12 @@
 
 The Operator class has undergone a major refactor with the following changes:
 
+* The `diagonalizing_gates()` representation has been moved to the highest-level 
+  `Operator` class and is therefore available to all subclasses. A condition 
+  `qml.operation.defines_diagonalizing_gates` has been added, which can be used 
+  in tape contexts without queueing.
+  [(#1985)](https://github.com/PennyLaneAI/pennylane/pull/1985)
+
 * A `hyperparameters` attribute was added to the operator class.
   [(#2017)](https://github.com/PennyLaneAI/pennylane/pull/2017)
 
@@ -36,4 +42,5 @@ The Operator class has undergone a major refactor with the following changes:
 
 This release contains contributions from (in alphabetical order):
 
-Olivia Di Matteo, David Wierichs
+Olivia Di Matteo, Maria Schuld, David Wierichs
+
diff --git a/pennylane/measure.py b/pennylane/measure.py
index 09507f9da..20444ac2b 100644
--- a/pennylane/measure.py
+++ b/pennylane/measure.py
@@ -440,9 +440,9 @@ def probs(wires=None, op=None):
     if isinstance(op, qml.Hamiltonian):
         raise qml.QuantumFunctionError("Hamiltonians are not supported for rotating probabilities.")
 
-    if op is not None and not hasattr(op, "diagonalizing_gates"):
+    if op is not None and not qml.operation.defines_diagonalizing_gates(op):
         raise qml.QuantumFunctionError(
-            f"{op} has not diagonalizing_gates attribute: cannot be used to rotate the probability"
+            f"{op} does not define diagonalizing gates : cannot be used to rotate the probability"
         )
 
     if wires is not None:
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 94a8d8816..ae95c5e13 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -349,8 +349,13 @@ class Operator(abc.ABC):
 
         This is a *class method* that should be defined for all
         new operations and observables that returns the eigenvalues
-        of the operator. Note that the eigenvalues are not guaranteed
-        to be in any particular order.
+        of the operator.
+
+        If :attr:`diagonalizing_gates` are specified, the order of the
+        eigenvalues needs to match the order of
+        the computational basis vectors when the observable is
+        diagonalized using these ops. Otherwise, no particular order is
+        guaranteed.
 
         This private method allows eigenvalues to be computed
         directly without instantiating the operators first.
@@ -364,7 +369,11 @@ class Operator(abc.ABC):
         **Example:**
 
         >>> qml.RZ._eigvals(0.5)
-        >>> array([0.96891242-0.24740396j, 0.96891242+0.24740396j])
+        array([0.96891242-0.24740396j, 0.96891242+0.24740396j])
+        >>> qml.PauliX(wires=0).diagonalizing_gates()
+        [Hadamard(wires=[0])]
+        >>> qml.PauliX._eigvals()
+        array([1, -1])
 
         Returns:
             array: eigenvalue representation
@@ -375,14 +384,21 @@ class Operator(abc.ABC):
     def eigvals(self):
         r"""Eigenvalues of an instantiated operator.
 
-        Note that the eigenvalues are not guaranteed to be in any
-        particular order.
+        If :attr:`diagonalizing_gates` are specified, the order of the
+        eigenvalues needs to match the order of
+        the computational basis vectors when the observable is
+        diagonalized using these ops. Otherwise, no particular order is
+        guaranteed.
 
         **Example:**
 
         >>> U = qml.RZ(0.5, wires=1)
         >>> U.eigvals
-        >>> array([0.96891242-0.24740396j, 0.96891242+0.24740396j])
+        array([0.96891242-0.24740396j, 0.96891242+0.24740396j])
+        >>> qml.PauliX(wires=0).diagonalizing_gates()
+        [Hadamard(wires=[0])]
+        >>> qml.PauliX.eigvals()
+        array([1, -1])
 
         Returns:
             array: eigvals representation
@@ -566,6 +582,22 @@ class Operator(abc.ABC):
             return self.decomposition(wires=self.wires)
         return self.decomposition(*self.parameters, wires=self.wires)
 
+    # pylint:disable=no-self-use
+    def diagonalizing_gates(self):
+        r"""Defines a partial representation of this operator as
+        an eigendecompisition.
+
+        Multiplied together, the diagonalizing gates
+        form the unitary :math:`U` in `O = U \Sigma U^{\dagger}`, while
+        :math:`\Sigma` is a diagonal matrix containing the eigenvalues.
+
+        Returns `None` if this operator does not define its diagonalizing gates.
+
+        Returns:
+            list[.Operator] or None: A list of operators.
+        """
+        return None
+
     def queue(self, context=qml.QueuingContext):
         """Append the operator to the Operator queue."""
         context.append(self)
@@ -943,54 +975,6 @@ class Observable(Operator):
     # pylint: disable=abstract-method
     return_type = None
 
-    @classmethod
-    def _eigvals(cls, *params):
-        """Eigenvalues of the observable.
-
-        The order of the eigenvalues needs to match the order of
-        the computational basis vectors when the observable is
-        diagonalized using :attr:`diagonalizing_gates`.
-
-        This is a *class method* that must be defined for all
-        new diagonal operations, that returns the eigenvalues
-        of the operator in the computational basis.
-
-        This private method allows eigenvalues to be computed
-        directly without instantiating the operators first.
-
-        To return the eigenvalues of *instantiated* operators,
-        please use the :attr:`~.Operator.eigvals` property instead.
-
-        **Example:**
-
-        >>> qml.PauliZ._eigvals()
-        >>> array([1, -1])
-
-        Returns:
-            array: eigenvalue representation
-        """
-        raise NotImplementedError
-
-    @property
-    def eigvals(self):
-        r"""Eigenvalues of an instantiated observable.
-
-        The order of the eigenvalues needs to match the order of
-        the computational basis vectors when the observable is
-        diagonalized using :attr:`diagonalizing_gates`. This is a
-        requirement for using qubit observables in quantum functions.
-
-        **Example:**
-
-        >>> U = qml.PauliZ(wires=1)
-        >>> U.eigvals
-        >>> array([1, -1])
-
-        Returns:
-            array: eigvals representation
-        """
-        return super().eigvals
-
     def __init__(self, *params, wires=None, do_queue=True, id=None):
         # extract the arguments
         if wires is None:
@@ -1110,16 +1094,6 @@ class Observable(Operator):
             return self.__add__(other.__mul__(-1))
         raise ValueError(f"Cannot subtract {type(other)} from Observable")
 
-    def diagonalizing_gates(self):
-        r"""Returns the list of operations such that they
-        diagonalize the observable in the computational basis.
-
-        Returns:
-            list(qml.Operation): A list of gates that diagonalize
-            the observable in the computational basis.
-        """
-        raise NotImplementedError
-
 
 class Tensor(Observable):
     """Container class representing tensor products of observables.
@@ -1894,3 +1868,14 @@ def is_trainable(obj):
     """Returns ``True`` if any of the parameters of an operator is trainable
     according to ``qml.math.requires_grad``."""
     return any(qml.math.requires_grad(p) for p in obj.parameters)
+
+
+@qml.BooleanFn
+def defines_diagonalizing_gates(obj):
+    """Returns ``True`` if an operator defines the diagonalizing
+    gates are defined."""
+
+    with qml.tape.stop_recording():
+        dgates = obj.diagonalizing_gates()
+
+    return dgates is not None
diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py
index 63f02cc9c..1363fbb8f 100644
--- a/pennylane/tape/tape.py
+++ b/pennylane/tape/tape.py
@@ -1007,12 +1007,11 @@ class QuantumTape(AnnotatedQueue):
         rotation_gates = []
 
         for observable in self.observables:
-            try:
-                # some observables do not have diagonalizing gates,
-                # in which case we just don't append any
+            # some observables do not have diagonalizing gates,
+            # in which case we just don't append any
+            diag = observable.diagonalizing_gates()
+            if diag is not None:
                 rotation_gates.extend(observable.diagonalizing_gates())
-            except NotImplementedError:
-                pass
 
         return rotation_gates
 


Test patch:
diff --git a/tests/test_prob.py b/tests/test_prob.py
index 0f8c292ee..16c866ba6 100644
--- a/tests/test_prob.py
+++ b/tests/test_prob.py
@@ -438,7 +438,8 @@ def test_hamiltonian_error(coeffs, obs, init_state, tol):
     "operation", [qml.SingleExcitation, qml.SingleExcitationPlus, qml.SingleExcitationMinus]
 )
 def test_generalize_prob_not_hermitian(operation):
-    """Test that the Operation or Observables has a diagonalizing_gates attribute."""
+    """Test that Operators that do not have a diagonalizing_gates representation cannot
+    be used in probability measurements."""
 
     dev = qml.device("default.qubit", wires=2)
 
@@ -450,7 +451,7 @@ def test_generalize_prob_not_hermitian(operation):
 
     with pytest.raises(
         qml.QuantumFunctionError,
-        match="has not diagonalizing_gates attribute: cannot be used to rotate the probability",
+        match="does not define diagonalizing gates : cannot be used to rotate the probability",
     ):
         circuit()