Repository: PennyLaneAI/pennylane
Pull request #4828 (2023-11-10 23:06:28)

Adds `__mul__` and `__rmul__` methods to `SparseHamiltonian`
**Context:** https://github.com/PennyLaneAI/pennylane/issues/2558

**Description of the Change:** Adds `__mul__` and `__rmul__` methods to `SparseHamiltonian`.

**Benefits:** `SparseHamiltonian`s stay as such when multiplied by a scalar value.

```
wires = range(20)
coeffs = [0.123 for _ in wires]
observables = [qml.PauliY(i) for i in wires]

H = qml.Hamiltonian(coeffs, observables)
Hmat = H.sparse_matrix()
H_sparse = qml.SparseHamiltonian(Hmat, wires)

new = 4.123 * H_sparse
print(new, type(new))
```

Old behaviour:

```
  (4.123) [SparseHamiltonian0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19] <class 'pennylane.ops.qubit.hamiltonian.Hamiltonian'>
```

New behaviour:

```
SparseHamiltonian(<1048576x1048576 sparse matrix of type '<class 'numpy.complex128'>'
	with 20971520 stored elements in Compressed Sparse Row format>, wires=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) <class 'pennylane.ops.qubit.observables.SparseHamiltonian'>
```

**Possible Drawbacks:** 

**Related GitHub Issues:** https://github.com/PennyLaneAI/pennylane/issues/2558


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://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4828?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: `1 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`677fbe0`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/677fbe00f70e8456dacd5b0c4e9a46923036eeca?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.64% compared to head [(`321260e`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4828?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.63%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4828      +/-   ##
==========================================
- Coverage   99.64%   99.63%   -0.01%     
==========================================
  Files         381      381              
  Lines       34377    34123     -254     
