Repository: PennyLaneAI/pennylane
Pull request #1082 (2021-02-11 21:10:23)

Update Hamiltonian data storing and result of ExpvalCost for trivial Hamiltonians
**Context:**

1. The methods for the `Hamiltonian` class responsible for performing arithmetic operations added in https://github.com/PennyLaneAI/pennylane/pull/765 implicitly assume that the input arguments `coeffs` and `ops` are `list` typed. This is so because some `list` specific methods such as `extend`, `copy` are being called, producing errors when `coeffs` or `ops` are other iterables than lists.
2. The `Hamiltonian.simplify` method may result in an empty `coeffs` and `ops` list when subtracting a `Hamiltonian` from itself.

**Description of the Change:**

1. Converts the `coeffs` and `ops` arguments of `Hamiltonian` to `list` internally
2. Changes `ExpvalCost` to always return `np.array(0)` in `__call__` when specifying a zero Hamiltonian (either all entries of `coeffs` are 0 or `coeffs` is an empty list)

**Benefits:**

1. Can do arithmetic operations of `Hamiltonian` objects even if instantiated with non-list arguments
2. Can use `ExpvalCost` with the difference of `Hamiltonian` objects even when that's `0`

**Possible Drawbacks:**
N/A

**Related GitHub Issues:**
N/A

**Examples**

