Repository: PennyLaneAI/pennylane
Pull request #1587 (2021-08-24 10:43:24)

Collection of improvements related to Hamiltonians
This is a colourful collection of improvements from other PRs around Hamiltonians and VQE.

I will comment in the code!

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/1587?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1587](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1587?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (d1a8d0e) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/4c09cda4e5105aebf8ec089ed4c3af8af8b1c543?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (4c09cda) will **decrease** coverage by `0.04%`.
> The diff coverage is `66.66%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1587      +/-   ##
==========================================
- Coverage   99.16%   99.12%   -0.05%     
==========================================
  Files         189      189              
  Lines       13642    13654      +12     
==========================================
+ Hits        13528    13534       +6     
- Misses        114      120       +6     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/\_qubit\_device.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1587/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19xdWJpdF9kZXZpY2UucHk=) | `96.72% <50.00%> (-1.93%)` | :arrow_down: |
| [pennylane/qnode.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1587/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3Fub2RlLnB5) | `98.85% <100.00%> (+<0.01%)` | :arrow_up: |
| [pennylane/utils.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1587/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3V0aWxzLnB5) | `98.25% <100.00%> (+0.02%)` | :arrow_up: |

------

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

mariaschuld: @glassnotes thanks for the comments, hopefully it is ready for merging now. I decided to stop being lazy and test the exceptions, and it was actually quite easy!



Patch:
diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py
index 48b328a6f..3bbd88a72 100644
--- a/pennylane/_qubit_device.py
+++ b/pennylane/_qubit_device.py
@@ -759,9 +759,15 @@ class QubitDevice(Device):
             )
             return probs[idx]
 
+        # exact expectation value
         if self.shots is None:
-            # exact expectation value
-            eigvals = self._asarray(observable.eigvals, dtype=self.R_DTYPE)
+            try:
+                eigvals = self._asarray(observable.eigvals, dtype=self.R_DTYPE)
+            except NotImplementedError as e:
+                raise ValueError(
+                    f"Cannot compute analytic expectations of {observable.name}."
+                ) from e
+
             prob = self.probability(wires=observable.wires)
             return self._dot(eigvals, prob)
 
@@ -779,9 +785,13 @@ class QubitDevice(Device):
             )
             return probs[idx] - probs[idx] ** 2
 
+        # exact variance value
         if self.shots is None:
-            # exact variance value
-            eigvals = self._asarray(observable.eigvals, dtype=self.R_DTYPE)
+            try:
+                eigvals = self._asarray(observable.eigvals, dtype=self.R_DTYPE)
+            except NotImplementedError as e:
+                # if observable has no info on eigenvalues, we cannot return this measurement
+                raise ValueError(f"Cannot compute analytic variance of {observable.name}.") from e
             prob = self.probability(wires=observable.wires)
             return self._dot((eigvals ** 2), prob) - self._dot(eigvals, prob) ** 2
 
@@ -811,6 +821,7 @@ class QubitDevice(Device):
                 samples = self._samples[sample_slice]
 
         else:
+
             # Replace the basis state in the computational basis with the correct eigenvalue.
             # Extract only the columns of the basis samples required based on ``wires``.
             samples = self._samples[
@@ -818,7 +829,11 @@ class QubitDevice(Device):
             ]  # Add np.array here for Jax support.
             powers_of_two = 2 ** np.arange(samples.shape[-1])[::-1]
             indices = samples @ powers_of_two
-            samples = observable.eigvals[indices]
+            try:
+                samples = observable.eigvals[indices]
+            except NotImplementedError as e:
+                # if observable has no info on eigenvalues, we cannot return this measurement
+                raise ValueError(f"Cannot compute samples of {observable.name}.") from e
 
         if bin_size is None:
             return samples
diff --git a/pennylane/qnode.py b/pennylane/qnode.py
index f6542056b..6e3df2998 100644
--- a/pennylane/qnode.py
+++ b/pennylane/qnode.py
@@ -605,19 +605,20 @@ class QNode:
             # construct the tape
             self.construct(args, kwargs)
 
-        # Execute the tape.
         # If the observable contains a Hamiltonian and the device does not
-        # support Hamiltonians, split tape into multiple tapes and execute
-        # sequentially. In future, this logic should be moved to the device
+        # support Hamiltonians, or if the simulation uses finite shots,
+        # split tape into multiple tapes of diagonalizable known observables.
+        # In future, this logic should be moved to the device
         # to allow for more efficient batch execution.
         supports_hamiltonian = self.device.supports_observable("Hamiltonian")
+        finite_shots = self.device.shots is not None
         hamiltonian_in_obs = "Hamiltonian" in [obs.name for obs in self.qtape.observables]
-        if hamiltonian_in_obs and not supports_hamiltonian:
+        if hamiltonian_in_obs and (not supports_hamiltonian or finite_shots):
             try:
                 tapes, fn = qml.transforms.hamiltonian_expand(self.qtape, group=False)
             except ValueError as e:
                 raise ValueError(
-                    "Only a single expectation of a Hamiltonian observable can be returned"
+                    "Only a single expectation measurement of a Hamiltonian observable can be returned "
                     "when using the {} device.".format(self.device.name)
                 ) from e
             results = [tape.execute(device=self.device) for tape in tapes]
diff --git a/pennylane/utils.py b/pennylane/utils.py
index f83b8b026..3e2134db4 100644
--- a/pennylane/utils.py
+++ b/pennylane/utils.py
@@ -144,11 +144,15 @@ def sparse_hamiltonian(H, wires=None):
 
     if wires is None:
         wires = H.wires
+    else:
+        wires = qml.wires.Wires(wires)
 
     n = len(wires)
     matrix = scipy.sparse.coo_matrix((2 ** n, 2 ** n), dtype="complex128")
 
-    for coeff, op in zip(H.coeffs, H.ops):
+    coeffs = qml.math.toarray(H.coeffs)
+
+    for coeff, op in zip(coeffs, H.ops):
 
         obs = []
         for o in qml.operation.Tensor(op).obs:


Test patch:
diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py
index 854103d67..1c0dc21b9 100644
--- a/tests/test_qubit_device.py
+++ b/tests/test_qubit_device.py
@@ -503,6 +503,12 @@ class TestExpval:
 
         assert res == obs
 
+    def test_no_eigval_error(self, mock_qubit_device_with_original_statistics):
+        """Tests that an error is thrown if expval is called with an observable that does not have eigenvalues defined."""
+        dev = mock_qubit_device_with_original_statistics()
+        with pytest.raises(ValueError, match="Cannot compute analytic expectations"):
+            dev.expval(qml.Hamiltonian([1.0], [qml.PauliX(0)]))
+
 
 class TestVar:
     """Test the var method"""
@@ -548,6 +554,12 @@ class TestVar:
 
         assert res == obs
 
+    def test_no_eigval_error(self, mock_qubit_device_with_original_statistics):
+        """Tests that an error is thrown if var is called with an observable that does not have eigenvalues defined."""
+        dev = mock_qubit_device_with_original_statistics()
+        with pytest.raises(ValueError, match="Cannot compute analytic variance"):
+            dev.var(qml.Hamiltonian([1.0], [qml.PauliX(0)]))
+
 
 class TestSample:
     """Test the sample method"""
@@ -606,6 +618,13 @@ class TestSample:
 
         assert np.array_equal(res, wire_samples)
 
+    def test_no_eigval_error(self, mock_qubit_device_with_original_statistics):
+        """Tests that an error is thrown if sample is called with an observable that does not have eigenvalues defined."""
+        dev = mock_qubit_device_with_original_statistics()
+        dev._samples = np.array([[1, 0], [0, 0]])
+        with pytest.raises(ValueError, match="Cannot compute samples"):
+            dev.sample(qml.Hamiltonian([1.0], [qml.PauliX(0)]))
+
 
 class TestEstimateProb:
     """Test the estimate_probability method"""
diff --git a/tests/test_vqe.py b/tests/test_vqe.py
index 249e34697..241daf05f 100644
--- a/tests/test_vqe.py
+++ b/tests/test_vqe.py
@@ -733,9 +733,7 @@ class TestNewVQE:
             qml.templates.StronglyEntanglingLayers(w, wires=range(4))
             return qml.expval(H), qml.expval(qml.PauliX(3))
 
-        with pytest.raises(
-            ValueError, match="Only a single expectation of a Hamiltonian observable"
-        ):
+        with pytest.raises(ValueError, match="expectation measurement of a Hamiltonian observable"):
             circuit()
 
     def test_error_non_expval_measurement(self):
@@ -749,18 +747,14 @@ class TestNewVQE:
         def circuit():
             return qml.sample(H)
 
-        with pytest.raises(
-            ValueError, match="Only a single expectation of a Hamiltonian observable "
-        ):
+        with pytest.raises(ValueError, match="expectation measurement of a Hamiltonian observable"):
             circuit()
 
         @qml.qnode(dev)
         def circuit():
             return qml.var(H)
 
-        with pytest.raises(
-            ValueError, match="Only a single expectation of a Hamiltonian observable"
-        ):
+        with pytest.raises(ValueError, match="expectation measurement of a Hamiltonian observable"):
             circuit()
 
     @pytest.mark.parametrize("diff_method", ["parameter-shift", "best"])