==========================================
- Hits        34256    34000     -256     
- Misses        121      123       +2     
```


| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4828?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/qubit/observables.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4828?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9vYnNlcnZhYmxlcy5weQ==) | `99.39% <83.33%> (-0.61%)` | :arrow_down: |

... and [42 files with indirect coverage changes](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4828/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4828?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

isaacdevlugt: @timmysilv this ok?

https://github.com/PennyLaneAI/pennylane/actions/runs/6856238041/job/18643028272?pr=4828#step:7:18

If I don't assign the result to a variable it yells at me too lol

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 270918021..df60fc60f 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -75,6 +75,10 @@
 * `qml.draw` and `qml.draw_mpl` now render operator ids.
   [(#4749)](https://github.com/PennyLaneAI/pennylane/pull/4749)
 
+* When multiplying `SparseHamiltonian`s by a scalar value, the result stays as a 
+  `SparseHamiltonian`.
+  [(#4828)](https://github.com/PennyLaneAI/pennylane/pull/4828)
+
 * `has_matrix` will now automatically be `True` if `Operator.matrix` is overridden, even if
   `Operator.compute_matrix` is not.
   [(#4844)](https://github.com/PennyLaneAI/pennylane/pull/4844)
@@ -202,6 +206,7 @@ This release contains contributions from (in alphabetical order):
 
 Guillermo Alonso,
 Astral Cai,
+Isaac De Vlugt,
 Amintor Dusko,
 Lillian Frederiksen,
 Emiliano Godinez Ramirez,
diff --git a/pennylane/ops/qubit/observables.py b/pennylane/ops/qubit/observables.py
index 03f1d1ebb..279489487 100644
--- a/pennylane/ops/qubit/observables.py
+++ b/pennylane/ops/qubit/observables.py
@@ -244,12 +244,22 @@ class SparseHamiltonian(Observable):
         if not isinstance(H, csr_matrix):
             raise TypeError("Observable must be a scipy sparse csr_matrix.")
         super().__init__(H, wires=wires, id=id)
+        self.H = H
         mat_len = 2 ** len(self.wires)
         if H.shape != (mat_len, mat_len):
             raise ValueError(
                 f"Sparse Matrix must be of shape ({mat_len}, {mat_len}). Got {H.shape}."
             )
 
+    def __mul__(self, value):
+        r"""The scalar multiplication operation between a scalar and a SparseHamiltonian."""
+        if not isinstance(value, (int, float)) and qml.math.ndim(value) != 0:
+            raise TypeError(f"Scalar value must be an int or float. Got {type(value)}")
+
+        return qml.SparseHamiltonian(csr_matrix.multiply(self.H, value), wires=self.wires)
+
+    __rmul__ = __mul__
+
     def label(self, decimals=None, base_label=None, cache=None):
         return super().label(decimals=decimals, base_label=base_label or "𝓗", cache=cache)
 


Test patch:
diff --git a/tests/ops/qubit/test_sparse.py b/tests/ops/qubit/test_sparse.py
index 883c96cec..a99baa944 100644
--- a/tests/ops/qubit/test_sparse.py
+++ b/tests/ops/qubit/test_sparse.py
@@ -19,6 +19,90 @@ import numpy as np
 from scipy.sparse import coo_matrix, csr_matrix
 import pennylane as qml
 
+SPARSEHAMILTONIAN_TEST_MATRIX = np.array(
+    [
+        [
+            0 + 0j,
+            0 - 0j,
+            0 + 0j,
+            -0 + 0j,
+            0 + 0j,
+            -0 + 0j,
+            -0 + 0j,
+            0 + 1.0j,
+        ],
+        [
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            -0 - 1.0j,
+            -0 + 0j,
+        ],
+        [
+            0 + 0j,
+            0 - 0j,
+            0 + 0j,
+            0 - 0j,
+            0 + 0j,
+            -0 - 1.0j,
+            0 + 0j,
+            -0 + 0j,
+        ],
+        [
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 1.0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+        ],
+        [
+            0 + 0j,
+            0 - 0j,
+            0 + 0j,
+            -0 - 1.0j,
+            0 + 0j,
+            0 - 0j,
+            0 + 0j,
+            -0 + 0j,
+        ],
+        [
+            0 + 0j,
+            0 + 0j,
+            0 + 1.0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+        ],
+        [
+            -0 + 0j,
+            0 + 1.0j,
+            0 + 0j,
+            0 - 0j,
+            0 + 0j,
+            0 - 0j,
+            0 + 0j,
+            0 - 0j,
+        ],
+        [
+            -0 - 1.0j,
+            -0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+            0 + 0j,
+        ],
+    ]
+)
 
 SPARSE_HAMILTONIAN_TEST_DATA = [(np.array([[1, 0], [-1.5, 0]])), (np.eye(4))]
 
@@ -212,3 +296,32 @@ class TestSparse:
 
         with pytest.raises(qml.operation.DiagGatesUndefinedError):
             dev.execute(qs)
+
+    @pytest.mark.parametrize("sparse_hamiltonian", SPARSEHAMILTONIAN_TEST_MATRIX)
+    def test_scalar_multipication(self, sparse_hamiltonian):
+        """Tests if the __mul__ method of SparseHamiltonian gives the correct result."""
+
+        value = 0.1234
+        num_wires = int(np.log2(len(sparse_hamiltonian)))
+        H = csr_matrix(SPARSEHAMILTONIAN_TEST_MATRIX)
+        H_multiplied = csr_matrix(SPARSEHAMILTONIAN_TEST_MATRIX * value)
+
+        H_sparse_mul_method = qml.SparseHamiltonian(H, wires=range(num_wires)) * value
+        H_sparse_mul_method = H_sparse_mul_method.sparse_matrix()
+        H_sparse_multiplied_before = qml.SparseHamiltonian(
+            H_multiplied, wires=range(num_wires)
+        ).sparse_matrix()
+
+        assert np.allclose(H_sparse_mul_method.toarray(), H_sparse_multiplied_before.toarray())
+
+    @pytest.mark.parametrize("sparse_hamiltonian", SPARSEHAMILTONIAN_TEST_MATRIX)
+    def test_scalar_multipication_typeerror(self, sparse_hamiltonian):
+        """Tests if the __mul__ method of SparseHamiltonian throws the correct error."""
+        value = [0.1234]
+        num_wires = int(np.log2(len(sparse_hamiltonian)))
+        H = csr_matrix(SPARSEHAMILTONIAN_TEST_MATRIX)
+
+        with pytest.raises(
+            TypeError, match="Scalar value must be an int or float. Got <class 'list'>"
+        ):
+            _ = qml.SparseHamiltonian(H, wires=range(num_wires)) * value