Repository: PennyLaneAI/pennylane
Pull request #1398 (2021-06-09 19:46:03)

Add functions to support a sparse matrix observable
**Context:**

This PR adds a new observable class for supporting sparse matrix objects and adds the functionality to compute the expectation value of the observable via direct matrix-vector multiplication.  

**Description of the Change:**

The observable class `SparseHamiltonian` is added to the `qml.ops.qubit` module and the new function `expval` is created in `qml.devices.default_qubit`.

**Benefits:**

The computation of a Hamiltonian expectation value is faster and more memory-efficient with the new functionality. 

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/1398?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1398](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (254ccac) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/e1a05d22eb38bbcfed18cbcbe3f245a0a1e39fe0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (e1a05d2) will **increase** coverage by `0.01%`.
> The diff coverage is `92.00%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1398      +/-   ##
==========================================
+ Coverage   98.18%   98.20%   +0.01%     
==========================================
  Files         157      160       +3     
  Lines       11687    11924     +237     
==========================================
+ Hits        11475    11710     +235     
- Misses        212      214       +2     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/devices/default\_qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdC5weQ==) | `99.10% <80.00%> (-0.90%)` | :arrow_down: |
| [pennylane/ops/qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC5weQ==) | `98.61% <100.00%> (+0.01%)` | :arrow_up: |
| [pennylane/\_grad.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19ncmFkLnB5) | `100.00% <0.00%> (ø)` | |
| [pennylane/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19faW5pdF9fLnB5) | `98.57% <0.00%> (ø)` | |
| [pennylane/interfaces/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvamF4LnB5) | `89.74% <0.00%> (ø)` | |
| [pennylane/fourier/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ZvdXJpZXIvX19pbml0X18ucHk=) | `100.00% <0.00%> (ø)` | |
| [pennylane/interfaces/torch.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvdG9yY2gucHk=) | `100.00% <0.00%> (ø)` | |
| [pennylane/transforms/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvX19pbml0X18ucHk=) | `100.00% <0.00%> (ø)` | |
| [pennylane/tape/qubit\_param\_shift.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RhcGUvcXViaXRfcGFyYW1fc2hpZnQucHk=) | `100.00% <0.00%> (ø)` | |
| [pennylane/grouping/group\_observables.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2dyb3VwaW5nL2dyb3VwX29ic2VydmFibGVzLnB5) | `100.00% <0.00%> (ø)` | |
| ... and [10 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398/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/1398?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/1398?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [e1a05d2...254ccac](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1398?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).

soranjh: > Thanks @soranjh! I'm happy to now approve this PR, however, I recommend updating the gradient test to test a case where the gradient is _not_ zero. There have been cases in the past where our gradient tests have been using test circuits that were too trivial, leading to bug not being uncovered.

Thanks @josh146. The gradient test is updated for a hydrogen Hamiltonian. Have you observed the codefactor issue "Complex Code" before? I computed the cyclomatic complexity with radon and everything seems normal in pennylane/ops/qubit.py, specially in my additions. Not sure which part of the code raises that issu.
josh146: @soranjh I have overridden the complex code warning :)
soranjh: > @soranjh I have overridden the complex code warning :)

Thanks Josh!

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 5ad7446e9..7d1cf6c49 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -31,6 +31,35 @@ Olivia Di Matteo, Ashish Panigrahi
 
 <h3>New features since last release</h3>
 
+* Added a sparse Hamiltonian observable and the functionality to support computing its expectation
+  value. [(#1398)](https://github.com/PennyLaneAI/pennylane/pull/1398)
+
+  For example, the following QNode returns the expectation value of a sparse Hamiltonian:
+
+  ```python
+  dev = qml.device("default.qubit", wires=2)
+
+  @qml.qnode(dev, diff_method="parameter-shift")
+  def circuit(param, H):
+      qml.PauliX(0)
+      qml.SingleExcitation(param, wires=[0, 1])
+      return qml.expval(qml.SparseHamiltonian(H, [0, 1]))
+  ```
+  
+  We can execute this QNode, passing in a sparse identity matrix:
+
+  ```pycon
+  >>> print(circuit([0.5], scipy.sparse.eye(4).tocoo()))
+  0.9999999999999999
+  ```
+
+  The expectation value of the sparse Hamiltonian is computed directly, which leads to executions
+  that are faster by orders of magnitude. Note that "parameter-shift" is the only differentiation
+  method that is currently supported when the observable is a sparse Hamiltonian.
+
+* Added functionality to compute the sparse matrix representation of a `qml.Hamiltonian` object.
+  [(#1394)](https://github.com/PennyLaneAI/pennylane/pull/1394)
+
 <h4>First class support for quantum kernels</h4>
 
 * The new `qml.kernels` module provides basic functionalities for [working with quantum
diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py
index d739f0df5..ea8a60acf 100644
--- a/pennylane/devices/default_qubit.py
+++ b/pennylane/devices/default_qubit.py
@@ -23,6 +23,7 @@ import functools
 from string import ascii_letters as ABC
 
 import numpy as np
+from scipy.sparse import coo_matrix
 
 from pennylane import QubitDevice, DeviceError, QubitStateVector, BasisState
 from pennylane.operation import DiagonalOperation
@@ -136,7 +137,16 @@ class DefaultQubit(QubitDevice):
         "QubitSum",
     }
 
-    observables = {"PauliX", "PauliY", "PauliZ", "Hadamard", "Hermitian", "Identity", "Projector"}
+    observables = {
+        "PauliX",
+        "PauliY",
+        "PauliZ",
+        "Hadamard",
+        "Hermitian",
+        "Identity",
+        "Projector",
+        "SparseHamiltonian",
+    }
 
     def __init__(self, wires, *, shots=None, cache=0, analytic=None):
         super().__init__(wires, shots, cache=cache, analytic=analytic)
@@ -438,6 +448,39 @@ class DefaultQubit(QubitDevice):
         phase = self._conj(parameters) if inverse else parameters
         return self._stack([state[sl_0], phase * state[sl_1]], axis=axes[0])
 
+    def expval(self, observable, shot_range=None, bin_size=None):
+        """Returns the expectation value of a Hamiltonian observable. When the observable is a
+         ``SparseHamiltonian`` object, the expectation value is computed directly for the full
+         Hamiltonian, which leads to faster execution.
+
+        Args:
+            observable (~.Observable): a PennyLane observable
+            shot_range (tuple[int]): 2-tuple of integers specifying the range of samples
+                to use. If not specified, all samples are used.
+            bin_size (int): Divides the shot range into bins of size ``bin_size``, and
+                returns the measurement statistic separately over each bin. If not
+                provided, the entire shot range is treated as a single bin.
+
+        Returns:
+            float: returns the expectation value of the observable
+        """
+        if observable.name == "SparseHamiltonian":
+            if self.shots is not None:
+                raise DeviceError("SparseHamiltonian must be used with shots=None")
+
+        if observable.name == "SparseHamiltonian" and self.shots is None:
+
+            ev = coo_matrix.dot(
+                coo_matrix(self._conj(self.state)),
+                coo_matrix.dot(
+                    observable.matrix, coo_matrix(self.state.reshape(len(self.state), 1))
+                ),
+            )
+
+            return np.real(ev.toarray()[0])
+
+        return super().expval(observable, shot_range=shot_range, bin_size=bin_size)
+
     def _get_unitary_matrix(self, unitary):  # pylint: disable=no-self-use
         """Return the matrix representing a unitary operation.
 
diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py
index 32710eece..cc1975097 100644
--- a/pennylane/ops/qubit.py
+++ b/pennylane/ops/qubit.py
@@ -21,10 +21,11 @@ import functools
 # pylint:disable=abstract-method,arguments-differ,protected-access
 import math
 import numpy as np
+import scipy
 from scipy.linalg import block_diag
 
 import pennylane as qml
-from pennylane.operation import AnyWires, DiagonalOperation, Observable, Operation
+from pennylane.operation import AnyWires, AllWires, DiagonalOperation, Observable, Operation
 from pennylane.templates.decorator import template
 from pennylane.templates.state_preparations import BasisStatePreparation, MottonenStatePreparation
 from pennylane.utils import expand, pauli_eigs
@@ -3087,6 +3088,45 @@ class Projector(Observable):
         return []
 
 
+class SparseHamiltonian(Observable):
+    r"""SparseHamiltonian(H)
+    A Hamiltonian represented directly as a sparse matrix in coordinate list (COO) format.
+
+    .. warning::
+
+        ``SparseHamiltonian`` observables can only be used to return expectation values.
+        Variances and samples are not supported.
+
+    .. note::
+
+        Note that the ``SparseHamiltonian`` observable should not be used with a subset of wires.
+
+    **Details:**
+
+    * Number of wires: All
+    * Number of parameters: 1
+    * Gradient recipe: None
+
+    Args:
+        H (coo_matrix): a sparse matrix in SciPy coordinate list (COO) format with
+            dimension :math:`(2^n, 2^n)`, where :math:`n` is the number of wires
+    """
+    num_wires = AllWires
+    num_params = 1
+    par_domain = None
+    grad_method = None
+
+    @classmethod
+    def _matrix(cls, *params):
+        A = params[0]
+        if not isinstance(A, scipy.sparse.coo_matrix):
+            raise TypeError("Observable must be a scipy sparse coo_matrix.")
+        return A
+
+    def diagonalizing_gates(self):
+        return []
+
+
 # =============================================================================
 # Arithmetic
 # =============================================================================
@@ -3333,7 +3373,7 @@ ops = {
 }
 
 
-obs = {"Hadamard", "PauliX", "PauliY", "PauliZ", "Hermitian", "Projector"}
+obs = {"Hadamard", "PauliX", "PauliY", "PauliZ", "Hermitian", "Projector", "SparseHamiltonian"}
 
 
 __all__ = list(ops | obs)
diff --git a/pennylane/qnode.py b/pennylane/qnode.py
index 3381eaea8..857e34e05 100644
--- a/pennylane/qnode.py
+++ b/pennylane/qnode.py
@@ -15,6 +15,7 @@
 This module contains the QNode class and qnode decorator.
 """
 # pylint: disable=import-outside-toplevel
+# pylint:disable=too-many-branches
 from collections.abc import Sequence
 from functools import lru_cache, update_wrapper
 import warnings
@@ -552,6 +553,7 @@ class QNode:
             )
 
         for obj in self.qtape.operations + self.qtape.observables:
+
             if getattr(obj, "num_wires", None) is qml.operation.WiresEnum.AllWires:
                 # check here only if enough wires
                 if len(obj.wires) != self.device.num_wires:
@@ -559,6 +561,15 @@ class QNode:
                         "Operator {} must act on all wires".format(obj.name)
                     )
 
+            if (
+                isinstance(obj, qml.ops.qubit.SparseHamiltonian)
+                and self.diff_method != "parameter-shift"
+            ):
+                raise qml.QuantumFunctionError(
+                    "SparseHamiltonian observable must be used with the parameter-shift"
+                    " differentiation method"
+                )
+
         # pylint: disable=protected-access
         obs_on_same_wire = len(self.qtape._obs_sharing_wires) > 0
         ops_not_supported = any(


Test patch:
diff --git a/tests/ops/test_sparse.py b/tests/ops/test_sparse.py
new file mode 100644
index 000000000..144f57888
--- /dev/null
+++ b/tests/ops/test_sparse.py
@@ -0,0 +1,177 @@
+# Copyright 2018-2020 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Unit tests for the SparseHamiltonian observable.
+"""
+import pytest
+import numpy as np
+from scipy.sparse import coo_matrix, csr_matrix
+import pennylane as qml
+from pennylane import DeviceError
+from pennylane.wires import Wires
+
+
+SPARSE_HAMILTONIAN_TEST_DATA = [(np.array([[1, 0], [-1.5, 0]])), (np.eye(4))]
+
+H_row = np.array([0, 1, 2, 3, 3, 4, 5, 6, 6, 7, 8, 9, 9, 10, 11, 12, 12, 13, 14, 15])
+H_col = np.array([0, 1, 2, 3, 12, 4, 5, 6, 9, 7, 8, 6, 9, 10, 11, 3, 12, 13, 14, 15])
+H_data = np.array(
+    [
+        0.72004228 + 0.0j,
+        0.24819411 + 0.0j,
+        0.24819411 + 0.0j,
+        0.47493347 + 0.0j,
+        0.18092703 + 0.0j,
+        -0.5363422 + 0.0j,
+        -0.52452263 + 0.0j,
+        -0.34359561 + 0.0j,
+        -0.18092703 + 0.0j,
+        0.3668115 + 0.0j,
+        -0.5363422 + 0.0j,
+        -0.18092703 + 0.0j,
+        -0.34359561 + 0.0j,
+        -0.52452263 + 0.0j,
+        0.3668115 + 0.0j,
+        0.18092703 + 0.0j,
+        -1.11700225 + 0.0j,
+        -0.44058791 + 0.0j,
+        -0.44058791 + 0.0j,
+        0.93441396 + 0.0j,
+    ]
+)
+H_hydrogen = coo_matrix((H_data, (H_row, H_col)), shape=(16, 16)).toarray()
+
+
+class TestSparse:
+    """Tests for sparse hamiltonian observable"""
+
+    @pytest.mark.parametrize("sparse_hamiltonian", SPARSE_HAMILTONIAN_TEST_DATA)
+    def test_sparse_diagonalization(self, sparse_hamiltonian):
+        """Test that the diagonalizing_gates property of the SparseHamiltonian class returns empty."""
+        num_wires = len(sparse_hamiltonian[0])
+        sparse_hamiltonian = coo_matrix(sparse_hamiltonian)
+        diag_gates = qml.SparseHamiltonian(
+            sparse_hamiltonian, range(num_wires)
+        ).diagonalizing_gates()
+
+        assert diag_gates == []
+
+    @pytest.mark.parametrize("sparse_hamiltonian", SPARSE_HAMILTONIAN_TEST_DATA)
+    def test_sparse_typeerror(self, sparse_hamiltonian):
+        """Test that the matrix property of the SparseHamiltonian class raises a TypeError on incorrect inputs."""
+        num_wires = len(sparse_hamiltonian[0])
+        sparse_hamiltonian = csr_matrix(sparse_hamiltonian)
+
+        dev = qml.device("default.qubit", wires=num_wires)
+
+        @qml.qnode(dev, diff_method="parameter-shift")
+        def circuit(sparse_hamiltonian, num_wires):
+            obs = qml.SparseHamiltonian(sparse_hamiltonian, range(num_wires))
+            return qml.expval(obs)
+
+        with pytest.raises(TypeError, match="Observable must be a scipy sparse coo_matrix"):
+            circuit(sparse_hamiltonian, num_wires)
+
+    @pytest.mark.parametrize("sparse_hamiltonian", SPARSE_HAMILTONIAN_TEST_DATA)
+    def test_sparse_matrix(self, sparse_hamiltonian, tol):
+        """Test that the matrix property of the SparseHamiltonian class returns the correct matrix."""
+        num_wires = len(sparse_hamiltonian[0])
+        sparse_hamiltonian = coo_matrix(sparse_hamiltonian)
+        returned_matrix = qml.SparseHamiltonian(sparse_hamiltonian, range(num_wires)).matrix
+        assert np.allclose(
+            returned_matrix.toarray(), sparse_hamiltonian.toarray(), atol=tol, rtol=0
+        )
+
+    def test_sparse_diffmethod_error(self):
+        """Test that an error is raised when the observable is SparseHamiltonian and the
+        differentiation method is not parameter-shift."""
+        dev = qml.device("default.qubit", wires=2, shots=None)
+
+        @qml.qnode(dev, diff_method="backprop")
+        def circuit(param):
+            qml.RX(param, wires=0)
+            return qml.expval(qml.SparseHamiltonian(coo_matrix(np.eye(4)), [0, 1]))
+
+        with pytest.raises(
+            qml.QuantumFunctionError,
+            match="SparseHamiltonian observable must be"
+            " used with the parameter-shift differentiation method",
+        ):
+            qml.grad(circuit)([0.5])
+
+    @pytest.mark.parametrize("qubits, hamiltonian, expected_output", [(4, H_hydrogen, -0.18092703)])
+    def test_sparse_gradient(self, qubits, hamiltonian, expected_output, tol):
+        """Tests that gradients are computed correctly for a SparseHamiltonian observable."""
+        dev = qml.device("default.qubit", wires=qubits, shots=None)
+
+        hamiltonian = coo_matrix(hamiltonian)
+
+        @qml.qnode(dev, diff_method="parameter-shift")
+        def circuit(param):
+            qml.PauliX(0)
+            qml.PauliX(1)
+            qml.DoubleExcitation(param, wires=[0, 1, 2, 3])
+            return qml.expval(qml.SparseHamiltonian(hamiltonian, wires=range(qubits)))
+
+        assert np.allclose(qml.grad(circuit)([0.0]), expected_output, atol=tol, rtol=0)
+
+    @pytest.mark.parametrize(
+        "qubits, operations, hamiltonian, expected_output",
+        [
+            (1, [], np.array([[1.0, 0.0], [0.0, 1.0]]), 1.0),
+            (
+                2,
+                [qml.PauliX(0), qml.PauliY(1)],
+                np.array(
+                    [
+                        [1.0, 0.0, 0.0, 0.0],
+                        [0.0, 1.0, 0.0, 0.0],
+                        [0.0, 0.0, 1.0, 0.0],
+                        [0.0, 0.0, 0.0, 1.0],
+                    ]
+                ),
+                1.0,
+            ),
+            (
+                4,
+                [
+                    qml.PauliX(0),
+                    qml.PauliX(1),
+                    qml.DoubleExcitation(0.22350048065138242, wires=[0, 1, 2, 3]),
+                ],
+                H_hydrogen,
+                -1.1373060481,
+            ),
+        ],
+    )
+    def test_sparse_hamiltonian_expval(self, qubits, operations, hamiltonian, expected_output, tol):
+        """Test that expectation values of sparse hamiltonians are properly calculated."""
+
+        hamiltonian = coo_matrix(hamiltonian)
+
+        dev = qml.device("default.qubit", wires=qubits, shots=None)
+        dev.apply(operations)
+        expval = dev.expval(qml.SparseHamiltonian(hamiltonian, range(qubits)))[0]
+
+        assert np.allclose(expval, expected_output, atol=tol, rtol=0)
+
+    def test_sparse_expval_error(self):
+        """Test that a DeviceError is raised when the observable is SparseHamiltonian and finite
+        shots is requested."""
+        hamiltonian = coo_matrix(np.array([[1.0, 0.0], [0.0, 1.0]]))
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.raises(DeviceError, match="SparseHamiltonian must be used with shots=None"):
+            dev.expval(qml.SparseHamiltonian(hamiltonian, [0]))[0]