Repository: PennyLaneAI/pennylane
Pull request #5138 (2024-02-01 13:29:17)

[Strings2] Improve string repr of Sum
`Sum` instances are now displayed multiline and without unnecessary brackets. Further, all outputs should be copy-and-paste-able. Some examples how this now looks:

```python3
>>> qml.operation.enable_new_opmath()
>>> X(0) + X(1) + X(2) # short outputs <60 characters in single line
X(0) + X(1) + X(2)
>>> 0.5 * (X(0) @ X(1)) + 0.7 * (X(1) @ X(2)) + 0.8 * (X(2) @ X(3)) # larger outputs break into multiple lines. copy-paste-able with surrounding brackets
(
    0.5 * (X(0) @ X(1))
  + 0.7 * (X(1) @ X(2))
  + 0.8 * (X(2) @ X(3))
)
```

There are also some peculiarities that cannot be improved at the moment because they are due to the nesting and lazy execution of products and sums:
```python3
>>> 0.5 * (X(0) @ (0.5 * X(1))) + 0.7 * X(1)
0.5 * (X(0) @ (0.5 * X(1))) + 0.7 * X(1)
```


ToDo

- [x] Basic functionality
- [x] Tests
- [x] multiline threshold solution? (Problem from nesting in e.g. X(0) + X(1) + X(2))
- [x] changelog

Documentation will be updated together with the changes from https://github.com/PennyLaneAI/pennylane/pull/5116 in a follow-up PR

