Repository: PennyLaneAI/pennylane
Pull request #5504 (2024-04-12 09:32:12)

[new opmath] Deprecation warnings and making `LinearCombination.compare` more user friendly
Removing `LinearCombination._obs_data`, which requires updating `LinearCombination.compare`. While at it, also updating the doc string to make it clear.

**Potential Drawbacks**

A comparison with an `Observable` that has no pauli_rep (i.e. involving a `Hadamard`) will not work due to https://github.com/PennyLaneAI/pennylane/blob/master/pennylane/ops/functions/equal.py#L160. I consider this a separate issue for updating qml.equal accordingly. Alternative solution is to give `Hadamard` a `pauli_rep`.

```
op1, op2 = (qml.ops.LinearCombination([1.0], [qml.Hadamard(0)]), qml.Hadamard(0))
op1, op2 = (qml.ops.LinearCombination([1.0], [qml.Hadamard(0) @ qml.Hadamard(1)]), qml.Hadamard(0) @ qml.Hadamard(1))
op1, op2 = (qml.ops.LinearCombination([1.0], [qml.Hadamard(0) @ X(1)]), qml.Hadamard(0) @ X(1))
```

[sc-59339]

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://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5504?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.66%. Comparing base [(`1089bb3`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/1089bb3448081f4fde7bc3b4b792f1abda3555ff?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`f4b0f24`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5504?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 6 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5504      +/-   ##
==========================================
- Coverage   99.66%   99.66%   -0.01%     
==========================================
  Files         402      404       +2     
  Lines       37635    37560      -75     
==========================================
- Hits        37510    37434      -76     
- Misses        125      126       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5504?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


Patch:
diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst
index 608ac2aa9..649b4dd3c 100644
--- a/doc/development/deprecations.rst
+++ b/doc/development/deprecations.rst
@@ -35,6 +35,9 @@ Pending deprecations
   - Deprecated in v0.36
   - Will be removed in v0.37
 
