Repository: PennyLaneAI/pennylane
Pull request #3391 (2022-11-18 16:43:42)

`hamiltonian()` and `operation()` methods to convert `PauliSentence` into native PL operators or Hamiltonians (PauliUtility #3)
**Context:**
Introduce the `PauliSentence.hamiltonian()` and `PauliSentence.operation()` methods which allow users to go from the lightweight representation of Pauli sentences to native PennyLane operations. 

**Description of the Change:**
- Introduce `PauliWord.hamiltonian()` and `PauliWord.operation()` methods + tests
- Introduce `PauliSentence.hamiltonian()` and `PauliSentence.operation()` methods + tests

**Benefits:**
- More interfacing support between this representation and PL

codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3391?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3391](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3391?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (a19bb0b) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/6881ca8076983f4f324492cfeb8edb10ec06a5be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (6881ca8) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master    #3391   +/-   ##
=======================================
  Coverage   99.70%   99.70%           
=======================================
  Files         298      298           
  Lines       26220    26274   +54     
=======================================
+ Hits        26142    26196   +54     
  Misses         78       78           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3391?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/op\_math/exp.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3391/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2V4cC5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/pauli/pauli\_arithmetic.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3391/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3BhdWxpL3BhdWxpX2FyaXRobWV0aWMucHk=) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

Jaybsoni: [sc-28974]

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 1dcec12c0..83ffca7e4 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -19,7 +19,7 @@
    [ 0  0  0  0  0  0  0 -1]]
   ```
 
-  [#3408](https://github.com/PennyLaneAI/pennylane/pull/3408)
+  [(#3408)](https://github.com/PennyLaneAI/pennylane/pull/3408)
 
 * Add the controlled Hadamard gate.
 
@@ -32,43 +32,43 @@
    [ 0.          0.          0.70710678 -0.70710678]]
   ```
 
