Repository: PennyLaneAI/pennylane (python)

Issue:
[BUG] Incorrect answers from qml.var due to sum hashing
### Expected behavior

Given some circuit like this,

```
dev = qml.device("default.qubit", wires=3)

@qml.qnode(dev)
def circuit_separated():
    qml.RX(np.pi / 3, 0)
    qml.RZ(np.pi / 3, 0)
    return [
        qml.var(qml.Z(0)), qml.var(qml.X(0))
    ]

>>> circuit_separated()
[tensor(0.75, requires_grad=True), tensor(0.4375, requires_grad=True)] # As expected
```

You expect the summed version to be
```
dev = qml.device("default.qubit", wires=3)

@qml.qnode(dev)
def circuit_summed():
    qml.RX(np.pi / 3, 0)
    qml.RZ(np.pi / 3, 0)
    return [
        qml.var(qml.Z(0) + qml.X(0))
    ]

>>> circuit_summed()
[tensor(0.75+0.4375, requires_grad=True)]
```

Furthermore,
When changing the circuit you expect the answer to change

```
dev = qml.device("default.qubit", wires=3)

@qml.qnode(dev)
def circuit_summed():
    qml.RX(np.pi / 3, 0)
    qml.RZ(np.pi / 3, 0)
    return [
        qml.var(qml.Z(0) + qml.X(0) + qml.Z(0) + qml.Z(0) + qml.Z(0)) # in fact you can add as many qml.Z(0) and qml.X(0)'s you want
    ]

>>> circuit_summed()
# should be some new answer
```


### Actual behavior

```
dev = qml.device("default.qubit", wires=3)
@qml.qnode(dev)
def circuit_summed():
    qml.RX(np.pi / 3, 0)
    qml.RZ(np.pi / 3, 0)
    return [
        qml.var(qml.Z(0) + qml.X(0))
    ]

circuit_summed()
>>> [tensor(0.4375, requires_grad=True)] # That's odd
```

If you don't restart kernel:
```
dev = qml.device("default.qubit", wires=3)

@qml.qnode(dev)
def circuit_summed():
    qml.RX(np.pi / 3, 0)
    qml.RZ(np.pi / 3, 0)
    return [
        qml.var(qml.Z(0) + qml.X(0) + qml.Z(0) + qml.Z(0) + qml.Z(0)) # in fact you can add as many qml.Z(0) and qml.X(0)'s you want
    ]

>>> circuit_summed()
[tensor(0.4375, requires_grad=True)] # That's odd, it's still the same answer!
```

If you restart kernel the answer changes:
```
>>> circuit_summed()
[tensor(9.4375, requires_grad=True)] # Not sure if correct though
```


### Additional information

_No response_

### Source code

_No response_

### Tracebacks

_No response_

### System information

