Repository: PennyLaneAI/pennylane

Issue:
Support adding observable to scalar `0`?
### Feature details

It would be nice to be able to do the following:
```
H = sum([qml.PauliX(i) for i in range(10)])
```

The issue here is that `sum` naturally starts at integer 0 but there's no way to add observables to an integer 0. Philosophically it seems valid to use `0` as the zero element of dxd matrices (similar behavior is supported for numpy arrays, e.g.).

### Implementation

```
    def __add__(self, other):
        r"""The addition operation between Observables/Tensors/qml.Hamiltonian objects."""
        if isinstance(other, numbers.Number) and other == 0:
            return self
        if isinstance(other, qml.Hamiltonian):
            return other + self
        if isinstance(other, (Observable, Tensor)):
            return qml.Hamiltonian([1, 1], [self, other], simplify=True)
        raise ValueError(f"Cannot add Observable and {type(other)}")
```
I'm assuming anything that is a `Number` implements `__eq__`, might need to double check that though.

### How important would you say this feature is?

1: Not important. Would be nice to have.

### Additional information

Workarounds:
```
H = sum([qml.PauliX(i) for i in range(10)], start=0*qml.PauliX(0)) # meh
H = qml.PauliX(0)
for i in range(1, 10):
    H += qml.PauliX(i) # ugh
```

For reference, the corresponding operation works in cirq:
```
H = sum([cirq.X(j) for j in cirq.LineQubit.range(10)])
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#2603](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (f8befc6) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/a61e25a0454812b69f259b5e76be42fe4803737d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (a61e25a) will **decrease** coverage by `40.69%`.
> The diff coverage is `75.00%`.

```diff
@@             Coverage Diff             @@
##           master    #2603       +/-   ##
===========================================
- Coverage   99.58%   58.89%   -40.70%     
===========================================
  Files         243      243               
  Lines       19589    19597        +8     