-  [#3408](https://github.com/PennyLaneAI/pennylane/pull/3408)
+  [(#3408)](https://github.com/PennyLaneAI/pennylane/pull/3408)
 
 * Support custom measurement processes:
   * `SampleMeasurement`, `StateMeasurement` and `CustomMeasurement` classes have been added.
     They contain an abstract method to process samples/quantum state/quantum script.
-    [#3286](https://github.com/PennyLaneAI/pennylane/pull/3286)
-    [#3388](https://github.com/PennyLaneAI/pennylane/pull/3388)
+    [(#3286)](https://github.com/PennyLaneAI/pennylane/pull/3286)
+    [(#3388)](https://github.com/PennyLaneAI/pennylane/pull/3388)
 
   * Add `_Expectation` class.
-    [#3343](https://github.com/PennyLaneAI/pennylane/pull/3343)
+    [(#3343)](https://github.com/PennyLaneAI/pennylane/pull/3343)
 
   * Add `_Sample` class.
-    [#3288](https://github.com/PennyLaneAI/pennylane/pull/3288)
+    [(#3288)](https://github.com/PennyLaneAI/pennylane/pull/3288)
 
   * Add `_Var` class.
-    [#3312](https://github.com/PennyLaneAI/pennylane/pull/3312)
+    [(#3312)](https://github.com/PennyLaneAI/pennylane/pull/3312)
 
   * Add `_Probability` class.
-    [#3287](https://github.com/PennyLaneAI/pennylane/pull/3287)
+    [(#3287)](https://github.com/PennyLaneAI/pennylane/pull/3287)
 
   * Add `_Counts` class.
-    [#3292](https://github.com/PennyLaneAI/pennylane/pull/3292)
+    [(#3292)](https://github.com/PennyLaneAI/pennylane/pull/3292)
 
   * Add `_State` class.
-    [#3287](https://github.com/PennyLaneAI/pennylane/pull/3287)
+    [(#3287)](https://github.com/PennyLaneAI/pennylane/pull/3287)
 
   * Add `_VnEntropy` class.
-    [#3326](https://github.com/PennyLaneAI/pennylane/pull/3326)
+    [(#3326)](https://github.com/PennyLaneAI/pennylane/pull/3326)
 
   * Add `_MutualInfo` class.
-    [#3327](https://github.com/PennyLaneAI/pennylane/pull/3327)
+    [(#3327)](https://github.com/PennyLaneAI/pennylane/pull/3327)
 
   * Add `ClassicalShadow` class.
-    [#3388](https://github.com/PennyLaneAI/pennylane/pull/3388)
+    [(#3388)](https://github.com/PennyLaneAI/pennylane/pull/3388)
 
   * Add `_ShadowExpval` class.
-    [#3388](https://github.com/PennyLaneAI/pennylane/pull/3388)
+    [(#3388)](https://github.com/PennyLaneAI/pennylane/pull/3388)
 
 * Functionality for fetching symbols and geometry of a compound from the PubChem Database using `qchem.mol_data`.
   [(#3289)](https://github.com/PennyLaneAI/pennylane/pull/3289)
@@ -91,7 +91,25 @@
   ```
 
 * New basis sets, `6-311g` and `CC-PVDZ`, are added to the qchem basis set repo.
-  [#3279](https://github.com/PennyLaneAI/pennylane/pull/3279)
+  [(#3279)](https://github.com/PennyLaneAI/pennylane/pull/3279)
+  
+* Added two new methods `operation()`, `hamiltonian()` for both `PauliSentence` and `PauliWord` classes to generate an equivalent PennyLane 
+  `Operation` or `Hamiltonian` instance from a `PauliSentence` or `PauliWord` one.
+  [(#3391)](https://github.com/PennyLaneAI/pennylane/pull/3391)
+
+  ```pycon
+  >>> pw = qml.pauli.PauliWord({0: 'X', 1: 'Y'})
+  >>> print(pw.operation())
+  PauliX(wires=[0]) @ PauliY(wires=[1])
+  >>> print(pw.hamiltonian())
+    (1) [X0 Y1]
+  >>>
+  >>> ps = qml.pauli.PauliSentence({pw: -1.23})
+  >>> print(ps.operation())
+  -1.23*(PauliX(wires=[0]) @ PauliY(wires=[1]))
+  >>> print(ps.hamiltonian())
+    (-1.23) [X0 Y1]
+  ```
 
 * Added a new gate operation `FermionicSWAP`, which implements the exchange of spin orbitals
   representing fermionic-modes while maintaining proper anti-symmetrization.
@@ -121,7 +139,7 @@
 
 * Support for purity computation is added. The `qml.math.purity` function computes the purity from a state vector or a density matrix:
 
-  [#3290](https://github.com/PennyLaneAI/pennylane/pull/3290)
+  [(#3290)](https://github.com/PennyLaneAI/pennylane/pull/3290)
 
   ```pycon
   >>> x = [1, 0, 0, 1] / np.sqrt(2)
@@ -192,7 +210,7 @@
   [(#3390)](https://github.com/PennyLaneAI/pennylane/pull/3390)
 
 * Remove private `_wires` setter from the `Controlled.map_wires` method.
-  [3405](https://github.com/PennyLaneAI/pennylane/pull/3405)
+  [(#3405)](https://github.com/PennyLaneAI/pennylane/pull/3405)
 
 * `QuantumTape._process_queue` has been moved to `qml.queuing.process_queue` to disentangle
   its functionality from the `QuantumTape` class.
@@ -205,7 +223,7 @@
 <h4>Return types project</h4>
 
 * The autograd interface for the new return types now supports devices with shot vectors.
-  [#3374](https://github.com/PennyLaneAI/pennylane/pull/3374)
+  [(#3374)](https://github.com/PennyLaneAI/pennylane/pull/3374)
 
   Example with a single measurement:
 
@@ -266,7 +284,7 @@
   ```
 
 * The TensorFlow interface for the new return types now supports devices with shot vectors.
-  [#3400](https://github.com/PennyLaneAI/pennylane/pull/3400)
+  [(#3400)](https://github.com/PennyLaneAI/pennylane/pull/3400)
 
   Example with a single measurement:
 
@@ -324,17 +342,17 @@
   ```
 
 * Updated `qml.transforms.split_non_commuting` to support the new return types.
-  [#3414](https://github.com/PennyLaneAI/pennylane/pull/3414)
+  [(#3414)](https://github.com/PennyLaneAI/pennylane/pull/3414)
 
 * Updated `qml.transforms.mitigate_with_zne` to support the new return types.
-  [#3415](https://github.com/PennyLaneAI/pennylane/pull/3415)
+  [(#3415)](https://github.com/PennyLaneAI/pennylane/pull/3415)
 
 
 <h3>Breaking changes</h3>
 
 * The `log_base` attribute has been moved from `MeasurementProcess` to the new `_VnEntropy` and
   `_MutualInfo` classes, which inherit from `MeasurementProcess`.
-  [#3326](https://github.com/PennyLaneAI/pennylane/pull/3326)
+  [(#3326)](https://github.com/PennyLaneAI/pennylane/pull/3326)
 
 * Python 3.7 support is no longer maintained.
   [(#3276)](https://github.com/PennyLaneAI/pennylane/pull/3276)
@@ -346,7 +364,7 @@
 
 * Change class name `ShadowMeasurementProcess` to `ClassicalShadow`, to be consistent with the
   `qml.classical_shadow` function name.
-  [#3388](https://github.com/PennyLaneAI/pennylane/pull/3388)
+  [(#3388)](https://github.com/PennyLaneAI/pennylane/pull/3388)
 
 * The method `qml.Operation.get_parameter_shift` is removed. The `gradients` module should be used
   for general parameter-shift rules instead.
@@ -364,14 +382,14 @@
   def statistics(self, circuit: QuantumScript, shot_range=None, bin_size=None):
   ```
 
-  [#3421](https://github.com/PennyLaneAI/pennylane/pull/3421)
+  [(#3421)](https://github.com/PennyLaneAI/pennylane/pull/3421)
 
 * The `MeasurementProcess.return_type` argument has been removed from the `__init__` method. Now
   it is a property of the class.
-  [#3434](https://github.com/PennyLaneAI/pennylane/pull/3434)
+  [(#3434)](https://github.com/PennyLaneAI/pennylane/pull/3434)
 
 * The `MeasurementProcess` class is now an abstract class.
-  [#3434](https://github.com/PennyLaneAI/pennylane/pull/3434)
+  [(#3434)](https://github.com/PennyLaneAI/pennylane/pull/3434)
 
 <h3>Deprecations</h3>
 
@@ -423,16 +441,16 @@ Deprecations cycles are tracked at [doc/developement/deprecations.rst](https://d
   [(#3177)](https://github.com/PennyLaneAI/pennylane/pull/3177)
 
 * Original tape `_obs_sharing_wires` attribute is updated during its expansion.
-  [#3293](https://github.com/PennyLaneAI/pennylane/pull/3293)
+  [(#3293)](https://github.com/PennyLaneAI/pennylane/pull/3293)
 
 * Small fix of `MeasurementProcess.map_wires`, where both the `self.obs` and `self._wires`
   attributes were modified.
-  [#3292](https://github.com/PennyLaneAI/pennylane/pull/3292)
+  [(#3292)](https://github.com/PennyLaneAI/pennylane/pull/3292)
 
 * An issue with `drain=False` in the adaptive optimizer is fixed. Before the fix, the operator pool
   needed to be re-constructed inside the optimization pool when `drain=False`. With the new fix,
   this reconstruction is not needed.
-  [#3361](https://github.com/PennyLaneAI/pennylane/pull/3361)
+  [(#3361)](https://github.com/PennyLaneAI/pennylane/pull/3361)
 
 * If the device originally has no shots but finite shots are dynamically specified, Hamiltonian
   expansion now occurs.
@@ -447,7 +465,7 @@ Deprecations cycles are tracked at [doc/developement/deprecations.rst](https://d
 
 * Fixed a bug where a QNode returning `qml.sample` would produce incorrect results when
   run on a device defined with a shot vector.
-  [#3422](https://github.com/PennyLaneAI/pennylane/pull/3422)
+  [(#3422)](https://github.com/PennyLaneAI/pennylane/pull/3422)
 
 <h3>Contributors</h3>
 
diff --git a/pennylane/ops/op_math/exp.py b/pennylane/ops/op_math/exp.py
index a63549fac..5ba333cfe 100644
--- a/pennylane/ops/op_math/exp.py
+++ b/pennylane/ops/op_math/exp.py
@@ -25,7 +25,6 @@ from pennylane.operation import (
     OperatorPropertyUndefined,
     Tensor,
 )
-from pennylane.pauli import is_pauli_word, pauli_word_to_string
 from pennylane.wires import Wires
 
 from .symbolicop import SymbolicOp
@@ -179,7 +178,7 @@ class Exp(SymbolicOp):
                     "when the base operator is a Tensor object with overlapping wires. "
                     f"Received base {self.base}."
                 )
-        return math.real(self.coeff) == 0 and is_pauli_word(self.base)
+        return math.real(self.coeff) == 0 and qml.pauli.is_pauli_word(self.base)
 
     def decomposition(self):
         r"""Representation of the operator as a product of other operators. Decomposes into
@@ -196,7 +195,7 @@ class Exp(SymbolicOp):
         if not self.has_decomposition:
             raise DecompositionUndefinedError
         new_coeff = math.real(2j * self.coeff)
-        string_base = pauli_word_to_string(self.base)
+        string_base = qml.pauli.pauli_word_to_string(self.base)
         return [qml.PauliRot(new_coeff, string_base, wires=self.wires)]
 
     def matrix(self, wire_order=None):
diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py
index 67bb6fd5a..e7fc6a512 100644
--- a/pennylane/pauli/pauli_arithmetic.py
+++ b/pennylane/pauli/pauli_arithmetic.py
@@ -18,12 +18,29 @@ from functools import reduce
 import numpy as np
 from scipy import sparse
 from pennylane import math, wires
+from pennylane.operation import Tensor
+from pennylane.ops import s_prod, op_sum, prod, Identity, PauliX, PauliY, PauliZ, Hamiltonian
+
 
 I = "I"
 X = "X"
 Y = "Y"
 Z = "Z"
 
+op_map = {
+    I: Identity,
+    X: PauliX,
+    Y: PauliY,
+    Z: PauliZ,
+}
+
+op_to_str_map = {
+    Identity: I,
+    PauliX: X,
+    PauliY: Y,
+    PauliZ: Z,
+}
+
 matI = np.eye(2)
 matX = np.array([[0, 1], [1, 0]])
 matY = np.array([[0, -1j], [1j, 0]])
@@ -186,6 +203,26 @@ class PauliWord(dict):
 
         return reduce(kron, (matrix_map[self[w]] for w in wire_order))
 
+    def operation(self, wire_order=None):
+        """Returns a native PennyLane``~.Operator`` representing the PauliWord."""
+        if len(self) == 0:
+            if wire_order in (None, [], wires.Wires([])):
+                raise ValueError("Can't get the operation for an empty PauliWord.")
+            return Identity(wires=wire_order)
+
+        factors = [op_map[op](wire) for wire, op in self.items()]
+        return factors[0] if len(factors) == 1 else prod(*factors)
+
+    def hamiltonian(self, wire_order=None):
+        """Return ~.Hamiltonian representing the PauliWord"""
+        if len(self) == 0:
+            if wire_order in (None, [], wires.Wires([])):
+                raise ValueError("Can't get the Hamiltonian for an empty PauliWord.")
+            return Hamiltonian([1], [Identity(wires=wire_order)])
+
+        obs = [op_map[op](wire) for wire, op in self.items()]
+        return Hamiltonian([1], [obs[0] if len(obs) == 1 else Tensor(*obs)])
+
 
 class PauliSentence(dict):
     """Dictionary representing a linear combination of Pauli words, with the keys
@@ -257,19 +294,30 @@ class PauliSentence(dict):
         Rasies:
             ValueError: Can't get the matrix of an empty PauliSentence.
         """
+
+        def _pw_wires(w):
+            """To account for empty pauli_words which represent identity operations."""
+            if w:
+                return wires.Wires(w)
+
+            ps_wires = self.wires
+            if len(ps_wires) > 0:
+                return wires.Wires(
+                    list(ps_wires)[0]
+                )  # return any wire from the Pauli sentence's wires
+            return wires.Wires([])
+
         if len(self) == 0:
             if wire_order is None or wire_order == wires.Wires([]):
                 raise ValueError("Can't get the matrix of an empty PauliSentence.")
-            return (
-                np.eye(2 ** len(wire_order))
-                if format == "dense"
-                else sparse.eye(2 ** len(wire_order), format=format)
-            )
+            if format == "dense":
+                return np.eye(2 ** len(wire_order))
+            return sparse.eye(2 ** len(wire_order), format=format)
 
         mats_and_wires_gen = (
             (
-                coeff * pw.to_mat(wire_order=wires.Wires(list(pw.wires)), format=format),
-                wires.Wires(list(pw.wires)),
+                coeff * pw.to_mat(wire_order=_pw_wires(list(pw.wires)), format=format),
+                _pw_wires(list(pw.wires)),
             )
             for pw, coeff in self.items()
         )
@@ -280,6 +328,29 @@ class PauliSentence(dict):
 
         return math.expand_matrix(reduced_mat, result_wire_order, wire_order=wire_order)
 
+    def operation(self, wire_order=None):
+        """Returns a native PennyLane``~.Operator`` representing the PauliSentence."""
+        if len(self) == 0:
+            if wire_order in (None, [], wires.Wires([])):
+                raise ValueError("Can't get the operation for an empty PauliSentence.")
+            return Identity(wires=wire_order)
+
+        summands = [
+            s_prod(coeff, pw.operation(wire_order=list(self.wires))) for pw, coeff in self.items()
+        ]
+        return summands[0] if len(summands) == 1 else op_sum(*summands)
+
+    def hamiltonian(self, wire_order=None):
+        """Returns a native PennyLane ~.Hamiltonian representing the PauliSentence."""
+        if len(self) == 0:
+            if wire_order in (None, [], wires.Wires([])):
+                raise ValueError("Can't get the Hamiltonian for an empty PauliSentence.")
+            return Hamiltonian([1], [Identity(wires=wire_order)])
+
+        return sum(
+            coeff * pw.hamiltonian(wire_order=list(self.wires)) for pw, coeff in self.items()
+        )
+
     def simplify(self, tol=1e-8):
         """Remove any PauliWords in the PauliSentence with coefficients less than the threshold tolerance."""
         items = list(self.items())


Test patch:
diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py
index ec8ca8795..8c5ae938f 100644
--- a/tests/pauli/test_pauli_arithmetic.py
+++ b/tests/pauli/test_pauli_arithmetic.py
@@ -16,6 +16,7 @@ import pytest
 from copy import copy
 
 from scipy import sparse
+import pennylane as qml
 from pennylane import numpy as np
 from pennylane.wires import Wires
 from pennylane.pauli.pauli_arithmetic import PauliWord, PauliSentence, I, X, Y, Z
@@ -38,6 +39,8 @@ pw4 = PauliWord({})
 
 ps1 = PauliSentence({pw1: 1.23, pw2: 4j, pw3: -0.5})
 ps2 = PauliSentence({pw1: -1.23, pw2: -4j, pw3: 0.5})
+ps1_hamiltonian = PauliSentence({pw1: 1.23, pw2: 4, pw3: -0.5})
+ps2_hamiltonian = PauliSentence({pw1: -1.23, pw2: -4, pw3: 0.5})
 ps3 = PauliSentence({pw3: -0.5, pw4: 1})
 ps4 = PauliSentence({pw4: 1})
 ps5 = PauliSentence({})
@@ -154,6 +157,63 @@ class TestPauliWord:
         assert sparse.issparse(sparse_mat)
         assert np.allclose(sparse_mat.toarray(), true_matrix)
 
+    tup_pw_operation = (
+        (PauliWord({0: X}), qml.PauliX(wires=0)),
+        (pw1, qml.prod(qml.PauliX(wires=1), qml.PauliY(wires=2))),
+        (pw2, qml.prod(qml.PauliX(wires="a"), qml.PauliX(wires="b"), qml.PauliZ(wires="c"))),
+        (pw3, qml.prod(qml.PauliZ(wires=0), qml.PauliZ(wires="b"), qml.PauliZ(wires="c"))),
+    )
+
+    @pytest.mark.parametrize("pw, op", tup_pw_operation)
+    def test_operation(self, pw, op):
+        """Test that a PauliWord can be cast to a PL operation."""
+        pw_op = pw.operation()
+        if len(pw) > 1:
+            for pw_factor, op_factor in zip(pw_op.operands, op.operands):
+                assert pw_factor.name == op_factor.name
+                assert pw_factor.wires == op_factor.wires
+        else:
+            assert pw_op.name == op.name
+            assert pw_op.wires == op.wires
+
+    def test_operation_empty(self):
+        """Test that an empty PauliWord with wire_order returns Identity."""
+        op = PauliWord({}).operation(wire_order=[0, 1])
+        id = qml.Identity(wires=[0, 1])
+        assert op.name == id.name
+        assert op.wires == id.wires
+
+    def test_operation_empty_error(self):
+        """Test that a ValueError is raised if an empty PauliWord is
+        cast to a PL operation."""
+        with pytest.raises(ValueError, match="Can't get the operation for an empty PauliWord."):
+            pw4.operation()
+
+    tup_pw_hamiltonian = (
+        (PauliWord({0: X}), 1 * qml.PauliX(wires=0)),
+        (pw1, 1 * qml.PauliX(wires=1) @ qml.PauliY(wires=2)),
+        (pw2, 1 * qml.PauliX(wires="a") @ qml.PauliX(wires="b") @ qml.PauliZ(wires="c")),
+        (pw3, 1 * qml.PauliZ(wires=0) @ qml.PauliZ(wires="b") @ qml.PauliZ(wires="c")),
+    )
+
+    @pytest.mark.parametrize("pw, h", tup_pw_hamiltonian)
+    def test_hamiltonian(self, pw, h):
+        """Test that a PauliWord can be cast to a Hamiltonian."""
+        pw_h = pw.hamiltonian()
+        assert pw_h.compare(h)
+
+    def test_hamiltonian_empty(self):
+        """Test that an empty PauliWord with wire_order returns Identity Hamiltonian."""
+        op = PauliWord({}).hamiltonian(wire_order=[0, 1])
+        id = 1 * qml.Identity(wires=[0, 1])
+        assert op.compare(id)
+
+    def test_hamiltonian_empty_error(self):
+        """Test that a ValueError is raised if an empty PauliWord is
+        cast to a Hamiltonian."""
+        with pytest.raises(ValueError, match="Can't get the Hamiltonian for an empty PauliWord."):
+            pw4.hamiltonian()
+
 
 class TestPauliSentence:
     def test_missing(self):
@@ -271,7 +331,6 @@ class TestPauliSentence:
         assert ps2 == copy_ps2
 
     ps_match = (
-        (ps3, "Can't get the matrix of an empty PauliWord."),
         (ps4, "Can't get the matrix of an empty PauliWord."),
         (ps5, "Can't get the matrix of an empty PauliSentence."),
     )
@@ -306,6 +365,11 @@ class TestPauliSentence:
             - 4j * np.kron(np.kron(matX, np.kron(matX, matZ)), np.eye(8))
             + 0.5 * np.kron(np.kron(matI, np.kron(matZ, np.kron(matZ, matZ))), np.eye(4)),
         ),
+        (
+            ps3,
+            [0, "b", "c"],
+            -0.5 * np.kron(matZ, np.kron(matZ, matZ)) + 1 * np.eye(8),
+        ),
     )
 
     @pytest.mark.parametrize("ps, wire_order, true_matrix", tup_ps_mat)
@@ -336,3 +400,118 @@ class TestPauliSentence:
         assert un_simplified_ps == expected_simplified_ps1
         un_simplified_ps.simplify(tol=1e-1)
         assert un_simplified_ps == expected_simplified_ps2
+
+    tup_ps_operation = (
+        (PauliSentence({PauliWord({0: X}): 1}), qml.s_prod(1, qml.PauliX(wires=0))),
+        (
+            ps1_hamiltonian,
+            qml.op_sum(
+                1.23 * qml.prod(qml.PauliX(wires=1), qml.PauliY(wires=2)),
+                4 * qml.prod(qml.PauliX(wires="a"), qml.PauliX(wires="b"), qml.PauliZ(wires="c")),
+                -0.5 * qml.prod(qml.PauliZ(wires=0), qml.PauliZ(wires="b"), qml.PauliZ(wires="c")),
+            ),
+        ),
+        (
+            ps2_hamiltonian,
+            qml.op_sum(
+                -1.23 * qml.prod(qml.PauliX(wires=1), qml.PauliY(wires=2)),
+                -4 * qml.prod(qml.PauliX(wires="a"), qml.PauliX(wires="b"), qml.PauliZ(wires="c")),
+                0.5 * qml.prod(qml.PauliZ(wires=0), qml.PauliZ(wires="b"), qml.PauliZ(wires="c")),
+            ),
+        ),
+    )
+
+    @pytest.mark.parametrize("ps, op", tup_ps_operation)
+    def test_operation(self, ps, op):
+        """Test that a PauliSentence can be cast to a PL operation."""
+
+        def _compare_ops(op1, op2):
+            assert op1.name == op2.name
+            assert op1.wires == op2.wires
+
+        ps_op = ps.operation()
+        if len(ps) > 1:
+            for ps_summand, op_summand in zip(ps_op.operands, op.operands):
+                assert ps_summand.scalar == op_summand.scalar
+                if isinstance(ps_summand.base, qml.ops.Prod):
+                    for pw_factor, op_factor in zip(ps_summand.base, op_summand.base):
+                        _compare_ops(pw_factor, op_factor)
+                else:
+                    ps_base, op_base = (ps_summand.base, op_summand.base)
+                    _compare_ops(ps_base, op_base)
+
+    def test_operation_with_identity(self):
+        """Test that a PauliSentence with an empty PauliWord can be cast to
+        operation correctly."""
+        full_ps_op = ps3.operation()
+        full_op = qml.op_sum(
+            -0.5 * qml.prod(qml.PauliZ(wires=0), qml.PauliZ(wires="b"), qml.PauliZ(wires="c")),
+            qml.s_prod(1, qml.Identity(wires=[0, "b", "c"])),
+        )
+
+        ps_op, op = (
+            full_ps_op.operands[1],
+            full_op.operands[1],
+        )  # testing that the identity term is constructed well
+        assert ps_op.scalar == op.scalar
+
+        ps_base, op_base = (ps_op.base, op.base)
+        assert ps_base.name == op_base.name
+        assert set(ps_base.wires) == set(op_base.wires)
+        # in constructing the identity wires are cast from set -> list and the order is not preserved
+
+    def test_operation_empty(self):
+        """Test that an empty PauliSentence with wire_order returns Identity."""
+        op = ps5.operation(wire_order=[0, 1])
+        id = qml.Identity(wires=[0, 1])
+        assert op.name == id.name
+        assert op.wires == id.wires
+
+    def test_operation_empty_error(self):
+        """Test that a ValueError is raised if an empty PauliSentence is
+        cast to a PL operation."""
+        with pytest.raises(ValueError, match="Can't get the operation for an empty PauliWord."):
+            ps4.operation()
+        with pytest.raises(ValueError, match="Can't get the operation for an empty PauliSentence."):
+            ps5.operation()
+
+    tup_ps_hamiltonian = (
+        (PauliSentence({PauliWord({0: X}): 1}), 1 * qml.PauliX(wires=0)),
+        (
+            ps1_hamiltonian,
+            +1.23 * qml.PauliX(wires=1) @ qml.PauliY(wires=2)
+            + 4 * qml.PauliX(wires="a") @ qml.PauliX(wires="b") @ qml.PauliZ(wires="c")
+            - 0.5 * qml.PauliZ(wires=0) @ qml.PauliZ(wires="b") @ qml.PauliZ(wires="c"),
+        ),
+        (
+            ps2_hamiltonian,
+            -1.23 * qml.PauliX(wires=1) @ qml.PauliY(wires=2)
+            - 4 * qml.PauliX(wires="a") @ qml.PauliX(wires="b") @ qml.PauliZ(wires="c")
+            + 0.5 * qml.PauliZ(wires=0) @ qml.PauliZ(wires="b") @ qml.PauliZ(wires="c"),
+        ),
+        (
+            ps3,
+            -0.5 * qml.PauliZ(wires=0) @ qml.PauliZ(wires="b") @ qml.PauliZ(wires="c")
+            + 1 * qml.Identity(wires=[0, "b", "c"]),
+        ),
+    )
+
+    @pytest.mark.parametrize("ps, h", tup_ps_hamiltonian)
+    def test_hamiltonian(self, ps, h):
+        """Test that a PauliSentence can be cast to a Hamiltonian."""
+        ps_h = ps.hamiltonian()
+        assert ps_h.compare(h)
+
+    def test_hamiltonian_empty(self):
+        """Test that an empty PauliSentence with wire_order returns Identity."""
+        op = ps5.hamiltonian(wire_order=[0, 1])
+        id = 1 * qml.Identity(wires=[0, 1])
+        assert op.compare(id)
+
+    def test_hamiltonian_empty_error(self):
+        """Test that a ValueError is raised if an empty PauliSentence is
+        cast to a Hamiltonian."""
+        with pytest.raises(
+            ValueError, match="Can't get the Hamiltonian for an empty PauliSentence."
+        ):
+            ps5.hamiltonian()