Repository: PennyLaneAI/pennylane
Pull request #5164 (2024-02-06 19:15:06)

[Terms3] Add `Sum.ops` and `Sum.coeffs` and directly deprecated them
Following https://github.com/PennyLaneAI/pennylane/pull/5132/ and https://github.com/PennyLaneAI/pennylane/pull/5133, the overarching goal is to achieve feature parity with `qml.Hamiltonian`, which on the other hand is part of the overarching goal of activating new opmath by default.

While for `qml.Hamiltonian` it made sense to access `coeffs` and `ops` directly, in `Sum` and `Prod` it requires some processing so it makes more sense (and costs less) to directly call the `terms()` method to obtain both.


[sc-56284]
[sc-53963]

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5164?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:
> Comparison is base [(`c5850e9`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/c5850e9d05e0690599c00ca129fa3470cc359f2f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68% compared to head [(`99c62fb`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5164?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.67%.


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


```diff
@@             Coverage Diff              @@
##           sumterms    #5164      +/-   ##
============================================
- Coverage     99.68%   99.67%   -0.01%     
============================================
  Files           394      394              
  Lines         35914    35936      +22     
============================================
+ Hits          35801    35821      +20     
- Misses          113      115       +2     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5164?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).

Qottmann: > What is the additional processing? Aren't we just doing coefs, ops = Sum.terms()

Yes! What I meant was that in `Hamiltonian`, `H.ops` and `H.coeffs` (as well as `H.terms()`) are directly accessed because they define the object, whereas in the case of `Sum` we need to do some processing (namely the one in `Sum.terms()`), so accessing for example `Sum.ops` and `Sum.coeffs` calls the `terms()` method twice, which is why we suggest users to use `Sum.terms()` in the first place to avoid that culprit.

Patch:
diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst
index 0f7b15ff1..57d39cfe7 100644
--- a/doc/development/deprecations.rst
+++ b/doc/development/deprecations.rst
@@ -68,6 +68,10 @@ Pending deprecations
   - Deprecated in v0.35
   - Will be removed in v0.36
 
+* ``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
+
 Completed deprecation cycles
 ----------------------------
 
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 5f088b7f4..a7189bb0b 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -365,6 +365,8 @@
   are being raised unexpectedly.
   [(#5122)](https://github.com/PennyLaneAI/pennylane/pull/5122)
 
+* `Sum.ops`, `Sum.coeffs`, `Prod.ops`, `Prod.coeffs` have been added for feature parity with `qml.Hamiltonian` but will be deprecated in the future.
+  [(#5164)](https://github.com/PennyLaneAI/pennylane/pull/5164)
 
 * Added a `partial_trace` function to `pennylane.math` for matrices.
   [(#5152)](https://github.com/PennyLaneAI/pennylane/pull/5152)
@@ -494,6 +496,9 @@
   functionality can be achieved using newer features in the `pauli` module.
   [(#5057)](https://github.com/PennyLaneAI/pennylane/pull/5057)
 
+* `Sum.ops`, `Sum.coeffs`, `Prod.ops` and `Prod.coeffs` will be deprecated in the future.
+  [(#5164)](https://github.com/PennyLaneAI/pennylane/pull/5164)
+
 <h3>Documentation 📝</h3>
 
 * The module documentation for `pennylane.tape` now explains the difference between `QuantumTape` and `QuantumScript`.
diff --git a/pennylane/ops/op_math/prod.py b/pennylane/ops/op_math/prod.py
index b1c787334..b096a8d66 100644
--- a/pennylane/ops/op_math/prod.py
+++ b/pennylane/ops/op_math/prod.py
@@ -16,6 +16,7 @@ This file contains the implementation of the Prod class which contains logic for
 computing the product between operations.
 """
 import itertools
+import warnings
 from copy import copy
 from functools import reduce, wraps
 from itertools import combinations
@@ -65,25 +66,30 @@ def prod(*ops, id=None, lazy=True):
 
         This operator supports batched operands:
 
-        >>> op = qml.prod(qml.RX(np.array([1, 2, 3]), wires=0), qml.PauliX(1))
+        >>> op = qml.prod(qml.RX(np.array([1, 2, 3]), wires=0), qml.X(1))
         >>> op.matrix().shape
         (3, 4, 4)
 
         But it doesn't support batching of operators:
 
-        >>> op = qml.prod(np.array([qml.RX(0.5, 0), qml.RZ(0.3, 0)]), qml.PauliZ(0))
+        >>> op = qml.prod(np.array([qml.RX(0.5, 0), qml.RZ(0.3, 0)]), qml.Z(0))
         AttributeError: 'numpy.ndarray' object has no attribute 'wires'
 
     .. seealso:: :class:`~.ops.op_math.Prod`
 
     **Example**
 
-    >>> prod_op = prod(qml.PauliX(0), qml.PauliZ(0))
+    >>> prod_op = prod(qml.X(0), qml.Z(0))
     >>> prod_op
-    PauliX(wires=[0]) @ PauliZ(wires=[0])
+    X(0) @ Z(0)
     >>> prod_op.matrix()
     array([[ 0, -1],
            [ 1,  0]])
+    >>> prod_op.simplify()
+    -1j * Y(0)
+    >>> prod_op.terms()
+    ([-1j], [Y(0)])
+
 
     You can also create a prod operator by passing a qfunc to prod, like the following:
 
@@ -143,14 +149,16 @@ class Prod(CompositeOp):
 
     **Example**
 
-    >>> prop_op = Prod(qml.PauliX(wires=0), qml.PauliZ(wires=0))
-    >>> prop_op
-    PauliX(wires=[0]) @ PauliZ(wires=[0])
-    >>> qml.matrix(prop_op)
-    array([[ 0,  -1],
-           [ 1,   0]])
-    >>> prop_op.terms()
-    ([1.0], [PauliX(wires=[0]) @ PauliZ(wires=[0])])
+    >>> prod_op = Prod(qml.X(0), qml.PauliZ(1))
+    >>> prod_op
+    X(0) @ Z(1)
+    >>> qml.matrix(prod_op, wire_order=prod_op.wires)
+    array([[ 0,  0,  1,  0],
+           [ 0,  0,  0, -1],
+           [ 1,  0,  0,  0],
+           [ 0, -1,  0,  0]])
+    >>> prod_op.terms()
+    ([1.0], [Z(1) @ X(0)])
 
     .. note::
         When a Prod operator is applied in a circuit, its factors are applied in the reverse order.
@@ -158,9 +166,9 @@ class Prod(CompositeOp):
         first applying :math:`\hat{op}_{2}` then :math:`\hat{op}_{1}` in the circuit). We can see this
         in the decomposition of the operator.
 
-    >>> op = Prod(qml.PauliX(wires=0), qml.PauliZ(wires=1))
+    >>> op = Prod(qml.X(0), qml.PauliZ(1))
     >>> op.decomposition()
-    [PauliZ(wires=[1]), PauliX(wires=[0])]
+    [Z(1), X(0)]
 
     .. details::
         :title: Usage Details
@@ -214,6 +222,13 @@ class Prod(CompositeOp):
         >>> weights = np.array([0.1], requires_grad=True)
         >>> qml.grad(circuit)(weights)
         array([-0.07059289])
+
+        Note that the :meth:`~Prod.terms` method always simplifies and flattens the operands.
+
+        >>> op = qml.ops.Prod(qml.X(0), qml.sum(qml.Y(0), qml.Z(1)))
+        >>> op.terms()
+        ([1j, 1.0], [Z(0), Z(1) @ X(0)])
+
     """
 
     _op_symbol = "@"
@@ -439,7 +454,7 @@ class Prod(CompositeOp):
             tuple[list[tensor_like or float], list[.Operation]]: list of coefficients :math:`c_i`
             and list of operations :math:`O_i`
 
-        ** Example **
+        **Example**
 
         >>> qml.operation.enable_new_opmath()
         >>> op = X(0) @ (0.5 * X(1) + X(2))
@@ -472,6 +487,36 @@ class Prod(CompositeOp):
                 ops.append(factor)
         return coeffs, ops
 
+    @property
+    def coeffs(self):
+        r"""
+        Scalar coefficients of the operator when flattened out.
+
+        This is a deprecated attribute, please use :meth:`~Prod.terms` instead.
+
+        .. seealso:: :attr:`~Prod.ops`, :class:`~Prod.pauli_rep`"""
+        warnings.warn(
+            "Prod.coeffs is deprecated and will be removed in future releases. You can access both (coeffs, ops) via op.terms(). Also consider op.operands.",
+            qml.PennyLaneDeprecationWarning,
+        )
+        coeffs, _ = self.terms()
+        return coeffs
+
+    @property
+    def ops(self):
+        r"""
+        Operator terms without scalar coefficients of the operator when flattened out.
+
+        This is a deprecated attribute, please use :meth:`~Prod.terms` instead.
+
+        .. seealso:: :attr:`~Prod.coeffs`, :class:`~Prod.pauli_rep`"""
+        warnings.warn(
+            "Prod.ops is deprecated and will be removed in future releases. You can access both (coeffs, ops) via op.terms() Also consider op.operands.",
+            qml.PennyLaneDeprecationWarning,
+        )
+        _, ops = self.terms()
+        return ops
+
 
 def _swappable_ops(op1, op2, wire_map: dict = None) -> bool:
     """Boolean expression that indicates if op1 and op2 don't have intersecting wires and if they
diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py
index 2d5a7e6dc..2bb57ff44 100644
--- a/pennylane/ops/op_math/sum.py
+++ b/pennylane/ops/op_math/sum.py
@@ -15,6 +15,7 @@
 This file contains the implementation of the Sum class which contains logic for
 computing the sum of operations.
 """
+import warnings
 import itertools
 from copy import copy
 from typing import List
@@ -96,7 +97,8 @@ class Sum(CompositeOp):
     .. note::
 
         This operator supports batched operands:
-        >>> op = qml.sum(qml.RX(np.array([1, 2, 3]), wires=0), qml.PauliX(1))
+
+        >>> op = qml.sum(qml.RX(np.array([1, 2, 3]), wires=0), qml.X(1))
         >>> op.matrix().shape
         (3, 4, 4)
 
@@ -108,14 +110,14 @@ class Sum(CompositeOp):
 
     **Example**
 
-    >>> summed_op = Sum(qml.PauliX(0), qml.PauliZ(0))
+    >>> summed_op = Sum(qml.X(0), qml.Z(0))
     >>> summed_op
-    PauliX(wires=[0]) + PauliZ(wires=[0])
+    X(0) + Z(0)
     >>> qml.matrix(summed_op)
     array([[ 1,  1],
            [ 1, -1]])
     >>> summed_op.terms()
-    ([1.0, 1.0], (PauliX(wires=[0]), PauliZ(wires=[0])))
+    ([1.0, 1.0], (X(0), Z(0)))
 
     .. details::
         :title: Usage Details
@@ -123,7 +125,7 @@ class Sum(CompositeOp):
         We can combine parameterized operators, and support sums between operators acting on
         different wires.
 
-        >>> summed_op = Sum(qml.RZ(1.23, wires=0), qml.Identity(wires=1))
+        >>> summed_op = Sum(qml.RZ(1.23, wires=0), qml.I(wires=1))
         >>> summed_op.matrix()
         array([[1.81677345-0.57695852j, 0.        +0.j        ,
                 0.        +0.j        , 0.        +0.j        ],
@@ -140,7 +142,7 @@ class Sum(CompositeOp):
 
         .. code-block:: python
 
-            sum_op = Sum(qml.PauliX(0), qml.PauliZ(1))
+            sum_op = Sum(qml.X(0), qml.Z(1))
             dev = qml.device("default.qubit", wires=2)
 
             @qml.qnode(dev, diff_method="best")
@@ -321,6 +323,7 @@ class Sum(CompositeOp):
          [X(0), X(1), Y(1) @ Y(0)])
 
         Note that this method disentangles nested structures of ``Sum`` instances like so.
+
         >>> op = 0.5 * X(0) + (2. * (X(1) + 3. * X(2)))
         >>> print(op)
         (0.5*(PauliX(wires=[0]))) + (2.0*((0.5*(PauliX(wires=[1]))) + (3.0*(PauliX(wires=[2])))))
@@ -346,6 +349,36 @@ class Sum(CompositeOp):
                 ops.append(factor)
         return coeffs, ops
 
+    @property
+    def coeffs(self):
+        r"""
+        Scalar coefficients of the operator when flattened out.
+
+        This is a deprecated attribute, please use :meth:`~Sum.terms` instead.
+
+        .. seealso:: :attr:`~Sum.ops`, :class:`~Sum.pauli_rep`"""
+        warnings.warn(
+            "Sum.coeffs is deprecated and will be removed in future releases. You can access both (coeffs, ops) via op.terms(). Also consider op.operands.",
+            qml.PennyLaneDeprecationWarning,
+        )
+        coeffs, _ = self.terms()
+        return coeffs
+
+    @property
+    def ops(self):
+        r"""
+        Operator terms without scalar coefficients of the operator when flattened out.
+
+        This is a deprecated attribute, please use :meth:`~Sum.terms` instead.
+
+        .. seealso:: :attr:`~Sum.coeffs`, :class:`~Sum.pauli_rep`"""
+        warnings.warn(
+            "Sum.ops is deprecated and will be removed in future releases. You can access both (coeffs, ops) via op.terms(). Also consider op.operands.",
+            qml.PennyLaneDeprecationWarning,
+        )
+        _, ops = self.terms()
+        return ops
+
     @classmethod
     def _sort(cls, op_list, wire_map: dict = None) -> List[Operator]:
         """Sort algorithm that sorts a list of sum summands by their wire indices.


Test patch:
diff --git a/tests/ops/op_math/test_prod.py b/tests/ops/op_math/test_prod.py
index 401b6d5c7..acc1a603d 100644
--- a/tests/ops/op_math/test_prod.py
+++ b/tests/ops/op_math/test_prod.py
@@ -89,6 +89,20 @@ ops_hermitian_status = (  # computed manually
 )
 
 
+def test_legacy_ops():
+    """Test that PennyLaneDepcreationWarning is raised when Prod.ops is called"""
+    H = qml.prod(X(0), X(1))
+    with pytest.warns(qml.PennyLaneDeprecationWarning, match="Prod.ops is deprecated and"):
+        _ = H.ops
+
+
+def test_legacy_coeffs():
+    """Test that PennyLaneDepcreationWarning is raised when Prod.coeffs is called"""
+    H = qml.prod(X(0), X(1))
+    with pytest.warns(qml.PennyLaneDeprecationWarning, match="Prod.coeffs is deprecated and"):
+        _ = H.coeffs
+
+
 # currently failing due to has_diagonalizing_gates logic
 @pytest.mark.xfail  # TODO: fix with story 49608
 def test_basic_validity():
diff --git a/tests/ops/op_math/test_sum.py b/tests/ops/op_math/test_sum.py
index 16943b117..cc3c1fd99 100644
--- a/tests/ops/op_math/test_sum.py
+++ b/tests/ops/op_math/test_sum.py
@@ -114,6 +114,20 @@ def compare_and_expand_mat(mat1, mat2):
     return smaller_mat, larger_mat
 
 
+def test_legacy_ops():
+    """Test that PennyLaneDepcreationWarning is raised when Sum.ops is called"""
+    H = qml.sum(X(0), X(1))
+    with pytest.warns(qml.PennyLaneDeprecationWarning, match="Sum.ops is deprecated and"):
+        _ = H.ops
+
+
+def test_legacy_coeffs():
+    """Test that PennyLaneDepcreationWarning is raised when Sum.ops is called"""
+    H = qml.sum(X(0), X(1))
+    with pytest.warns(qml.PennyLaneDeprecationWarning, match="Sum.coeffs is deprecated and"):
+        _ = H.coeffs
+
+
 class TestInitialization:
     """Test the initialization."""