```shell
Name: PennyLane
Version: 0.37.0.dev0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /Users/austin.huang/pennylane/pennylane-qiskit/venv/lib/python3.11/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, packaging, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane_Lightning

Platform info:           macOS-14.5-arm64-arm-64bit
Python version:          3.11.9
Numpy version:           1.26.4
Scipy version:           1.13.1
Installed devices:
- qiskit.aer (PennyLane-qiskit-0.35.0.dev0)
- qiskit.basicaer (PennyLane-qiskit-0.35.0.dev0)
- qiskit.ibmq (PennyLane-qiskit-0.35.0.dev0)
- qiskit.ibmq.circuit_runner (PennyLane-qiskit-0.35.0.dev0)
- qiskit.ibmq.sampler (PennyLane-qiskit-0.35.0.dev0)
- qiskit.remote (PennyLane-qiskit-0.35.0.dev0)
- default.clifford (PennyLane-0.37.0.dev0)
- default.gaussian (PennyLane-0.37.0.dev0)
- default.mixed (PennyLane-0.37.0.dev0)
- default.qubit (PennyLane-0.37.0.dev0)
- default.qubit.autograd (PennyLane-0.37.0.dev0)
- default.qubit.jax (PennyLane-0.37.0.dev0)
- default.qubit.legacy (PennyLane-0.37.0.dev0)
- default.qubit.tf (PennyLane-0.37.0.dev0)
- default.qubit.torch (PennyLane-0.37.0.dev0)
- default.qutrit (PennyLane-0.37.0.dev0)
- default.qutrit.mixed (PennyLane-0.37.0.dev0)
- default.tensor (PennyLane-0.37.0.dev0)
- null.qubit (PennyLane-0.37.0.dev0)
- lightning.qubit (PennyLane_Lightning-0.36.0)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.

Pull request:
fix sum hash issue

**Context:**

`Sum.hash` uses `frozenset` to calculate the hash in an order independent way.  The problem is that `frozenset` ignores the fact that some operations may occur multiple times in the sum.  This leads to an incorrect hash.

The bad hash then propagated to incorrect `eigvals` and `diagonalizing_gates`

**Description of the Change:**

Uses `collections.Counter` to count how many times the operations occur.

**Benefits:**

Correct answers.

**Possible Drawbacks:**

speed.

**Related GitHub Issues:**


Fixes #5849 [sc-65903]

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index f944e02b5..847bcf3d6 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -341,6 +341,10 @@
 
 <h3>Bug fixes 🐛</h3>
 
+* Fixes a bug where `Sum`'s with repeated identical operations ended up with the same hash as
+  `Sum`'s with different numbers of repeats.
+  [(#5851)](https://github.com/PennyLaneAI/pennylane/pull/5851)
+
 * `qml.qaoa.cost_layer` and `qml.qaoa.mixer_layer` can now be used with `Sum` operators.
   [(#5846)](https://github.com/PennyLaneAI/pennylane/pull/5846)
 
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
index dcb8f07b5..8536b7953 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -371,6 +371,9 @@ def _equal_operators(
             f"Got {op1.hyperparameters}\n and {op2.hyperparameters}."
         )
 
+    if any(qml.math.is_abstract(d) for d in op1.data + op2.data):
+        # assume all tracers are independent
+        return "Data contains a tracer. Abstract tracers are assumed to be unique."
     if not all(
         qml.math.allclose(d1, d2, rtol=rtol, atol=atol) for d1, d2 in zip(op1.data, op2.data)
     ):
diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py
index 6b40ed3a5..a8f1a0e0d 100644
--- a/pennylane/ops/op_math/sum.py
+++ b/pennylane/ops/op_math/sum.py
@@ -19,6 +19,7 @@ computing the sum of operations.
 
 import itertools
 import warnings
+from collections import Counter
 from collections.abc import Iterable
 from copy import copy
 from typing import List
@@ -240,7 +241,7 @@ class Sum(CompositeOp):
     @property
     def hash(self):
         # Since addition is always commutative, we do not need to sort
-        return hash(("Sum", frozenset(o.hash for o in self.operands)))
+        return hash(("Sum", hash(frozenset(Counter(self.operands).items()))))
 
     @property
     def grouping_indices(self):


Test patch:
diff --git a/tests/ops/functions/conftest.py b/tests/ops/functions/conftest.py
index 0be32f31e..93ff0de19 100644
--- a/tests/ops/functions/conftest.py
+++ b/tests/ops/functions/conftest.py
@@ -35,6 +35,7 @@ from pennylane.ops.op_math.pow import PowObs, PowOperation, PowOpObs
 
 _INSTANCES_TO_TEST = [
     qml.sum(qml.PauliX(0), qml.PauliZ(0)),
+    qml.sum(qml.X(0), qml.X(0), qml.Z(0), qml.Z(0)),
     qml.BasisState([1], wires=[0]),
     qml.ControlledQubitUnitary(np.eye(2), control_wires=1, wires=0),
     qml.QubitStateVector([0, 1], wires=0),
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
index e8df77ce6..62c2fa7be 100644
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -2756,3 +2756,14 @@ class DepthIncreaseOperator(Operator):
     @property
     def wires(self):
         return self._op.wires
+
+
+@pytest.mark.jax
+def test_ops_with_abstract_parameters_not_equal():
+    """Test that ops are not equal if any data is tracers."""
+
+    import jax
+
+    assert not jax.jit(qml.equal)(qml.RX(0.1, 0), qml.RX(0.1, 0))
+    with pytest.raises(AssertionError, match="Data contains a tracer"):
+        jax.jit(assert_equal)(qml.RX(0.1, 0), qml.RX(0.1, 0))
diff --git a/tests/ops/op_math/test_sum.py b/tests/ops/op_math/test_sum.py
index 0f50c7ade..cf7ddf6f1 100644
--- a/tests/ops/op_math/test_sum.py
+++ b/tests/ops/op_math/test_sum.py
@@ -660,6 +660,9 @@ class TestProperties:
         op3 = Sum(qml.PauliX("a"), qml.PauliY("b"), qml.PauliZ(-1))
         assert op3.hash != op1.hash
 
+        op4 = Sum(qml.X("a"), qml.X("a"), qml.Y("b"))
+        assert op4.hash != op1.hash
+
     @pytest.mark.parametrize("sum_method", [sum_using_dunder_method, qml.sum])
     @pytest.mark.parametrize("ops_lst", ops)
     def test_is_hermitian(self, ops_lst, sum_method):
@@ -708,6 +711,15 @@ class TestProperties:
         assert np.allclose(eig_vals, true_eigvals)
         assert np.allclose(eig_vecs, true_eigvecs)
 
+    def test_eigendecomposition_repeat_operations(self):
+        """Test that the eigendecomposition works with a repeated operation."""
+        op1 = qml.X(0) + qml.X(0) + qml.X(0)
+        op2 = qml.X(0) + qml.X(0)
+
+        assert op1.diagonalizing_gates() == op2.diagonalizing_gates()
+        assert qml.math.allclose(op1.eigvals(), np.array([-3.0, 3.0]))
+        assert qml.math.allclose(op2.eigvals(), np.array([-2.0, 2.0]))
+
     op_pauli_reps = (
         (
             qml.sum(qml.PauliX(wires=0), qml.PauliY(wires=0), qml.PauliZ(wires=0)),