Repository: PennyLaneAI/pennylane (python)

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)])
```

Pull request description:
modified `__add__` for `Observable` and `Hamiltonian` to support adding `0`
**Context:**

We would like to be able to sum over a list of observables as follows:
```
H = sum([qml.PauliX(i) for i in range(10)])
```
Since `sum` starts at integer `0` by default, we need to define addition between observables and `0`.

**Description of the Change:**

We modified the `__add__` method for `Observable` and `Hamiltonian` classes, so that integer `0` can be added to those objects, either from the left side or the right side. Unit tests were added for this feature.

**Benefits:**

Now we can directly `sum` a list of observables in an elegant way.

**Possible Drawbacks:**

None.

**Related GitHub Issues:**

Closes #2423.

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"""