Builds on top of https://github.com/PennyLaneAI/pennylane/pull/5116
[sc-54959]
[sc-54955]

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/5138?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 [(`b239db5`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/b239db56399cb65b1085dbf0660a0360906d8f81?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68% compared to head [(`5c8df1f`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5138?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68%.


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


```diff
@@              Coverage Diff              @@
##           paulistrings    #5138   +/-   ##
=============================================
  Coverage         99.68%   99.68%           
=============================================
  Files               394      394           
  Lines             35737    35743    +6     
=============================================
+ Hits              35625    35631    +6     
  Misses              112      112           
```



</details>

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

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5138?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 [(`b5ca15a`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/b5ca15a5477be6a8b35316d6bc585610122a577b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68% compared to head [(`ef12f93`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5138?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68%.


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


```diff
@@              Coverage Diff              @@
##           paulistrings    #5138   +/-   ##
=============================================
  Coverage         99.68%   99.68%           
=============================================
  Files               394      394           
  Lines             35875    35883    +8     
=============================================
+ Hits              35763    35771    +8     
  Misses              112      112           
```



</details>

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

albi3ro: ![Screenshot 2024-02-08 at 10 54 39 AM](https://github.com/PennyLaneAI/pennylane/assets/6364575/e803296d-409b-455f-9a64-425b5fa671ba)


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 8f9d6af82..b5653eaec 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -220,6 +220,18 @@
   ```
   [(#5116)](https://github.com/PennyLaneAI/pennylane/pull/5116)
 
+* String representations of `Sum` objects now break into multiple lines
+  whenever the output is larger than 50 characters.
+  ```
+  >>> 0.5 * (X(0) @ X(1)) + 0.7 * (X(1) @ X(2)) + 0.8 * (X(2) @ X(3))
+  (
+      0.5 * (X(0) @ X(1))
+    + 0.7 * (X(1) @ X(2))
+    + 0.8 * (X(2) @ X(3))
+  )
+  ```
+  [(#5138)](https://github.com/PennyLaneAI/pennylane/pull/5138)
+
 * `qml.ctrl` called on operators with custom controlled versions will return instances
   of the custom class, and it will also flatten nested controlled operators to a single
   multi-controlled operation. For `PauliX`, `CNOT`, `Toffoli`, and `MultiControlledX`,
diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py
index 9bd23f119..a2033fb4e 100644
--- a/pennylane/ops/op_math/sum.py
+++ b/pennylane/ops/op_math/sum.py
@@ -164,6 +164,21 @@ class Sum(CompositeOp):
         # Since addition is always commutative, we do not need to sort
         return hash(("Sum", frozenset(o.hash for o in self.operands)))
 
+    def __str__(self):
+        """String representation of the PauliSentence."""
+        ops = self.operands
+        return " + ".join(f"{str(op)}" if i == 0 else f"{str(op)}" for i, op in enumerate(ops))
+
+    def __repr__(self):
+        """Terminal representation for PauliSentence"""
+        # post-processing the flat str() representation
+        # We have to do it like this due to the possible
+        # nesting of Sums, e.g. X(0) + X(1) + X(2) is a sum(sum(X(0), X(1)), X(2))
+        if len(main_string := str(self)) > 50:
+            main_string = main_string.replace(" + ", "\n  + ")
+            return f"(\n    {main_string}\n)"
+        return main_string
+
     @property
     def is_hermitian(self):
         """If all of the terms in the sum are hermitian, then the Sum is hermitian."""


Test patch:
diff --git a/tests/ops/op_math/test_sum.py b/tests/ops/op_math/test_sum.py
index 8089e31ae..68582d2a5 100644
--- a/tests/ops/op_math/test_sum.py
+++ b/tests/ops/op_math/test_sum.py
@@ -14,6 +14,7 @@
 """
 Unit tests for the Sum arithmetic class of qubit operations
 """
+# pylint: disable=eval-used
 from typing import Tuple
 
 import gate_data as gd  # a file containing matrix rep of each gate
@@ -22,7 +23,7 @@ import pytest
 
 import pennylane as qml
 import pennylane.numpy as qnp
-from pennylane import math
+from pennylane import math, X, Y, Z
 from pennylane.wires import Wires
 from pennylane.operation import AnyWires, MatrixUndefinedError, Operator
 from pennylane.ops.op_math import Prod, Sum
@@ -174,6 +175,47 @@ class TestInitialization:
         assert np.allclose(eig_vals, cached_vals)
         assert np.allclose(eig_vecs, cached_vecs)
 
+    qml.operation.enable_new_opmath()
+    SUM_REPR = (
+        (qml.sum(X(0), Y(1), Z(2)), "X(0) + Y(1) + Z(2)"),
+        (X(0) + X(1) + X(2), "X(0) + X(1) + X(2)"),
+        (0.5 * X(0) + 0.7 * X(1), "0.5 * X(0) + 0.7 * X(1)"),
+        (0.5 * (X(0) @ X(1)) + 0.7 * X(1), "0.5 * (X(0) @ X(1)) + 0.7 * X(1)"),
+        (
+            0.5 * (X(0) @ (0.5 * X(1))) + 0.7 * X(1) + 0.8 * qml.CNOT((0, 1)),
+            "(\n    0.5 * (X(0) @ (0.5 * X(1)))\n  + 0.7 * X(1)\n  + 0.8 * CNOT(wires=[0, 1])\n)",
+        ),
+        (
+            0.5 * (X(0) @ (0.5 * X(1))) + 0.7 * X(1) + 0.8 * (X(0) @ Y(1) @ Z(1)),
+            "(\n    0.5 * (X(0) @ (0.5 * X(1)))\n  + 0.7 * X(1)\n  + 0.8 * ((X(0) @ Y(1)) @ Z(1))\n)",
+        ),
+    )
+    qml.operation.disable_new_opmath()
+
+    @pytest.mark.parametrize("op, repr_true", SUM_REPR)
+    def test_repr(self, op, repr_true):
+        """Test the string representation of Sum instances"""
+        assert repr(op) == repr_true
+
+    qml.operation.enable_new_opmath()
+    SUM_REPR_EVAL = (
+        X(0) + Y(1) + Z(2),  # single line output
+        0.5 * X(0) + 3.5 * Y(1) + 10 * Z(2),  # single line output
+        X(0) @ X(1) + Y(1) @ Y(2) + Z(2),  # single line output
+        0.5 * (X(0) @ X(1) @ X(2))
+        + 1000 * (Y(1) @ X(0) @ X(1))
+        + 1000000000 * Z(2),  # multiline output
+        # qml.sum(*[0.5 * X(i) for i in range(10)]) # multiline output needs fixing of https://github.com/PennyLaneAI/pennylane/issues/5162 before working
+    )
+    qml.operation.disable_new_opmath()
+
+    @pytest.mark.parametrize("op", SUM_REPR_EVAL)
+    def test_eval_sum(self, op):
+        """Test that string representations of Sum can be evaluated and yield the same operator"""
+        qml.operation.enable_new_opmath()
+        assert qml.equal(eval(repr(op)), op)
+        qml.operation.disable_new_opmath()
+
 
 class TestMatrix:
     """Test matrix-related methods."""
diff --git a/tests/pulse/test_parametrized_hamiltonian.py b/tests/pulse/test_parametrized_hamiltonian.py
index f8a55d05d..51f9fa191 100644
--- a/tests/pulse/test_parametrized_hamiltonian.py
+++ b/tests/pulse/test_parametrized_hamiltonian.py
@@ -231,7 +231,9 @@ class TestCall:
         H = ParametrizedHamiltonian(coeffs, obs)
 
         assert isinstance(H([2], 4), qml.ops.Sum)
-        assert repr(H([2], t=4)) == "(2 * GellMann2(wires=[0])) + (10 * GellMann1(wires=[0]))"
+        assert (
+            repr(H([2], t=4)) == "(\n    2 * GellMann2(wires=[0])\n  + 10 * GellMann1(wires=[0])\n)"
+        )
         assert np.all(
             qml.matrix(H([2], 4))
             == np.array(
diff --git a/tests/test_tensor_measurements.py b/tests/test_tensor_measurements.py
index bca43eba5..bc6386251 100644
--- a/tests/test_tensor_measurements.py
+++ b/tests/test_tensor_measurements.py
@@ -21,6 +21,7 @@ import numpy as np
 import pennylane as qml
 from pennylane import expval, var, sample
 
+np.random.seed(0)
 
 Z = np.array([[1, 0], [0, -1]])
 THETA = np.linspace(0.11, 3, 5)