Repository: PennyLaneAI/pennylane

Issue:
Hamiltonians created via addition produce incorrect results with `ExpvalCost(optimize=True)`
Good afternoon, 
I was having problem with vqe when I set the flag: optmize = True . I noticed that there were errors when entering Hamiltonians of the form Z1@Z2 + Z2@Z1. We know it is the same as 2.Z1.Z2 but the optimizer seems to have some kind of problem with this kind of Hamiltonians (vqe with optimize = False works correctly in this situation).
Although by preprocessing the Hamiltonian we avoid the problem, I was writing to take the problem into account.
Greetings!


Discussion:
Thanks for reporting this @KetpuntoG! Someone from the team will help investigate and pin down this bug shortly 🙂 
Hi @KetpuntoG,

Thank you for drawing our attention to this! :slightly_smiling_face: Could you perhaps post a non-working example?

I attempted to reproduce the error, though, with a naive example, the `optimize=False` and the `optimize=True` cases seem to produce the same results:
```python
import pennylane as qml
import numpy as np

dev = qml.device('default.qubit', wires=4)

coeffs = [1]
obs1 = [
    qml.PauliZ(0) @ qml.PauliZ(1)
]
H1 = qml.vqe.Hamiltonian(coeffs, obs1)

obs2 = [
    qml.PauliZ(1) @ qml.PauliZ(0)
]
H2 = qml.vqe.Hamiltonian(coeffs, obs2)

ansatz = qml.templates.StronglyEntanglingLayers
dev = qml.device("default.qubit", wires=4)

params = np.ones((3,4,3))

cost1 = qml.ExpvalCost(ansatz, H1 + H2, dev, parallel=False)
cost2 = qml.ExpvalCost(ansatz, H1 + H2, dev, parallel=True)
print(np.allclose(cost1(params), cost2(params)))
```
```
True
```
When checking the terms of the Hamiltonian that is the sum of `H1` and `H2`, it seems that we get the result you suggested we would expect:
```python
print((H1 + H2).terms)
```
```
([2], [PauliZ(wires=[0]) @ PauliZ(wires=[1])])
```
Just for internally keeping track of bugs, removing the bug label for now, but I'll readd once we've tracked down the underlying specific issue. :slightly_smiling_face: 
Hi! Here is part of the code. In the first part I am creating a 19 qubit Hamiltonian with random coefficients:


```
import pennylane as qml
from pennylane import numpy as np
from time import time

prob = 0.5 
size = 19
shots = 1000
obs = []
for j in range(size):
    if np.random.rand() < prob :obs.append(qml.PauliZ(wires = j))
        
    for i in range(size):
        if np.random.rand() < prob :obs.append(qml.PauliZ(wires = j) @ qml.PauliZ(wires = i))

coefs = (np.random.rand(len(obs))-0.5)*2

H = qml.Hamiltonian(coefs, obs)


def ansantz(w, wires = list(range(size))):
    for i in wires:
        qml.RX(w[i], wires = i)
        
        
def vqe(w):
    dev2 = qml.device("default.qubit", size, shots = shots)
    cost_fn = qml.ExpvalCost(ansantz, H, dev2, optimize = False)
    return cost_fn(w)

def vqe_compact(w):
    dev2 = qml.device("default.qubit", size, shots = shots)
    cost_fn = qml.ExpvalCost(ansantz, H, dev2, optimize = True)
    return cost_fn(w)


def duration(f, w):
    time1 = time()
    print("value:", f(w))
    sol = time() - time1
    print("time:", sol)
    
w = np.random.rand(size)*2*np.pi

print("vqe")
duration(vqe, w)
print("vqe compact")
duration(vqe_compact, w)

```
In my case the output is:

vqe
value: -4.667471020167216
time: 50.281818866729736
vqe compact
value: -8.409149436899536
time: 3.5194497108459473


Hi @KetpuntoG, thank you! Definitely something odd going on, we'll be investigating this. Thanks again for catching and reporting it. :)

