Repository: PennyLaneAI/pennylane
Pull request #1213 (2021-04-14 17:56:02)

add private helper functions for squaring Hamiltonian terms and colle…
In order to evaluate flow constraints of the cost Hamiltonian in the max weighted cycle QAOA problem, we need to reliably expand quadratic terms and simplify the resulting expressions. 

Add a private function to find the coefficients and operators of the term of the Hamiltonian to be squared. 
Add a private function to collect duplicate terms of a Hamiltonian.

**Example of current and desired behaviour:**

Using the tensor product operation `@` as currently implemented we find:

```
>>> coeffs = [0, -1, -1, 1, 1]
>>> ops = [qml.Identity(wires=[0]), qml.PauliZ(wires=[0]), qml.PauliZ(wires=[1]), qml.PauliZ(wires=[2]), qml.PauliZ(wires=[4])]
>>> H = qml.Hamiltonian(coeffs, ops)
>>> square_H = H @ H
>>> print(square_H)
  (0) [I0]
+ (1) [Z0 Z0]
+ (1) [Z1 Z1]
+ (1) [Z2 Z2]
+ (1) [Z4 Z4]
+ (-2) [Z0 Z2]
+ (-2) [Z0 Z4]
+ (-2) [Z1 Z2]
+ (-2) [Z1 Z4]
+ (2) [Z0 Z1]
+ (2) [Z2 Z4]
```

The desired output of the above example would be: 
```
  (4) [I0]
+ (-2) [Z0 Z2]
+ (-2) [Z0 Z4]
+ (-2) [Z1 Z2]
+ (-2) [Z1 Z4]
+ (2) [Z0 Z1]
+ (2) [Z2 Z4]
```

