Repository: PennyLaneAI/pennylane
Pull request #3820 (2023-02-24 15:35:01)

Fix equal for measurements with observables, measurement process repr, Hermitian casting fix
This PR does some minor fixes on four different things.

1) Using the keyword arguments when comparing observables and eigenvalues of measurements with `qml.equal`

2) Don't request the eigenvalues to compare if the measurements contain observables in `qml.equal`.  We don't want to calculate the eigenvalues of large observables if we don't have to

3) Don't cast the input to `qml.Hermitian` with `qml.math.asarray` unless the input is a list.  This was casting autograd arrays to numpy arrays, potentially losing trainability information.

4) Include the manual eigenvalues `_eigvals` in the representation of a Measurement process when relevant.

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3820?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> :exclamation: No coverage uploaded for pull request base (`v0.29.0-rc0@a5ab2e8`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#section-missing-base-commit).
> The diff coverage is `n/a`.

```diff
@@              Coverage Diff               @@
##             v0.29.0-rc0    #3820   +/-   ##
==============================================
  Coverage               ?   99.72%           
==============================================
  Files                  ?      336           
  Lines                  ?    29463           
  Branches               ?        0           
==============================================
  Hits                   ?    29383           
  Misses                 ?       80           
  Partials               ?        0           
```



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-0.29.0.md b/doc/releases/changelog-0.29.0.md
index d684b0c22..a66b108d2 100644
--- a/doc/releases/changelog-0.29.0.md
+++ b/doc/releases/changelog-0.29.0.md
@@ -661,6 +661,10 @@
   which allows for visualizing only part of a complete circuit/QNode.
   [(#3760)](https://github.com/PennyLaneAI/pennylane/pull/3760)
 
+* The string representation of a Measurement Process now includes the `_eigvals`
+  property if it is set.
+  [(#3820)](https://github.com/PennyLaneAI/pennylane/pull/3820)
+
 <h3>Breaking changes</h3>
 
 * The argument `mode` in execution is replaced by the boolean `grad_on_execution` in the new execution pipeline.
@@ -844,6 +848,14 @@
 * Ensure that a `QNode` does not return an empty iterable.
   [(#3769)](https://github.com/PennyLaneAI/pennylane/pull/3769)
 
+* The keyword arguments for `qml.equal` now are used when comparing the observables of a 
+  Measurement Process. The eigvals of measurements are only requested if both observables are `None`,
+  saving computational effort.
+  [(#3820)](https://github.com/PennyLaneAI/pennylane/pull/3820)
+
+* Only converts input to `qml.Hermitian` to a numpy array if the input is a list.
+  [(#3820)](https://github.com/PennyLaneAI/pennylane/pull/3820)
+
 <h3>Contributors</h3>
 
 This release contains contributions from (in alphabetical order):
diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py
index 317be85a7..70a6ed046 100644
--- a/pennylane/measurements/measurements.py
+++ b/pennylane/measurements/measurements.py
@@ -288,7 +288,9 @@ class MeasurementProcess(ABC):
     def __repr__(self):
         """Representation of this class."""
         if self.obs is None:
-            return f"{self.return_type.value}(wires={self.wires.tolist()})"
+            if self._eigvals is None:
+                return f"{self.return_type.value}(wires={self.wires.tolist()})"
+            return f"{self.return_type.value}(eigvals={self._eigvals}, wires={self.wires.tolist()})"
 
         # Todo: when tape is core the return type will always be taken from the MeasurementProcess
         if getattr(self.obs, "return_type", None) is None:
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
index a8bc80dcd..d62e1e74a 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -173,7 +173,6 @@ def _equal_operators(
     atol=1e-9,
 ):
     """Default function to determine whether two Operator objects are equal."""
-
     if not isinstance(
         op2, type(op1)
     ):  # clarifies cases involving PauliX/Y/Z (Observable/Operation)
@@ -305,25 +304,45 @@ def _equal_hamiltonian(op1: Hamiltonian, op2: Observable, **kwargs):
 
 @_equal.register
 # pylint: disable=unused-argument
-def _equal_measurements(op1: MeasurementProcess, op2: MeasurementProcess, **kwargs):
+def _equal_measurements(
+    op1: MeasurementProcess,
+    op2: MeasurementProcess,
+    check_interface=True,
+    check_trainability=True,
+    rtol=1e-5,
+    atol=1e-9,
+):
     """Determine whether two MeasurementProcess objects are equal"""
 
     if op1.obs is not None and op2.obs is not None:
-        observables_match = equal(op1.obs, op2.obs)
-    # check obs equality when either one is None (False) or both are None (True)
-    else:
-        observables_match = op1.obs == op2.obs
-    wires_match = op1.wires == op2.wires
-    eigvals_match = qml.math.allequal(op1.eigvals(), op2.eigvals())
+        return equal(
+            op1.obs,
+            op2.obs,
+            check_interface=check_interface,
+            check_trainability=check_trainability,
+            rtol=rtol,
+            atol=atol,
+        )
+
+    if op1.wires != op2.wires:
+        return False
+
+    if op1.obs is None and op2.obs is None:
+        # only compare eigvals if both observables are None.
+        # Can be expensive to compute for large observables
+        if op1.eigvals() is not None and op2.eigvals() is not None:
+            return qml.math.allclose(op1.eigvals(), op2.eigvals(), rtol=rtol, atol=atol)
+
+        return op1.eigvals() is None and op2.eigvals() is None
 
-    return observables_match and wires_match and eigvals_match
+    return False
 
 
 @_equal.register
 # pylint: disable=unused-argument
 def _(op1: VnEntropyMP, op2: VnEntropyMP, **kwargs):
     """Determine whether two MeasurementProcess objects are equal"""
-    eq_m = _equal_measurements(op1, op2)
+    eq_m = _equal_measurements(op1, op2, **kwargs)
     log_base_match = op1.log_base == op2.log_base
     return eq_m and log_base_match
 
@@ -332,7 +351,7 @@ def _(op1: VnEntropyMP, op2: VnEntropyMP, **kwargs):
 # pylint: disable=unused-argument
 def _(op1: MutualInfoMP, op2: MutualInfoMP, **kwargs):
     """Determine whether two MeasurementProcess objects are equal"""
-    eq_m = _equal_measurements(op1, op2)
+    eq_m = _equal_measurements(op1, op2, **kwargs)
     log_base_match = op1.log_base == op2.log_base
     return eq_m and log_base_match
 
diff --git a/pennylane/ops/qubit/observables.py b/pennylane/ops/qubit/observables.py
index 46f4b4844..03537767f 100644
--- a/pennylane/ops/qubit/observables.py
+++ b/pennylane/ops/qubit/observables.py
@@ -67,7 +67,7 @@ class Hermitian(Observable):
     _eigs = {}
 
     def __init__(self, A, wires, do_queue=True, id=None):
-        A = qml.math.asarray(A)
+        A = np.array(A) if isinstance(A, list) else A
         if not qml.math.is_abstract(A):
             if isinstance(wires, Sequence) and not isinstance(wires, str):
                 if len(wires) == 0:


Test patch:
diff --git a/tests/measurements/test_measurements.py b/tests/measurements/test_measurements.py
index 62114b5d5..2a2a5d400 100644
--- a/tests/measurements/test_measurements.py
+++ b/tests/measurements/test_measurements.py
@@ -272,6 +272,9 @@ class TestProperties:
         expected = "probs(PauliZ(wires=['a']))"
         assert str(m) == expected
 
+        m = ProbabilityMP(eigvals=(1, 0), wires=qml.wires.Wires(0))
+        assert repr(m) == "probs(eigvals=[1 0], wires=[0])"
+
 
 class TestExpansion:
     """Test for measurement expansion"""
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
index da98188f2..6e2661f35 100644
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -23,6 +23,7 @@ import pytest
 import pennylane as qml
 from pennylane import numpy as npp
 from pennylane.measurements import ExpectationMP
+from pennylane.measurements.probs import ProbabilityMP
 from pennylane.ops.op_math import SymbolicOp, Controlled
 
 PARAMETRIZED_OPERATIONS_1P_1W = [
@@ -1139,6 +1140,82 @@ class TestEqual:
         assert not qml.equal(op1, op2)
 
 
+class TestMeasurementsEqual:
+    @pytest.mark.jax
+    def test_observables_different_interfaces(self):
+        """Check that the check_interface keyword is used when comparing observables."""
+
+        import jax
+
+        M1 = np.eye(2)
+        M2 = jax.numpy.eye(2)
+        ob1 = qml.Hermitian(M1, 0)
+        ob2 = qml.Hermitian(M2, 0)
+
+        assert not qml.equal(qml.expval(ob1), qml.expval(ob2), check_interface=True)
+        assert qml.equal(qml.expval(ob1), qml.expval(ob2), check_interface=False)
+
+    def test_observables_different_trainability(self):
+        """Check the check_trainability keyword argument affects comparisons of measurements."""
+        M1 = qml.numpy.eye(2, requires_grad=True)
+        M2 = qml.numpy.eye(2, requires_grad=False)
+
+        ob1 = qml.Hermitian(M1, 0)
+        ob2 = qml.Hermitian(M2, 0)
+
+        assert not qml.equal(qml.expval(ob1), qml.expval(ob2), check_trainability=True)
+        assert qml.equal(qml.expval(ob1), qml.expval(ob2), check_trainability=False)
+
+    def test_observables_atol(self):
+        """Check that the atol keyword argument affects comparisons of measurements."""
+        M1 = np.eye(2)
+        M2 = M1 + 1e-3
+
+        ob1 = qml.Hermitian(M1, 0)
+        ob2 = qml.Hermitian(M2, 0)
+
+        assert not qml.equal(qml.expval(ob1), qml.expval(ob2))
+        assert qml.equal(qml.expval(ob1), qml.expval(ob2), atol=1e-1)
+
+    def test_observables_rtol(self):
+        """Check rtol affects comparison of measurement observables."""
+        M1 = np.eye(2)
+        M2 = np.diag([1 + 1e-3, 1 - 1e-3])
+
+        ob1 = qml.Hermitian(M1, 0)
+        ob2 = qml.Hermitian(M2, 0)
+
+        assert not qml.equal(qml.expval(ob1), qml.expval(ob2))
+        assert qml.equal(qml.expval(ob1), qml.expval(ob2), rtol=1e-2)
+
+    def test_eigvals_atol(self):
+        """Check atol affects comparisons of eigenvalues."""
+        m1 = ProbabilityMP(eigvals=(1, 1e-3))
+        m2 = ProbabilityMP(eigvals=(1, 0))
+
+        assert not qml.equal(m1, m2)
+        assert qml.equal(m1, m2, atol=1e-2)
+
+    def test_eigvals_rtol(self):
+        """Check that rtol affects comparisons of eigenvalues."""
+        m1 = ProbabilityMP(eigvals=(1 + 1e-3, 0))
+        m2 = ProbabilityMP(eigvals=(1, 0))
+
+        assert not qml.equal(m1, m2)
+        assert qml.equal(m1, m2, rtol=1e-2)
+
+    def test_observables_equal_but_wire_order_not(self):
+        """Test that when the wire orderings are not equal but the observables are, that
+        we still get True."""
+
+        x1 = qml.PauliX(1)
+        z0 = qml.PauliZ(0)
+
+        o1 = qml.prod(x1, z0)
+        o2 = qml.prod(z0, x1)
+        assert qml.equal(qml.expval(o1), qml.expval(o2))
+
+
 class TestObservablesComparisons:
     """Tests comparisons between Hamiltonians, Tensors and PauliX/Y/Z operators"""
 
diff --git a/tests/ops/qubit/test_observables.py b/tests/ops/qubit/test_observables.py
index e41dd7790..8f2484aff 100644
--- a/tests/ops/qubit/test_observables.py
+++ b/tests/ops/qubit/test_observables.py
@@ -148,6 +148,13 @@ class TestSimpleObservables:
 class TestHermitian:
     """Test the Hermitian observable"""
 
+    def test_preserves_autograd_trainability(self):
+        """Asserts that the provided matrix is not internally converted to a vanilla numpy array."""
+        a = qml.numpy.eye(2, requires_grad=True)
+        op = qml.Hermitian(a, 0)
+        assert op.data[0] is a
+        assert qml.math.requires_grad(op.data[0])
+
     def test_hermitian_creation_exceptions(self):
         """Tests that the hermitian matrix method raises the proper errors."""
         H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)