Repository: PennyLaneAI/pennylane
Pull request #2103 (2022-01-18 17:08:45)

Add a modified simplify function for building Hamiltonians
**Context:**
Add a modified version of the simplify function to the `hf` module. This functions combines redundant terms in a Hamiltonian and eliminates terms with zero coefficients.

**Description of the Change:**
Function `_simplify` is added to `qml.hf.hamiltonian`.

**Benefits:**
The new function makes construction of molecular Hamiltonians more efficient. For LiH, as an example, the time to construct the Hamiltonian is reduced from ~200 s to ~10 s.

**Possible Drawbacks:**

**Related GitHub Issues:**


github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.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/2103?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#2103](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (8e0ce7b) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/3520fa9c0c3fed972c80302c54564976fcef3415?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (3520fa9) will **decrease** coverage by `2.22%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/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/2103?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@            Coverage Diff             @@
##           master    #2103      +/-   ##
==========================================
- Coverage   99.19%   96.96%   -2.23%     
==========================================
  Files         228      228              
  Lines       17531    17533       +2     
==========================================
- Hits        17390    17001     -389     
- Misses        141      532     +391     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/hf/hamiltonian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2hmL2hhbWlsdG9uaWFuLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/hf/tapering.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2hmL3RhcGVyaW5nLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/interfaces/batch/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvYmF0Y2gvamF4LnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvamF4LnB5) | `0.00% <0.00%> (-97.44%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF9qYXgucHk=) | `0.00% <0.00%> (-96.63%)` | :arrow_down: |
| [pennylane/qnn/cost.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3Fubi9jb3N0LnB5) | `42.85% <0.00%> (-57.15%)` | :arrow_down: |
| [pennylane/math/multi\_dispatch.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvbXVsdGlfZGlzcGF0Y2gucHk=) | `76.68% <0.00%> (-23.32%)` | :arrow_down: |
| [pennylane/transforms/classical\_jacobian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvY2xhc3NpY2FsX2phY29iaWFuLnB5) | `77.08% <0.00%> (-22.92%)` | :arrow_down: |
| [pennylane/math/utils.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvdXRpbHMucHk=) | `81.81% <0.00%> (-18.19%)` | :arrow_down: |
| [pennylane/math/is\_independent.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvaXNfaW5kZXBlbmRlbnQucHk=) | `88.50% <0.00%> (-11.50%)` | :arrow_down: |
| ... and [15 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103?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/2103?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [3520fa9...8e0ce7b](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2103?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).


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 48ed2d4f2..405f61e95 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -4,6 +4,14 @@
 
 <h3>New features since last release</h3>
 
+* Added a modified version of the `simplify` function to the `hf` module.
+  [(#2103)](https://github.com/PennyLaneAI/pennylane/pull/2103)
+
+  This function combines redundant terms in a Hamiltonian and eliminates terms with a coefficient
+  smaller than a cutoff value. The new function makes construction of molecular Hamiltonians more
+  efficient. For LiH, as an example, the time to construct the Hamiltonian is reduced roughly by a
+  factor of 20.
+
 * The JAX interface now supports evaluating vector-valued QNodes. Vector-valued
   QNodes include those with:
   * `qml.probs`;
diff --git a/pennylane/hf/hamiltonian.py b/pennylane/hf/hamiltonian.py
index 160a7651d..3479a3951 100644
--- a/pennylane/hf/hamiltonian.py
+++ b/pennylane/hf/hamiltonian.py
@@ -247,7 +247,8 @@ def generate_hamiltonian(mol, cutoff=1.0e-12, core=None, active=None):
         for n, t in enumerate(h_ferm[1]):
 
             if len(t) == 0:
-                coeffs = np.array([h_ferm[0][n]])
+                coeffs = anp.array([0.0])
+                coeffs = coeffs + np.array([h_ferm[0][n]])
                 ops = ops + [qml.Identity(0)]
 
             elif len(t) == 2:
@@ -282,7 +283,7 @@ def generate_hamiltonian(mol, cutoff=1.0e-12, core=None, active=None):
                     coeffs = np.concatenate([coeffs, np.array(op[0]) * h_ferm[0][n]])
                     ops = ops + op[1]
 
-        h = qml.Hamiltonian(coeffs, ops, simplify=True)
+        h = simplify(qml.Hamiltonian(coeffs, ops), cutoff=cutoff)
 
         return h
 
@@ -355,6 +356,54 @@ def _generate_qubit_operator(op):
     return c, o
 
 
+def simplify(h, cutoff=1.0e-12):
+    r"""Add together identical terms in the Hamiltonian.
+
+    The Hamiltonian terms with identical Pauli words are added together and eliminated if the
+    overall coefficient is smaller than a cutoff value.
+
+    Args:
+        h (Hamiltonian): PennyLane Hamiltonian
+        cutoff (float): cutoff value for discarding the negligible terms
+
+    Returns:
+        Hamiltonian: Simplified PennyLane Hamiltonian
+
+    **Example**
+
+    >>> c = np.array([0.5, 0.5])
+    >>> h = qml.Hamiltonian(c, [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliY(1)])
+    >>> print(simplify(h))
+    (1.0) [X0 Y1]
+    """
+    wiremap = dict(zip(h.wires, range(len(h.wires) + 1)))
+
+    c = []
+    o = []
+    for i, op in enumerate(h.terms[1]):
+        op = qml.operation.Tensor(op).prune()
+        op = qml.grouping.pauli_word_to_string(op, wire_map=wiremap)
+        if op not in o:
+            c.append(h.terms[0][i])
+            o.append(op)
+        else:
+            c[o.index(op)] += h.terms[0][i]
+
+    coeffs = []
+    ops = []
+    nonzero_ind = np.argwhere(abs(np.array(c)) > cutoff).flatten()
+    for i in nonzero_ind:
+        coeffs.append(c[i])
+        ops.append(qml.grouping.string_to_pauli_word(o[i], wire_map=wiremap))
+
+    try:
+        coeffs = qml.math.stack(coeffs)
+    except ValueError:
+        pass
+
+    return qml.Hamiltonian(coeffs, ops)
+
+
 def _pauli_mult(p1, p2):
     r"""Return the result of multiplication between two tensor products of Pauli operators.
 
diff --git a/pennylane/hf/tapering.py b/pennylane/hf/tapering.py
index b1ef7c949..184ddce97 100644
--- a/pennylane/hf/tapering.py
+++ b/pennylane/hf/tapering.py
@@ -17,11 +17,12 @@ This module contains the functions needed for tapering qubits using symmetries.
 # pylint: disable=unnecessary-lambda
 
 import functools
-import numpy
+
 import autograd.numpy as anp
+import numpy
 import pennylane as qml
 from pennylane import numpy as np
-from pennylane.hf.hamiltonian import _generate_qubit_operator
+from pennylane.hf.hamiltonian import _generate_qubit_operator, simplify
 
 
 def _binary_matrix(terms, num_qubits, wire_map=None):
@@ -193,7 +194,7 @@ def get_generators(nullspace, num_qubits):
             x, z = op
             tau @= pauli_map[f"{x}{z}"](idx)
 
-        ham = qml.Hamiltonian([1.0], [tau], simplify=True)
+        ham = simplify(qml.Hamiltonian([1.0], [tau]))
         generators.append(ham)
 
     return generators
@@ -328,54 +329,7 @@ def _observable_mult(obs_a, obs_b):
             o.append(op)
             c.append(phase * obs_a.terms[0][i] * obs_b.terms[0][j])
 
-    return _simplify(qml.Hamiltonian(qml.math.stack(c), o))
-
-
-def _simplify(h, cutoff=1.0e-12):
-    r"""Add together identical terms in the Hamiltonian.
-
-    The Hamiltonian terms with identical Pauli words are added together and eliminated if the
-    overall coefficient is zero.
-
-    Args:
-        h (Hamiltonian): PennyLane Hamiltonian
-        cutoff (float): cutoff value for discarding the negligible terms
-
-    Returns:
-        Hamiltonian: Simplified PennyLane Hamiltonian
-
-    **Example**
-
-    >>> c = np.array([0.5, 0.5])
-    >>> h = qml.Hamiltonian(c, [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliY(1)])
-    >>> print(_simplify(h))
-    (1.0) [X0 Y1]
-    """
-    s = []
-    wiremap = dict(zip(h.wires, range(len(h.wires) + 1)))
-
-    for term in h.terms[1]:
-        term = qml.operation.Tensor(term).prune()
-        s.append(qml.grouping.pauli_word_to_string(term, wire_map=wiremap))
-
-    o = list(set(s))
-    c = [0.0] * len(o)
-    for i, item in enumerate(s):
-        c[o.index(item)] += h.terms[0][i]
-    c = qml.math.stack(c)
-
-    coeffs = []
-    ops = []
-    nonzero_ind = np.argwhere(abs(c) > cutoff).flatten()
-    for i in nonzero_ind:
-        coeffs.append(c[i])
-        ops.append(qml.grouping.string_to_pauli_word(o[i], wire_map=wiremap))
-    try:
-        coeffs = qml.math.stack(coeffs)
-    except ValueError:
-        pass
-
-    return qml.Hamiltonian(coeffs, ops)
+    return simplify(qml.Hamiltonian(qml.math.stack(c), o))
 
 
 def clifford(generators, paulix_ops):
@@ -478,7 +432,7 @@ def transform_hamiltonian(h, generators, paulix_ops, paulix_sector):
     c = anp.multiply(val, h.terms[0])
     c = qml.math.stack(c)
 
-    return _simplify(qml.Hamiltonian(c, o))
+    return simplify(qml.Hamiltonian(c, o))
 
 
 def optimal_sector(qubit_op, generators, active_electrons):


Test patch:
diff --git a/tests/hf/test_hamiltonians.py b/tests/hf/test_hamiltonians.py
index a0ece4bda..26273bdb5 100644
--- a/tests/hf/test_hamiltonians.py
+++ b/tests/hf/test_hamiltonians.py
@@ -23,6 +23,7 @@ from pennylane.hf.hamiltonian import (
     _generate_qubit_operator,
     _pauli_mult,
     _return_pauli,
+    simplify,
     generate_electron_integrals,
     generate_fermionic_hamiltonian,
     generate_hamiltonian,
@@ -379,3 +380,49 @@ def test_gradient_expvalH():
     grad_finitediff = (e_2 - e_1) / 0.0002
 
     assert np.allclose(grad_autograd[0][0], grad_finitediff)
+
+
+@pytest.mark.parametrize(
+    ("hamiltonian", "result"),
+    [
+        (
+            qml.Hamiltonian(
+                np.array([0.5, 0.5]), [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliY(1)]
+            ),
+            qml.Hamiltonian(np.array([1.0]), [qml.PauliX(0) @ qml.PauliY(1)]),
+        ),
+        (
+            qml.Hamiltonian(
+                np.array([0.5, -0.5]),
+                [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliY(1)],
+            ),
+            qml.Hamiltonian([], []),
+        ),
+        (
+            qml.Hamiltonian(
+                np.array([0.0, -0.5]),
+                [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliZ(1)],
+            ),
+            qml.Hamiltonian(np.array([-0.5]), [qml.PauliX(0) @ qml.PauliZ(1)]),
+        ),
+        (
+            qml.Hamiltonian(
+                np.array([0.25, 0.25, 0.25, -0.25]),
+                [
+                    qml.PauliX(0) @ qml.PauliY(1),
+                    qml.PauliX(0) @ qml.PauliZ(1),
+                    qml.PauliX(0) @ qml.PauliY(1),
+                    qml.PauliX(0) @ qml.PauliY(1),
+                ],
+            ),
+            qml.Hamiltonian(
+                np.array([0.25, 0.25]),
+                [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliZ(1)],
+            ),
+        ),
+    ],
+)
+def test_simplify(hamiltonian, result):
+    r"""Test that simplify returns the correct hamiltonian."""
+    h = simplify(hamiltonian)
+    assert h.compare(result)
diff --git a/tests/hf/test_tapering.py b/tests/hf/test_tapering.py
index 9609d7740..2ae42005c 100644
--- a/tests/hf/test_tapering.py
+++ b/tests/hf/test_tapering.py
@@ -15,16 +15,16 @@
 Unit tests for functions needed for qubit tapering.
 """
 import functools
-import scipy
-import pytest
+
 import pennylane as qml
+import pytest
+import scipy
 from pennylane import numpy as np
 from pennylane.hf.tapering import (
     _binary_matrix,
     _kernel,
     _observable_mult,
     _reduced_row_echelon,
-    _simplify,
     clifford,
     generate_paulis,
     generate_symmetries,
@@ -358,52 +358,6 @@ def test_observable_mult(obs_a, obs_b, result):
     assert o.compare(result)
 
 
-@pytest.mark.parametrize(
-    ("hamiltonian", "result"),
-    [
-        (
-            qml.Hamiltonian(
-                np.array([0.5, 0.5]), [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliY(1)]
-            ),
-            qml.Hamiltonian(np.array([1.0]), [qml.PauliX(0) @ qml.PauliY(1)]),
-        ),
-        (
-            qml.Hamiltonian(
-                np.array([0.5, -0.5]),
-                [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliY(1)],
-            ),
-            qml.Hamiltonian([], []),
-        ),
-        (
-            qml.Hamiltonian(
-                np.array([0.0, -0.5]),
-                [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliZ(1)],
-            ),
-            qml.Hamiltonian(np.array([-0.5]), [qml.PauliX(0) @ qml.PauliZ(1)]),
-        ),
-        (
-            qml.Hamiltonian(
-                np.array([0.25, 0.25, 0.25, -0.25]),
-                [
-                    qml.PauliX(0) @ qml.PauliY(1),
-                    qml.PauliX(0) @ qml.PauliZ(1),
-                    qml.PauliX(0) @ qml.PauliY(1),
-                    qml.PauliX(0) @ qml.PauliY(1),
-                ],
-            ),
-            qml.Hamiltonian(
-                np.array([0.25, 0.25]),
-                [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliZ(1)],
-            ),
-        ),
-    ],
-)
-def test_simplify(hamiltonian, result):
-    r"""Test that simplify returns the correct hamiltonian."""
-    h = _simplify(hamiltonian)
-    assert h.compare(result)
-
-
 @pytest.mark.parametrize(
     ("generator", "paulix_ops", "result"),
     [