This may no longer be needed when related issues on [Hamiltonian simplification](https://github.com/PennyLaneAI/pennylane/issues/1107) are closed


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/1213?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1213](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1213?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (7fc1e76) into [qaoa_cycles_module](https://codecov.io/gh/PennyLaneAI/pennylane/commit/a4548bf25e1a0530f790d7eec9078260a4950ac7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (a4548bf) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1213/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1213?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@                 Coverage Diff                 @@
##           qaoa_cycles_module    #1213   +/-   ##
===================================================
  Coverage               98.12%   98.13%           
===================================================
  Files                     146      146           
  Lines                   10992    11028   +36     
===================================================
+ Hits                    10786    10822   +36     
  Misses                    206      206           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1213?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/qaoa/cycle.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1213/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3Fhb2EvY3ljbGUucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1213?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1213?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [a4548bf...7fc1e76](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1213?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

trbromley: Thanks @anthayes92! Looks good, could we come up with two minimal working examples that motivate why we need these functions (to be sure it's worth adding them in)?

E.g., could we have a simple Hamiltonian `H` where squaring with `H @ H` doesn't work as intended? Likewise a Hamiltonian `H` where calling the `simplify()` method doesn't work as intended?

This will also help us understand the shortfalls of the current approach in `qml.Hamiltonian`. Also, it can help us decide whether we could just make a quick change to the `qml.Hamiltonian.simplify()` method instead of having these improvements just available in the `cycle` module.

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 1cf4343c2..94f4359f1 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -60,6 +60,7 @@
   [(#1207)](https://github.com/PennyLaneAI/pennylane/pull/1207)
   [(#1209)](https://github.com/PennyLaneAI/pennylane/pull/1209)
   [(#1251)](https://github.com/PennyLaneAI/pennylane/pull/1251)
+  [(#1213)](https://github.com/PennyLaneAI/pennylane/pull/1213)
 
 * Adds `QubitCarry` and `QubitSum` operations for basic arithmetic.
   [(#1169)](https://github.com/PennyLaneAI/pennylane/pull/1169)
diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py
index 2d783f327..333197f29 100644
--- a/pennylane/qaoa/cycle.py
+++ b/pennylane/qaoa/cycle.py
@@ -14,8 +14,8 @@
 r"""
 Functionality for finding the maximum weighted cycle of directed graphs.
 """
-
-from typing import Dict, Tuple
+import itertools
+from typing import Dict, Tuple, Iterable, List
 import networkx as nx
 import numpy as np
 import pennylane as qml
@@ -272,3 +272,37 @@ def loss_hamiltonian(graph: nx.Graph) -> qml.Hamiltonian:
         ops.append(qml.PauliZ(wires=edges_to_qubits[edge]))
 
     return qml.Hamiltonian(coeffs, ops)
+
+
+def _square_hamiltonian_terms(
+    coeffs: Iterable[float], ops: Iterable[qml.operation.Observable]
+) -> Tuple[List[float], List[qml.operation.Observable]]:
+    """Calculates the coefficients and observables that compose the squared Hamiltonian.
+
+    Args:
+        coeffs (Iterable[float]): coeffients of the input Hamiltonian
+        ops (Iterable[qml.operation.Observable]): observables of the input Hamiltonian
+
+    Returns:
+        Tuple[List[float], List[qml.operation.Observable]]: The list of coefficients and list of observables
+        of the squared Hamiltonian.
+    """
+    squared_coeffs, squared_ops = [], []
+    pairs = [(coeff, op) for coeff, op in zip(coeffs, ops)]
+    products = itertools.product(pairs, repeat=2)
+
+    for (coeff1, op1), (coeff2, op2) in products:
+        squared_coeffs.append(coeff1 * coeff2)
+
+        if isinstance(op1, qml.Identity):
+            squared_ops.append(op2)
+        elif isinstance(op2, qml.Identity):
+            squared_ops.append(op1)
+        elif op1.wires == op2.wires and type(op1) == type(op2):
+            squared_ops.append(qml.Identity(0))
+        elif op2.wires[0] < op1.wires[0]:
+            squared_ops.append(op2 @ op1)
+        else:
+            squared_ops.append(op1 @ op2)
+
+    return squared_coeffs, squared_ops


Test patch:
diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py
index 397d8804a..c2daf9c4f 100644
--- a/tests/test_qaoa.py
+++ b/tests/test_qaoa.py
@@ -26,6 +26,7 @@ from pennylane.qaoa.cycle import (
     edges_to_wires,
     wires_to_edges,
     loss_hamiltonian,
+    _square_hamiltonian_terms,
     cycle_mixer,
     _partial_cycle_mixer,
 )
@@ -1007,3 +1008,56 @@ class TestCycles:
 
         with pytest.raises(KeyError, match="does not contain weight data"):
             loss_hamiltonian(g)
+
+    def test_square_hamiltonian_terms(self):
+        """Test if the _square_hamiltonian_terms function returns the expected result on a fixed
+        example"""
+        coeffs = [1, -1, -1, 1]
+        ops = [qml.Identity(0), qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(3)]
+
+        expected_coeffs = [
+            1,
+            -1,
+            -1,
+            1,
+            -1,
+            1,
+            1,
+            -1,
+            -1,
+            1,
+            1,
+            -1,
+            1,
+            -1,
+            -1,
+            1,
+        ]
+        expected_ops = [
+            qml.Identity(0),
+            qml.PauliZ(0),
+            qml.PauliZ(1),
+            qml.PauliZ(3),
+            qml.PauliZ(0),
+            qml.Identity(0),
+            qml.PauliZ(0) @ qml.PauliZ(1),
+            qml.PauliZ(0) @ qml.PauliZ(3),
+            qml.PauliZ(1),
+            qml.PauliZ(0) @ qml.PauliZ(1),
+            qml.Identity(0),
+            qml.PauliZ(1) @ qml.PauliZ(3),
+            qml.PauliZ(3),
+            qml.PauliZ(0) @ qml.PauliZ(3),
+            qml.PauliZ(1) @ qml.PauliZ(3),
+            qml.Identity(0),
+        ]
+
+        squared_coeffs, squared_ops = _square_hamiltonian_terms(coeffs, ops)
+
+        assert squared_coeffs == expected_coeffs
+        assert all(
+            [
+                op1.name == op2.name and op1.wires == op2.wires
+                for op1, op2 in zip(expected_ops, squared_ops)
+            ]
+        )