Resolving patch:
diff --git a/pennylane/grouping/group_observables.py b/pennylane/grouping/group_observables.py
--- a/pennylane/grouping/group_observables.py
+++ b/pennylane/grouping/group_observables.py
@@ -15,15 +15,18 @@
 This module contains the high-level Pauli-word-partitioning functionality used in measurement optimization.
 """
 
-from pennylane.wires import Wires
+from copy import copy
+
+import numpy as np
+
+from pennylane.grouping.graph_colouring import largest_first, recursive_largest_first
 from pennylane.grouping.utils import (
-    observables_to_binary_matrix,
-    binary_to_pauli,
     are_identical_pauli_words,
+    binary_to_pauli,
+    observables_to_binary_matrix,
     qwc_complement_adj_matrix,
 )
-from pennylane.grouping.graph_colouring import largest_first, recursive_largest_first
-import numpy as np
+from pennylane.wires import Wires
 
 GROUPING_TYPES = frozenset(["qwc", "commuting", "anticommuting"])
 GRAPH_COLOURING_METHODS = {"lf": largest_first, "rlf": recursive_largest_first}
@@ -236,11 +239,16 @@ def group_observables(observables, coefficients=None, grouping_type="qwc", metho
 
     partitioned_coeffs = [[0] * len(g) for g in partitioned_paulis]
 
+    observables = copy(observables)
+    coefficients = copy(coefficients)
     for i, partition in enumerate(partitioned_paulis):
         for j, pauli_word in enumerate(partition):
             for observable in observables:
                 if are_identical_pauli_words(pauli_word, observable):
-                    partitioned_coeffs[i][j] = coefficients[observables.index(observable)]
+                    ind = observables.index(observable)
+                    partitioned_coeffs[i][j] = coefficients[ind]
+                    observables.pop(ind)
+                    coefficients.pop(ind)
                     break
 
     return partitioned_paulis, partitioned_coeffs


Test patch:
diff --git a/tests/grouping/test_optimize_measurements.py b/tests/grouping/test_optimize_measurements.py
--- a/tests/grouping/test_optimize_measurements.py
+++ b/tests/grouping/test_optimize_measurements.py
@@ -111,6 +111,38 @@ def test_optimize_measurements_qwc_generic_case_with_coefficients(self):
             grouped_coeffs[i] == grouped_coeffs_sol[i] for i in range(len(grouped_coeffs_sol))
         )
 
+    @pytest.mark.parametrize(
+        "obs",
+        [
+            [PauliZ(0), PauliZ(0)],
+            [PauliX(0) @ PauliX(1), PauliX(0) @ PauliX(1)],
+            [PauliX(0) @ PauliX(1), PauliX(1) @ PauliX(0)],
+        ],
+    )
+    def test_optimize_measurements_qwc_term_multiple_times(self, obs):
+        """Tests if coefficients are properly re-structured even if the same
+        terms appear multiple times.
+
+        Although it should be fair to assume that the terms are unique in the
+        Hamiltonian, making sure that grouping happens even with terms appearing
+        multiple times can ensure that there is no unexpected behaviour.
+        """
+        coefficients = [1.43, 4.21]
+
+        diagonalized_groupings_sol = [obs]
+
+        grouped_coeffs_sol = [[1.43, 4.21]]
+
+        grouped_coeffs = optimize_measurements(
+            obs, coefficients, grouping="qwc", colouring_method="rlf"
+        )[2]
+
+        assert len(grouped_coeffs) == len(grouped_coeffs)
+
+        assert all(
+            grouped_coeffs[i] == grouped_coeffs_sol[i] for i in range(len(grouped_coeffs_sol))
+        )
+
     def test_optimize_measurements_not_implemented_catch(self):
         """Tests that NotImplementedError is raised for methods other than ``'qwc'``."""
 
diff --git a/tests/test_vqe.py b/tests/test_vqe.py
--- a/tests/test_vqe.py
+++ b/tests/test_vqe.py
@@ -990,6 +990,62 @@ def test_optimize(self, interface, tf_support, torch_support):
 
         assert np.allclose(c1, c2)
 
+    @pytest.mark.parametrize("interface", ["tf", "torch", "autograd"])
+    def test_optimize_multiple_terms(self, interface, tf_support, torch_support):
+        """Test that an ExpvalCost with observable optimization gives the same
+        result as another ExpvalCost without observable optimization even when there
+        are non-unique Hamiltonian terms."""
+        if interface == "tf" and not tf_support:
+            pytest.skip("This test requires TensorFlow")
+        if interface == "torch" and not torch_support:
+            pytest.skip("This test requires Torch")
+
+        dev = qml.device("default.qubit", wires=5)
+        obs = [
+            qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[4]),  # <---- These two terms
+            qml.PauliZ(wires=[4]) @ qml.PauliZ(wires=[2]),  # <---- are equal
+            qml.PauliZ(wires=[1]),
+            qml.PauliZ(wires=[2]),
+            qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[2]),
+            qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[0]),
+            qml.PauliZ(wires=[3]) @ qml.PauliZ(wires=[1]),
+            qml.PauliZ(wires=[4]) @ qml.PauliZ(wires=[3]),
+        ]
+
+        coefs = (np.random.rand(len(obs)) - 0.5) * 2
+        hamiltonian = qml.Hamiltonian(coefs, obs)
+
+        cost = qml.ExpvalCost(
+            qml.templates.StronglyEntanglingLayers,
+            hamiltonian,
+            dev,
+            optimize=True,
+            interface=interface,
+            diff_method="parameter-shift",
+        )
+        cost2 = qml.ExpvalCost(
+            qml.templates.StronglyEntanglingLayers,
+            hamiltonian,
+            dev,
+            optimize=False,
+            interface=interface,
+            diff_method="parameter-shift",
+        )
+
+        w = qml.init.strong_ent_layers_uniform(2, 5, seed=1967)
+
+        c1 = cost(w)
+        exec_opt = dev.num_executions
+        dev._num_executions = 0
+
+        c2 = cost2(w)
+        exec_no_opt = dev.num_executions
+
+        assert exec_opt == 1  # Number of groups in the Hamiltonian
+        assert exec_no_opt == 8
+
+        assert np.allclose(c1, c2)
+
     def test_optimize_grad(self):
         """Test that the gradient of ExpvalCost is accessible and correct when using observable
         optimization and the autograd interface."""