===========================================
- Hits        19508    11541     -7967     
- Misses         81     8056     +7975     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/operation.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wZXJhdGlvbi5weQ==) | `80.31% <75.00%> (-16.42%)` | :arrow_down: |
| [pennylane/ops/qubit/hamiltonian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9oYW1pbHRvbmlhbi5weQ==) | `86.09% <75.00%> (-13.91%)` | :arrow_down: |
| [pennylane/interfaces/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/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%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/jax\_jit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvamF4X2ppdC5weQ==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/tensorflow.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvdGVuc29yZmxvdy5weQ==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/tensorflow\_autograph.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvdGVuc29yZmxvd19hdXRvZ3JhcGgucHk=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/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%> (-96.67%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_tf.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF90Zi5weQ==) | `0.00% <0.00%> (-92.76%)` | :arrow_down: |
| [pennylane/qchem/openfermion\_obs.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3FjaGVtL29wZW5mZXJtaW9uX29icy5weQ==) | `7.26% <0.00%> (-92.74%)` | :arrow_down: |
| [pennylane/hf/hamiltonian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2hmL2hhbWlsdG9uaWFuLnB5) | `8.13% <0.00%> (-91.87%)` | :arrow_down: |
| ... and [186 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603/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/2603?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/2603?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [a61e25a...f8befc6](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2603?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).

Qi-Hu: > Hi @Qi-Hu, let us know when the PR is ready for review, or if you have any questions 🙂

Hi @co9olguy, this PR is ready for review. Thanks!
Qi-Hu: Hi @antalszava, thanks a lot for your detailed comments and useful suggestions! I have made a few changes according to your suggestions.

I have two questions:
1. The `__iadd__` method is only explicitly defined for the `Hamiltonian` class. Do we need to test it for general observables?
2. I added more test cases for the `__add__` method of `Observable` class, including `SparseHamiltonian`, `Projector` and `CVObservable`. However, there were a few test failures. They seemed to be caused by the same issue, which is related to the attribute 'tobytes'. I haven't figured out why this happened, but I will continue working on it.

```
pennylane/operation.py:1635: in compare
    return other._obs_data() == self._obs_data()
pennylane/operation.py:1598: in _obs_data
    parameters = tuple(param.tobytes() for param in ob.parameters)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

.0 = <list_iterator object at 0x7fc90b25a850>

>   parameters = tuple(param.tobytes() for param in ob.parameters)
E   AttributeError: 'float' object has no attribute 'tobytes'

pennylane/operation.py:1598: AttributeError
```
Qi-Hu: Hi @antalszava, I realized the `compare` method is not supported by a few classes: `Projector`, `SparseHamiltonian`, `QuadOperator` and `FockStateProjector`. To be more specific, their parameters do not have `tobytes` attribute. I do not know if this is an issue that needs to be fixed, so I just didn't include these test cases in my most recent commit.

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index e32411a68..c12f9fd9d 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -4,6 +4,14 @@
 
 <h3>New features since last release</h3>
 
+* Support adding `Observable` objects to the integer `0`.
+  [(#2603)](https://github.com/PennyLaneAI/pennylane/pull/2603)
+
+  This allows us to directly sum a list of observables as follows:
+  ```
+  H = sum([qml.PauliX(i) for i in range(10)])
+  ```
+
 * Parameter broadcasting within operations and tapes was introduced.
   [(#2575)](https://github.com/PennyLaneAI/pennylane/pull/2575)
   [(#2590)](https://github.com/PennyLaneAI/pennylane/pull/2590)
@@ -363,6 +371,5 @@
 This release contains contributions from (in alphabetical order):
 
 Amintor Dusko, Chae-Yeun Park, Christian Gogolin, Christina Lee, David Wierichs, Edward Jiang, Guillermo Alonso-Linaje,
-Jay Soni, Juan Miguel Arrazola, Maria Schuld, Mikhail Andrenkov, Romain Moyard, Samuel Banning, Soran Jahangiri, 
+Jay Soni, Juan Miguel Arrazola, Maria Schuld, Mikhail Andrenkov, Romain Moyard, Qi Hu, Samuel Banning, Soran Jahangiri, 
 Utkarsh Azad, WingCode
-
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 58d0ae84e..81d010cef 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -97,6 +97,7 @@ import abc
 import copy
 import itertools
 import functools
+import numbers
 import warnings
 from enum import IntEnum
 from scipy.sparse import kron, eye, coo_matrix
@@ -1659,12 +1660,16 @@ class Observable(Operator):
 
     def __add__(self, other):
         r"""The addition operation between Observables/Tensors/qml.Hamiltonian objects."""
+        if isinstance(other, numbers.Number) and other == 0:
+            return self
         if isinstance(other, qml.Hamiltonian):
             return other + self
         if isinstance(other, (Observable, Tensor)):
             return qml.Hamiltonian([1, 1], [self, other], simplify=True)
         raise ValueError(f"Cannot add Observable and {type(other)}")
 
+    __radd__ = __add__
+
     def __mul__(self, a):
         r"""The scalar multiplication operation between a scalar and an Observable/Tensor."""
         if isinstance(a, (int, float)):
diff --git a/pennylane/ops/qubit/hamiltonian.py b/pennylane/ops/qubit/hamiltonian.py
index 568c006bb..91a90f36b 100644
--- a/pennylane/ops/qubit/hamiltonian.py
+++ b/pennylane/ops/qubit/hamiltonian.py
@@ -17,6 +17,7 @@ arithmetic operations on their input states.
 """
 # pylint: disable=too-many-arguments,too-many-instance-attributes
 import itertools
+import numbers
 from copy import copy
 from collections.abc import Iterable
 
@@ -571,6 +572,9 @@ class Hamiltonian(Observable):
         ops = self.ops.copy()
         self_coeffs = copy(self.coeffs)
 
+        if isinstance(H, numbers.Number) and H == 0:
+            return self
+
         if isinstance(H, Hamiltonian):
             coeffs = qml.math.concatenate([self_coeffs, copy(H.coeffs)], axis=0)
             ops.extend(H.ops.copy())
@@ -585,6 +589,8 @@ class Hamiltonian(Observable):
 
         raise ValueError(f"Cannot add Hamiltonian and {type(H)}")
 
+    __radd__ = __add__
+
     def __mul__(self, a):
         r"""The scalar multiplication operation between a scalar and a Hamiltonian."""
         if isinstance(a, (int, float)):
@@ -604,6 +610,9 @@ class Hamiltonian(Observable):
 
     def __iadd__(self, H):
         r"""The inplace addition operation between a Hamiltonian and a Hamiltonian/Tensor/Observable."""
+        if isinstance(H, numbers.Number) and H == 0:
+            return self
+
         if isinstance(H, Hamiltonian):
             self._coeffs = qml.math.concatenate([self._coeffs, H.coeffs], axis=0)
             self._ops.extend(H.ops.copy())


Test patch:
diff --git a/tests/ops/qubit/test_hamiltonian.py b/tests/ops/qubit/test_hamiltonian.py
index 2f21c9556..e64a6e73f 100644
--- a/tests/ops/qubit/test_hamiltonian.py
+++ b/tests/ops/qubit/test_hamiltonian.py
@@ -268,6 +268,34 @@ add_hamiltonians = [
     ),
 ]
 
+add_zero_hamiltonians = [
+    qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]),
+    qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]),
+    qml.Hamiltonian(
+        [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)]
+    ),
+]
+
+iadd_zero_hamiltonians = [
+    # identical hamiltonians
+    (
+        qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]),
+        qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]),
+    ),
+    (
+        qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]),
+        qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]),
+    ),
+    (
+        qml.Hamiltonian(
+            [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)]
+        ),
+        qml.Hamiltonian(
+            [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)]
+        ),
+    ),
+]
+
 sub_hamiltonians = [
     (
         qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]),
@@ -723,6 +751,16 @@ class TestHamiltonian:
         """Tests that Hamiltonians are added correctly"""
         assert H.compare(H1 + H2)
 
+    @pytest.mark.parametrize("H", add_zero_hamiltonians)
+    def test_hamiltonian_add_zero(self, H):
+        """Tests that Hamiltonians can be added to zero"""
+        assert H.compare(H + 0)
+        assert H.compare(0 + H)
+        assert H.compare(H + 0.0)
+        assert H.compare(0.0 + H)
+        assert H.compare(H + 0e1)
+        assert H.compare(0e1 + H)
+
     @pytest.mark.parametrize(("coeff", "H", "res"), mul_hamiltonians)
     def test_hamiltonian_mul(self, coeff, H, res):
         """Tests that scalars and Hamiltonians are multiplied correctly"""
@@ -764,6 +802,16 @@ class TestHamiltonian:
         H1 += H2
         assert H.compare(H1)
 
+    @pytest.mark.parametrize(("H1", "H2"), iadd_zero_hamiltonians)
+    def test_hamiltonian_iadd_zero(self, H1, H2):
+        """Tests in-place addition between Hamiltonians and zero"""
+        H1 += 0
+        assert H1.compare(H2)
+        H1 += 0.0
+        assert H1.compare(H2)
+        H1 += 0e1
+        assert H1.compare(H2)
+
     @pytest.mark.parametrize(("coeff", "H", "res"), mul_hamiltonians)
     def test_hamiltonian_imul(self, coeff, H, res):
         """Tests that scalars and Hamiltonians are multiplied inline correctly"""
diff --git a/tests/test_operation.py b/tests/test_operation.py
index 40d537899..597aee8c5 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -19,6 +19,8 @@ from functools import reduce
 import warnings
 
 import pytest
+from scipy.sparse import csr_matrix
+
 import numpy as np
 from pennylane import numpy as pnp
 from numpy.linalg import multi_dot
@@ -27,6 +29,7 @@ import pennylane as qml
 from pennylane.operation import Tensor, operation_derivative, Operator, Operation
 
 from gate_data import I, X, CNOT, Toffoli, SWAP, II
+from pennylane.ops import cv
 from pennylane.wires import Wires
 
 
@@ -1478,6 +1481,23 @@ add_obs = [
     ),
 ]
 
+add_zero_obs = [
+    qml.PauliX(0),
+    qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2),
+    qml.PauliX(0) @ qml.Hadamard(2),
+    # qml.Projector(np.array([1, 1]), wires=[0, 1]),
+    # qml.SparseHamiltonian(csr_matrix(np.array([[1, 0], [-1.5, 0]])), 1),
+    # CVObservables
+    qml.Identity(1),
+    cv.NumberOperator(wires=[1]),
+    cv.TensorN(wires=[1]),
+    cv.X(wires=[1]),
+    cv.P(wires=[1]),
+    # cv.QuadOperator(1.234, wires=0),
+    # cv.FockStateProjector([1,2,3], wires=[0, 1, 2]),
+    cv.PolyXP(np.array([1.0, 2.0, 3.0]), wires=[0]),
+]
+
 mul_obs = [
     (qml.PauliZ(0), 3, qml.Hamiltonian([3], [qml.PauliZ(0)])),
     (qml.PauliZ(0) @ qml.Identity(1), 3, qml.Hamiltonian([3], [qml.PauliZ(0)])),
@@ -1580,6 +1600,16 @@ class TestTensorObservableOperations:
         """Tests addition between Tensors and Observables"""
         assert obs.compare(obs1 + obs2)
 
+    @pytest.mark.parametrize("obs", add_zero_obs)
+    def test_add_zero(self, obs):
+        """Tests adding Tensors and Observables to zero"""
+        assert obs.compare(obs + 0)
+        assert obs.compare(0 + obs)
+        assert obs.compare(obs + 0.0)
+        assert obs.compare(0.0 + obs)
+        assert obs.compare(obs + 0e1)
+        assert obs.compare(0e1 + obs)
+
     @pytest.mark.parametrize(("coeff", "obs", "res_obs"), mul_obs)
     def test_scalar_multiplication(self, coeff, obs, res_obs):
         """Tests scalar multiplication of Tensors and Observables"""