Repository: PennyLaneAI/pennylane
Pull request #1550 (2021-08-18 09:37:34)

Make `sparse_hamiltonian` function wire label independent
The function in `qml.utils.sparse_hamiltonian` currently has two limitations which can cause edge cases to fail:

1) it expects Hamiltonians using integer-valued wire labels 
2) it expects Hamiltonians whose observables are tensor product of single-qubit operators

This PR fixes the first issue and tests it. 

The second issue is more complicated, since we would need to compute tensor products of operators such as `qml.PauliX(1) @ qml.Hermitian(..,wires=[0,1])`, but this PR adds at least a check for this case.



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

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

```diff
@@           Coverage Diff           @@
##           master    #1550   +/-   ##
=======================================
  Coverage   98.79%   98.79%           
=======================================
  Files         187      187           
  Lines       13395    13398    +3     
=======================================
+ Hits        13233    13236    +3     
  Misses        162      162           
```


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

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1550?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/1550?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [2cae58d...4e49f97](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1550?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/.github/CHANGELOG.md b/.github/CHANGELOG.md
index b7b1cb65b..77151a0cd 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -91,6 +91,11 @@
 
 <h3>Improvements</h3>
 
+* The `utils.sparse_hamiltonian` function can now deal with non-integer 
+  wire labels, and it throws an error for the edge case of observables that are 
+  created from multi-qubit operations.
+  [(#1550)](https://github.com/PennyLaneAI/pennylane/pull/1550)
+
 * Added the matrix attribute to `qml.templates.subroutines.GroverOperator`
   [(#1553)](https://github.com/PennyLaneAI/pennylane/pull/1553)
 
diff --git a/pennylane/utils.py b/pennylane/utils.py
index 391f3b260..f83b8b026 100644
--- a/pennylane/utils.py
+++ b/pennylane/utils.py
@@ -106,12 +106,14 @@ def decompose_hamiltonian(H, hide_identity=False):
     return coeffs, obs
 
 
-def sparse_hamiltonian(H):
+def sparse_hamiltonian(H, wires=None):
     r"""Computes the sparse matrix representation a Hamiltonian in the computational basis.
 
     Args:
         H (~.Hamiltonian): Hamiltonian operator for which the matrix representation should be
          computed
+        wires (Iterable): Wire labels that indicate the order of wires according to which the matrix
+         is constructed. If not profided, ``H.wires`` is used.
 
     Returns:
         coo_matrix: a sparse matrix in scipy coordinate list (COO) format with dimension
@@ -140,18 +142,32 @@ def sparse_hamiltonian(H):
     if not isinstance(H, qml.Hamiltonian):
         raise TypeError("Passed Hamiltonian must be of type `qml.Hamiltonian`")
 
-    n = len(H.wires)
+    if wires is None:
+        wires = H.wires
+
+    n = len(wires)
     matrix = scipy.sparse.coo_matrix((2 ** n, 2 ** n), dtype="complex128")
 
-    for coeffs, ops in zip(H.coeffs, H.ops):
+    for coeff, op in zip(H.coeffs, H.ops):
+
+        obs = []
+        for o in qml.operation.Tensor(op).obs:
+            if len(o.wires) > 1:
+                # todo: deal with operations created from multi-qubit operations such as Hermitian
+                raise ValueError(
+                    "Can only sparsify Hamiltonians whose constituent observables consist of "
+                    "(tensor products of) single-qubit operators; got {}.".format(op)
+                )
+            obs.append(scipy.sparse.coo_matrix(o.matrix))
 
-        obs = [scipy.sparse.coo_matrix(o.matrix) for o in qml.operation.Tensor(ops).obs]
         mat = [scipy.sparse.eye(2, format="coo")] * n
 
-        for i, j in enumerate(ops.wires):
-            mat[j] = obs[i]
+        for i, wire in enumerate(op.wires):
+            # find index of this wire in the ordering
+            idx = wires.index(wire)
+            mat[idx] = obs[i]
 
-        matrix += functools.reduce(lambda i, j: scipy.sparse.kron(i, j, format="coo"), mat) * coeffs
+        matrix += functools.reduce(lambda i, j: scipy.sparse.kron(i, j, format="coo"), mat) * coeff
 
     return matrix.tocoo()
 


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 391e44749..886dba15f 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -140,11 +140,12 @@ class TestSparse:
     """Tests the sparse_hamiltonian function"""
 
     @pytest.mark.parametrize(
-        ("coeffs", "obs", "ref_matrix"),
+        ("coeffs", "obs", "wires", "ref_matrix"),
         [
             (
                 [1, -0.45],
                 [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1)],
+                None,
                 np.array(
                     [
                         [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.45j, 0.0 + 0.0j],
@@ -154,6 +155,95 @@ class TestSparse:
                     ]
                 ),
             ),
+            (
+                [0.1],
+                [qml.PauliZ("b") @ qml.PauliX("a")],
+                ["a", "c", "b"],
+                np.array(
+                    [
+                        [
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.1 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                        ],
+                        [
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            -0.1 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                        ],
+                        [
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.1 + 0.0j,
+                            0.0 + 0.0j,
+                        ],
+                        [
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            -0.1 + 0.0j,
+                        ],
+                        [
+                            0.1 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                        ],
+                        [
+                            0.0 + 0.0j,
+                            -0.1 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                        ],
+                        [
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.1 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                        ],
+                        [
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            -0.1 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                            0.0 + 0.0j,
+                        ],
+                    ]
+                ),
+            ),
             (
                 [0.21, -0.78, 0.52],
                 [
@@ -161,6 +251,7 @@ class TestSparse:
                     qml.PauliX(0) @ qml.PauliZ(1),
                     qml.PauliY(0) @ qml.PauliZ(1),
                 ],
+                None,
                 np.array(
                     [
                         [0.21 + 0.0j, 0.0 + 0.0j, -0.78 - 0.52j, 0.0 + 0.0j],
@@ -172,11 +263,11 @@ class TestSparse:
             ),
         ],
     )
-    def test_sparse_matrix(self, coeffs, obs, ref_matrix):
+    def test_sparse_matrix(self, coeffs, obs, wires, ref_matrix):
         """Tests that sparse_hamiltonian returns a correct sparse matrix"""
         H = qml.Hamiltonian(coeffs, obs)
 
-        sparse_matrix = qml.utils.sparse_hamiltonian(H)
+        sparse_matrix = qml.utils.sparse_hamiltonian(H, wires)
 
         assert np.allclose(sparse_matrix.toarray(), ref_matrix)
 
@@ -201,6 +292,15 @@ class TestSparse:
         with pytest.raises(TypeError, match="Passed Hamiltonian must be of type"):
             qml.utils.sparse_hamiltonian(np.eye(2))
 
+    def test_observable_error(self):
+        """Tests that an error is thrown if the observables are themselves constructed from multi-qubit
+        operations."""
+        with pytest.raises(ValueError, match="Can only sparsify Hamiltonians"):
+            H = qml.Hamiltonian(
+                [0.1], [qml.PauliZ("c") @ qml.Hermitian(np.eye(4), wires=["a", "b"])]
+            )
+            qml.utils.sparse_hamiltonian(H, wires=["a", "c", "b"])
+
 
 class TestFlatten:
     """Tests the flatten and unflatten functions"""