1. Adding up `Hamiltonian` objects that take non-lists as arguments (raised a `TypeError` previously)
```python
import pennylane as qml
import numpy as np

h1 = qml.Hamiltonian((1, 1.2, 0.1), np.array([qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]))
h2 = qml.Hamiltonian(np.array([0.5, 0.3, 1]), (qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)))

print(h1+h2)
```
```
(1.5) [X0]
+ (1.2) [Z1]
+ (1.1) [X2]
+ (0.3) [X1]
```
2. `ExpvalCost` of the difference of a `Hamiltonian` and itself  (used to return the invalid `<h_minus>= False` result)
```
import pennylane as qml
import numpy as np

nr_qubits = 2

dev = qml.device('default.qubit', wires = nr_qubits)

def circuit(theta, wires):
    qml.PauliX(0)

h1 = qml.Hamiltonian([0.5], [qml.PauliZ(0) @ qml.PauliZ(1)]) 
h2 = qml.Hamiltonian([0.5], [qml.PauliZ(0) @ qml.PauliZ(1)])

h_minus = h1 - h2
print( "<h_minus>=", qml.ExpvalCost(circuit, h_minus, dev)() )
```
```
<h_minus>= 0
```

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [.github/CHANGELOG.md](/PennyLaneAI/pennylane/blob/master/.github/CHANGELOG.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://codecov.io/gh/PennyLaneAI/pennylane/pull/1082?src=pr&el=h1) Report
> Merging [#1082](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1082?src=pr&el=desc) (08968dd) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/6ecd0272b52ea31f0c3906fceaa69d2c49e269fb?el=desc) (6ecd027) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1082/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1082?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master    #1082   +/-   ##
=======================================
  Coverage   97.72%   97.72%           
=======================================
  Files         154      154           
  Lines       11652    11652           
=======================================
  Hits        11387    11387           
  Misses        265      265           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1082?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pennylane/vqe/vqe.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1082/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3ZxZS92cWUucHk=) | `93.78% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1082?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1082?src=pr&el=footer). Last update [6ecd027...08968dd](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1082?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 23cdff31c..5ff7c532b 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -91,6 +91,14 @@
 
 <h3>Bug fixes</h3>
 
+* Fixes a bug where `Hamiltonian` objects created with non-list arguments
+  raised an error for arithmetic operations.
+  [(#1082)](https://github.com/PennyLaneAI/pennylane/pull/1082)
+
+* Fixes a bug where `Hamiltonian` objects with no coefficients or operations
+  would return a faulty result when used with `ExpvalCost`.
+  [(#1082)](https://github.com/PennyLaneAI/pennylane/pull/1082)
+
 * Fixes a bug where inverse operations could not be differentiated
   using backpropagation on `default.qubit`.
   [(#1072)](https://github.com/PennyLaneAI/pennylane/pull/1072)
@@ -101,7 +109,7 @@
 
 This release contains contributions from (in alphabetical order):
 
-Thomas Bromley, Josh Izaac, Daniel Polatajko, Chase Roberts, Maria Schuld
+Thomas Bromley, Josh Izaac, Daniel Polatajko, Chase Roberts, Maria Schuld, Antal Száva.
 
 # Release 0.14.0 (current release)
 
diff --git a/pennylane/vqe/vqe.py b/pennylane/vqe/vqe.py
index 01675bafe..5250153bb 100644
--- a/pennylane/vqe/vqe.py
+++ b/pennylane/vqe/vqe.py
@@ -90,8 +90,8 @@ class Hamiltonian:
                     "Could not create circuits. Some or all observables are not valid."
                 )
 
-        self._coeffs = coeffs
-        self._ops = observables
+        self._coeffs = list(coeffs)
+        self._ops = list(observables)
 
         if simplify:
             self.simplify()
@@ -486,6 +486,10 @@ class ExpvalCost:
         self._multiple_devices = isinstance(device, Sequence)
         """Bool: Records if multiple devices are input"""
 
+        if all(c == 0 for c in coeffs) or not coeffs:
+            self.cost_fn = lambda *args, **kwargs: np.array(0)
+            return
+
         tape_mode = qml.tape_mode_active()
         self._optimize = optimize
 


Test patch:
diff --git a/tests/test_vqe.py b/tests/test_vqe.py
index 373c46cee..069325cd4 100644
--- a/tests/test_vqe.py
+++ b/tests/test_vqe.py
@@ -111,6 +111,14 @@ hamiltonians_with_expvals = [
     ((0.5, 1.2), (qml.PauliZ(0), qml.PauliZ(1)), [0.5 * 1.0, 1.2 * 1.0]),
 ]
 
+
+
+zero_hamiltonians_with_expvals = [
+    ([], [], [0]),
+    ((0, 0), (qml.PauliZ(0), qml.PauliZ(1)), [0]),
+    ((0,0,0), (qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(1)), [0]),
+]
+
 simplify_hamiltonians = [
     (
         qml.Hamiltonian([1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(1)]),
@@ -143,6 +151,20 @@ simplify_hamiltonians = [
             [qml.Hermitian(np.array([[1, 0], [0, -1]]), "a"), qml.PauliX("b") @ qml.PauliY(1.3)],
         ),
     ),
+
+    # Simplifies to zero Hamiltonian
+    (
+        qml.Hamiltonian([1, -0.5, -0.5], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(0)]),
+        qml.Hamiltonian([], []),
+    ),
+    (
+        qml.Hamiltonian([1, -1], [qml.PauliX(4) @ qml.Identity(0) @ qml.PauliX(1), qml.PauliX(4) @ qml.PauliX(1)]),
+        qml.Hamiltonian([], []),
+    ),
+    (
+        qml.Hamiltonian([0], [qml.Identity(0)]),
+        qml.Hamiltonian([0], [qml.Identity(0)]),
+    ),
 ]
 
 equal_hamiltonians = [
@@ -231,6 +253,15 @@ add_hamiltonians = [
         qml.PauliX("b") @ qml.Identity(5),
         qml.Hamiltonian([2, 1.2, 0.1], [qml.PauliX("b"), qml.PauliZ(3.1), qml.PauliX(1.6)]),
     ),
+
+    # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists
+    (
+        qml.Hamiltonian((1, 1.2, 0.1), (qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2))),
+        qml.Hamiltonian(np.array([0.5, 0.3, 1]), np.array([qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)])),
+        qml.Hamiltonian(
+            (1.5, 1.2, 1.1, 0.3), np.array([qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)])
+        ),
+    ),
 ]
 
 sub_hamiltonians = [
@@ -276,6 +307,28 @@ sub_hamiltonians = [
         qml.PauliX("b") @ qml.Identity(1),
         qml.Hamiltonian([1.2, 0.1], [qml.PauliZ(3.1), qml.PauliX(1.6)]),
     ),
+
+    # The result is the zero Hamiltonian
+    (
+        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([], []),
+    ),
+    (
+        qml.Hamiltonian([1, 2], [qml.PauliX(4), qml.PauliZ(2)]),
+        qml.Hamiltonian([1, 2], [qml.PauliX(4), qml.PauliZ(2)]),
+        qml.Hamiltonian([], []),
+    ),
+
+
+    # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists
+    (
+        qml.Hamiltonian((1, 1.2, 0.1), (qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2))),
+        qml.Hamiltonian(np.array([0.5, 0.3, 1.6]), np.array([qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)])),
+        qml.Hamiltonian(
+            (0.5, 1.2, -1.5, -0.3), np.array([qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)])
+        ),
+    ),
 ]
 
 mul_hamiltonians = [
@@ -300,6 +353,25 @@ mul_hamiltonians = [
             [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), qml.PauliZ(23) @ qml.PauliZ(0)],
         ),
     ),
+
+    # The result is the zero Hamiltonian
+    (
+        0,
+        qml.Hamiltonian([1], [qml.PauliX(0)]),
+        qml.Hamiltonian([0], [qml.PauliX(0)]),
+    ),
+    (
+        0,
+        qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]),
+        qml.Hamiltonian([0, 0, 0], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]),
+    ),
+
+    # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists
+    (
+        3,
+        qml.Hamiltonian((1.5, 0.5), (qml.PauliX(0), qml.PauliZ(1))),
+        qml.Hamiltonian(np.array([4.5, 1.5]), np.array([qml.PauliX(0), qml.PauliZ(1)])),
+    ),
 ]
 
 matmul_hamiltonians = [
@@ -347,6 +419,21 @@ matmul_hamiltonians = [
         qml.PauliX(2),
         qml.Hamiltonian([1, 1], [qml.PauliX(0) @ qml.PauliX(2), qml.PauliZ(1) @ qml.PauliX(2)]),
     ),
+
+    # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists
+    (
+        qml.Hamiltonian((1, 1), (qml.PauliX(0), qml.PauliZ(1))),
+        qml.Hamiltonian(np.array([0.5, 0.5]), np.array([qml.PauliZ(2), qml.PauliZ(3)])),
+        qml.Hamiltonian(
+            (0.5, 0.5, 0.5, 0.5),
+            np.array([
+                qml.PauliX(0) @ qml.PauliZ(2),
+                qml.PauliX(0) @ qml.PauliZ(3),
+                qml.PauliZ(1) @ qml.PauliZ(2),
+                qml.PauliZ(1) @ qml.PauliZ(3),
+            ]),
+        ),
+    ),
 ]
 
 big_hamiltonian_coeffs = np.array(
@@ -507,7 +594,7 @@ class TestHamiltonian:
         """Tests that the Hamiltonian object is created with
         the correct attributes"""
         H = qml.vqe.Hamiltonian(coeffs, ops)
-        assert H.terms == (coeffs, ops)
+        assert H.terms == (list(coeffs), list(ops))
 
     @pytest.mark.parametrize("coeffs, ops", invalid_hamiltonians)
     def test_hamiltonian_invalid_init_exception(self, coeffs, ops):
@@ -709,7 +796,7 @@ class TestVQE:
         assert type(expval(params)) == np.float64
         assert np.shape(expval(params)) == ()  # expval should be scalar
 
-    @pytest.mark.parametrize("coeffs, observables, expected", hamiltonians_with_expvals)
+    @pytest.mark.parametrize("coeffs, observables, expected", hamiltonians_with_expvals + zero_hamiltonians_with_expvals)
     def test_cost_expvals(self, coeffs, observables, expected):
         """Tests that the cost function returns correct expectation values"""
         dev = qml.device("default.qubit", wires=2)
@@ -824,6 +911,25 @@ class TestVQE:
         assert np.allclose(dc, big_hamiltonian_grad)
         assert np.allclose(dc2, big_hamiltonian_grad)
 
+    @pytest.mark.parametrize('opt', [True, False])
+    def test_grad_zero_hamiltonian(self, opt):
+        """Test that the gradient of ExpvalCost is accessible and correct when using observable
+        optimization and the autograd interface with a zero Hamiltonian."""
+        if not qml.tape_mode_active():
+            pytest.skip("This test is only intended for tape mode")
+
+        dev = qml.device("default.qubit", wires=4)
+        hamiltonian = qml.Hamiltonian([0], [qml.PauliX(0)])
+
+        cost = qml.ExpvalCost(
+            qml.templates.StronglyEntanglingLayers, hamiltonian, dev, optimize=opt, diff_method="parameter-shift"
+        )
+
+        w = qml.init.strong_ent_layers_uniform(2, 4, seed=1967)
+
+        dc = qml.grad(cost)(w)
+        assert np.allclose(dc, 0)
+
     def test_optimize_grad_torch(self, torch_support):
         """Test that the gradient of ExpvalCost is accessible and correct when using observable
         optimization and the Torch interface."""