+New operator arithmetic deprecations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 * ``op.ops`` and ``op.coeffs`` will be deprecated in the future. Use ``op.terms()`` instead.
 
   - Added and deprecated for ``Sum`` and ``Prod`` instances in v0.35
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 01be34366..930795440 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -425,6 +425,9 @@
 * Since `default.mixed` does not support snapshots with measurements, attempting to do so will result in a `DeviceError` instead of getting the density matrix.
   [(#5416)](https://github.com/PennyLaneAI/pennylane/pull/5416)
 
+* `LinearCombination._obs_data` is removed. You can still use `LinearCombination.compare` to check mathematical equivalence between a `LinearCombination` and another operator.
+  [(#5504)](https://github.com/PennyLaneAI/pennylane/pull/5504)
+
 <h3>Deprecations 👋</h3>
 
 * `qml.load` is deprecated. Instead, please use the functions outlined in the *Importing workflows* quickstart guide, such as `qml.from_qiskit`.
diff --git a/pennylane/ops/op_math/linear_combination.py b/pennylane/ops/op_math/linear_combination.py
index e4d943f42..47a4b01ea 100644
--- a/pennylane/ops/op_math/linear_combination.py
+++ b/pennylane/ops/op_math/linear_combination.py
@@ -15,7 +15,7 @@
 LinearCombination class
 """
 # pylint: disable=too-many-arguments, protected-access, too-many-instance-attributes
-
+import warnings
 import itertools
 import numbers
 from copy import copy
@@ -324,58 +324,17 @@ class LinearCombination(Sum):
         coeffs, ops, pr = self._simplify_coeffs_ops(self.coeffs, self.ops, self.pauli_rep, cutoff)
         return LinearCombination(coeffs, ops, _pauli_rep=pr)
 
-    def _obs_data(self):
-        r"""Extracts the data from a ``LinearCombination`` and serializes it in an order-independent fashion.
-
-        This allows for comparison between ``LinearCombination``s that are equivalent, but are defined with terms and tensors
-        expressed in different orders. For example, `qml.X(0) @ qml.Z(1)` and
-        `qml.Z(1) @ qml.X(0)` are equivalent observables with different orderings.
-
-        .. Note::
-
-            In order to store the data from each term of the ``LinearCombination`` in an order-independent serialization,
-            we make use of sets. Note that all data contained within each term must be immutable, hence the use of
-            strings and frozensets.
-
-        **Example**
-
-        >>> H = qml.ops.LinearCombination([1, 1], [qml.X(0) @ qml.X(1), qml.Z(0)])
-        >>> print(H._obs_data())
-        {(1, frozenset({('Prod', <Wires = [0, 1]>, ())})),
-         (1, frozenset({('PauliZ', <Wires = [0]>, ())}))}
-        """
-        data = set()
-
-        coeffs_arr = qml.math.toarray(self.coeffs)
-        for co, op in zip(coeffs_arr, self.ops):
-            obs = op.non_identity_obs if isinstance(op, Tensor) else [op]
-            tensor = []
-            for ob in obs:
-                parameters = tuple(
-                    str(param) for param in ob.parameters
-                )  # Converts params into immutable type
-                if isinstance(ob, qml.GellMann):
-                    parameters += (ob.hyperparameters["index"],)
-                tensor.append((ob.name, ob.wires, parameters))
-            data.add((co, frozenset(tensor)))
-
-        return data
-
     def compare(self, other):
-        r"""Determines whether the operator is equivalent to another.
+        r"""Determines mathematical equivalence between operators
 
-        Currently only supported for :class:`~LinearCombination`, :class:`~.Observable`, or :class:`~.Tensor`.
-        LinearCombinations/observables are equivalent if they represent the same operator
-        (their matrix representations are equal), and they are defined on the same wires.
+        ``LinearCombination`` and other operators are equivalent if they mathematically represent the same operator
+        (their matrix representations are equal), acting on the same wires.
 
         .. Warning::
 
-            The compare method does **not** check if the matrix representation
-            of a :class:`~.Hermitian` observable is equal to an equivalent
-            observable expressed in terms of Pauli matrices, or as a
-            linear combination of Hermitians.
-            To do so would require the matrix form of LinearCombinations and Tensors
-            be calculated, which would drastically increase runtime.
+            This method does not compute explicit matrices but uses the underlyding operators and coefficients for comparisons. When both operators
+            consist purely of Pauli operators, and therefore have a valid ``op.pauli_rep``, the comparison is cheap.
+            When that is not the case (e.g. one of the operators contains a ``Hadamard`` gate), it can be more expensive as it involves mathematical simplification of both operators.
 
         Returns:
             (bool): True if equivalent.
@@ -407,16 +366,20 @@ class LinearCombination(Sum):
                 pr2.simplify()
                 return pr1 == pr2
 
-            if isinstance(other, (LinearCombination, qml.ops.Hamiltonian)):
+            if isinstance(other, (qml.ops.Hamiltonian, Tensor)):
+                warnings.warn(
+                    f"Attempting to compare a legacy operator class instance {other} of type {type(other)} with {self} of type {type(self)}."
+                    f"You are likely disabling/enabling new opmath in the same script or explicitly create legacy operator classes Tensor and ops.Hamiltonian."
+                    f"Please visit https://docs.pennylane.ai/en/latest/introduction/new_opmath.html for more information and help troubleshooting.",
+                    UserWarning,
+                )
                 op1 = self.simplify()
                 op2 = other.simplify()
-                return op1._obs_data() == op2._obs_data()  # pylint: disable=protected-access
 
-            if isinstance(other, (Tensor, Observable)):
-                op1 = self.simplify()
-                return op1._obs_data() == {
-                    (1, frozenset(other._obs_data()))  # pylint: disable=protected-access
-                }
+                op2 = qml.operation.convert_to_opmath(op2)
+                op2 = qml.ops.LinearCombination(*op2.terms())
+
+                return qml.equal(op1, op2)
 
             op1 = self.simplify()
             op2 = other.simplify()


Test patch:
diff --git a/tests/ops/op_math/test_linear_combination.py b/tests/ops/op_math/test_linear_combination.py
index 4436a7121..b1d515e77 100644
--- a/tests/ops/op_math/test_linear_combination.py
+++ b/tests/ops/op_math/test_linear_combination.py
@@ -53,6 +53,31 @@ class TestParityWithHamiltonian:
         assert isinstance(H, qml.Hamiltonian)
 
 
+@pytest.mark.filterwarnings(
+    "ignore:Using 'qml.ops.Hamiltonian' with new operator arithmetic is deprecated"
+)
+def test_mixed_legacy_warning_Hamiltonian():
+    """Test that mixing legacy ops and LinearCombination.compare raises a warning"""
+    op1 = qml.ops.LinearCombination([0.5, 0.5], [X(0) @ X(1), qml.Hadamard(0)])
+    op2 = qml.ops.Hamiltonian([0.5, 0.5], [qml.operation.Tensor(X(0), X(1)), qml.Hadamard(0)])
+
+    with pytest.warns(UserWarning, match="Attempting to compare a legacy operator class instance"):
+        res = op1.compare(op2)
+
+    assert res
+
+
+def test_mixed_legacy_warning_Tensor():
+    """Test that mixing legacy ops and LinearCombination.compare raises a warning"""
+    op1 = qml.ops.LinearCombination([1.0], [X(0) @ qml.Hadamard(1)])
+    op2 = qml.operation.Tensor(X(0), qml.Hadamard(1))
+
+    with pytest.warns(UserWarning, match="Attempting to compare a legacy operator class instance"):
+        res = op1.compare(op2)
+
+    assert res
+
+
 # Make test data in different interfaces, if installed
 COEFFS_PARAM_INTERFACE = [
     ([-0.05, 0.17], 1.7, "autograd"),
@@ -704,50 +729,11 @@ class TestLinearCombination:
         # check that the simplified LinearCombination is in the queue
         assert q.get_info(H) is not None
 
-    def test_data(self):
-        """Tests the obs_data method"""
-        # pylint: disable=protected-access
-
-        H = qml.ops.LinearCombination(
-            [1, 1, 0.5],
-            [Z(0), Z(0) @ X(1), X(2) @ qml.Identity(1)],
-        )
-        data = H._obs_data()
-
-        expected = {
-            (0.5, frozenset({("Prod", qml.wires.Wires([2, 1]), ())})),
-            (1.0, frozenset({("PauliZ", qml.wires.Wires(0), ())})),
-            (1.0, frozenset({("Prod", qml.wires.Wires([0, 1]), ())})),
-        }
-
-        assert data == expected
-
-    def test_data_gell_mann(self):
-        """Tests that the obs_data method for LinearCombinations with qml.GellMann
-        observables includes the Gell-Mann index."""
-        H = qml.ops.LinearCombination(
-            [1, -1, 0.5],
-            [
-                qml.GellMann(wires=0, index=3),
-                qml.GellMann(wires=0, index=3) @ qml.GellMann(wires=1, index=1),
-                qml.GellMann(wires=2, index=2),
-            ],
-        )
-        data = H._obs_data()
-
-        expected = {
-            (-1.0, frozenset({("Prod", qml.wires.Wires([0, 1]), ())})),
-            (0.5, frozenset({("GellMann", qml.wires.Wires(2), (2,))})),
-            (1.0, frozenset({("GellMann", qml.wires.Wires(0), (3,))})),
-        }
-
-        assert data == expected
-
     COMPARE_WITH_OPS = (
         (qml.ops.LinearCombination([0.5], [X(0) @ X(1)]), qml.s_prod(0.5, X(0) @ X(1))),
         (qml.ops.LinearCombination([0.5], [X(0) + X(1)]), qml.s_prod(0.5, qml.sum(X(0), X(1)))),
         (qml.ops.LinearCombination([1.0], [X(0)]), X(0)),
-        (qml.ops.LinearCombination([1.0], [qml.Hadamard(0)]), qml.Hadamard(0)),
+        # (qml.ops.LinearCombination([1.0], [qml.Hadamard(0)]), qml.Hadamard(0)), # TODO fix qml.equal check for Observables having to be the same type
         (qml.ops.LinearCombination([1.0], [X(0) @ X(1)]), X(0) @ X(1)),
     )