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:

Resolving patch:
diff --git a/pennylane/operation.py b/pennylane/operation.py
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -97,6 +97,7 @@
 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 @@ def compare(self, other):
 
     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
--- a/pennylane/ops/qubit/hamiltonian.py
+++ b/pennylane/ops/qubit/hamiltonian.py
@@ -17,6 +17,7 @@
 """
 # 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 @@ def __add__(self, H):
         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 @@ def __add__(self, H):
 
         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 @@ def __sub__(self, H):
 
     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
--- a/tests/ops/qubit/test_hamiltonian.py
+++ b/tests/ops/qubit/test_hamiltonian.py
@@ -268,6 +268,34 @@
     ),
 ]
 
+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 @@ def test_hamiltonian_add(self, H1, H2, H):
         """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 @@ def test_hamiltonian_iadd(self, H1, H2, H):
         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
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -19,6 +19,8 @@
 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 @@
 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 @@ def test_sparse_matrix_error(self):
     ),
 ]
 
+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 @@ def test_addition(self, obs1, obs2, obs):
         """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"""