Repository: PennyLaneAI/pennylane
Pull request #3443 (2022-11-29 16:09:08)

Integration of PauliArithmetic with native PL classes (Pauli Integration #1)
**Context:**
Now that we have a light weight representation, this PR adds an attribute to the native PL Pauli ops and arithmetic ops to track and update this representation.

Later PRs will update the simplify and matrix methods of PL ops to rely on this representation.

**Description of the Change:**
- Add a private attribute `_pauli_rep` which is `NotImplemented` for all operators except for the base Pauli ops and applicable arithmetic ops. 
- Fix an issue where multiplying an empty PauliSentence `PauliSentence({})` with any other Pauli sentence, would return an empty `PauliSentence({})`.  

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://codecov.io/gh/PennyLaneAI/pennylane/pull/3443?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3443](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (436ab72) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/79738a10001bff2f205ef36379bc99368edb748d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (79738a1) will **decrease** coverage by `0.03%`.
> The diff coverage is `86.88%`.

```diff
@@            Coverage Diff             @@
##           master    #3443      +/-   ##
==========================================
- Coverage   99.70%   99.67%   -0.04%     
==========================================
  Files         299      299              
  Lines       26637    26695      +58     
==========================================
+ Hits        26558    26607      +49     
- Misses         79       88       +9     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443?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/prod.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL3Byb2QucHk=) | `98.12% <33.33%> (-1.88%)` | :arrow_down: |
| [pennylane/ops/op\_math/sum.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL3N1bS5weQ==) | `95.69% <42.85%> (-4.31%)` | :arrow_down: |
| [pennylane/operation.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wZXJhdGlvbi5weQ==) | `97.02% <100.00%> (+0.01%)` | :arrow_up: |
| [pennylane/ops/identity.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9pZGVudGl0eS5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/op\_math/pow.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL3Bvdy5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/op\_math/sprod.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL3Nwcm9kLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/qubit/non\_parametric\_ops.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9ub25fcGFyYW1ldHJpY19vcHMucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/pauli/pauli\_arithmetic.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/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%> (ø)` | |
| [pennylane/queuing.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3F1ZXVpbmcucHk=) | `98.85% <0.00%> (-1.15%)` | :arrow_down: |
| [pennylane/tape/qscript.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RhcGUvcXNjcmlwdC5weQ==) | `100.00% <0.00%> (ø)` | |
| ... and [3 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3443/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | |

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)


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 990e94dbd..f5831ae30 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -359,6 +359,9 @@
   Replaces `qml.transforms.make_tape` with `make_qscript`.
   [(#3429)](https://github.com/PennyLaneAI/pennylane/pull/3429)
 
+* Add a `_pauli_rep` attribute to operators to integrate the new Pauli arithmetic classes with native PennyLane objects.
+  [(#3443)](https://github.com/PennyLaneAI/pennylane/pull/3443)
+
 * Extended the functionality of `qml.matrix` to qutrits.
   [(#3460)](https://github.com/PennyLaneAI/pennylane/pull/3460)
 
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 5d3c5b911..61ee568a5 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -840,6 +840,7 @@ class Operator(abc.ABC):
         self._name = self.__class__.__name__  #: str: name of the operator
         self._id = id
         self.queue_idx = None  #: int, None: index of the Operator in the circuit queue, or None if not in a queue
+        self._pauli_rep = None  # Union[PauliSentence, None]: Representation of the operator as a pauli sentence, if applicable
 
         wires_from_args = False
         if wires is None:
diff --git a/pennylane/ops/identity.py b/pennylane/ops/identity.py
index fefe1d021..5f2a8038d 100644
--- a/pennylane/ops/identity.py
+++ b/pennylane/ops/identity.py
@@ -49,6 +49,7 @@ class Identity(CVObservable, Operation):
     def __init__(self, *params, wires=None, do_queue=True, id=None):
         super().__init__(*params, wires=wires, do_queue=do_queue, id=id)
         self._hyperparameters = {"n_wires": len(self.wires)}
+        self._pauli_rep = qml.pauli.PauliSentence({qml.pauli.PauliWord({}): 1.0})
 
     def label(self, decimals=None, base_label=None, cache=None):
         return base_label or "I"
diff --git a/pennylane/ops/op_math/composite.py b/pennylane/ops/op_math/composite.py
index 3b6a8edcb..fd1541e4a 100644
--- a/pennylane/ops/op_math/composite.py
+++ b/pennylane/ops/op_math/composite.py
@@ -60,6 +60,7 @@ class CompositeOp(Operator):
         self._hash = None
         self._has_overlapping_wires = None
         self._overlapping_ops = None
+        self._pauli_rep = self._build_pauli_rep()
 
         if do_queue:
             self.queue()
@@ -332,3 +333,7 @@ class CompositeOp(Operator):
                 setattr(new_op, attr, value)
 
         return new_op
+
+    @abc.abstractmethod
+    def _build_pauli_rep(self):
+        """The function to generate the pauli representation for the composite operator."""
diff --git a/pennylane/ops/op_math/pow.py b/pennylane/ops/op_math/pow.py
index 0ba214219..07a04f2e3 100644
--- a/pennylane/ops/op_math/pow.py
+++ b/pennylane/ops/op_math/pow.py
@@ -216,6 +216,21 @@ class Pow(SymbolicOp):
 
         super().__init__(base, do_queue=do_queue, id=id)
 
+        if isinstance(self.z, int) and self.z > 0:
+            if (
+                base_pauli_rep := getattr(
+                    self.base, "_pauli_rep", None
+                )  # pylint: disable=protected-access
+            ) is not None:
+                pr = qml.pauli.PauliSentence({})
+                for _ in range(self.z):
+                    pr = pr * base_pauli_rep
+                self._pauli_rep = pr
+            else:
+                self._pauli_rep = None
+        else:
+            self._pauli_rep = None
+
     def __repr__(self):
         return (
             f"({self.base})**{self.z}"
diff --git a/pennylane/ops/op_math/prod.py b/pennylane/ops/op_math/prod.py
index 39066673f..c75904a21 100644
--- a/pennylane/ops/op_math/prod.py
+++ b/pennylane/ops/op_math/prod.py
@@ -302,6 +302,16 @@ class Prod(CompositeOp):
     def arithmetic_depth(self) -> int:
         return 1 + max(factor.arithmetic_depth for factor in self)
 
+    def _build_pauli_rep(self):
+        """PauliSentence representation of the Product of operations."""
+        if all(
+            operand_pauli_reps := [
+                op._pauli_rep for op in self.operands  # pylint: disable=protected-access
+            ]
+        ):
+            return reduce(lambda a, b: a * b, operand_pauli_reps)
+        return None
+
     def _simplify_factors(self, factors: Tuple[Operator]) -> Tuple[complex, Operator]:
         """Reduces the depth of nested factors and groups identical factors.
 
diff --git a/pennylane/ops/op_math/sprod.py b/pennylane/ops/op_math/sprod.py
index f90245330..cdcb22c2d 100644
--- a/pennylane/ops/op_math/sprod.py
+++ b/pennylane/ops/op_math/sprod.py
@@ -21,6 +21,7 @@ from typing import Union
 import autoray
 
 import pennylane as qml
+import pennylane.math as qnp
 from pennylane.interfaces import SUPPORTED_INTERFACES
 from pennylane.operation import Operator
 from pennylane.ops.op_math.pow import Pow
@@ -114,6 +115,14 @@ class SProd(SymbolicOp):
         self._check_scalar_is_valid()
         super().__init__(base=base, do_queue=do_queue, id=id)
 
+        if base_pauli_rep := getattr(self.base, "_pauli_rep", None):
+            pr = {}
+            for pw, coeff in base_pauli_rep.items():
+                pr[pw] = qnp.dot(coeff, self.scalar)  # to support dispatching over interfaces
+            self._pauli_rep = qml.pauli.PauliSentence(pr)
+        else:
+            self._pauli_rep = None
+
     def __repr__(self):
         """Constructor-call-like representation."""
         return f"{self.scalar}*({self.base})"
diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py
index a45dc2d19..6a2a0d701 100644
--- a/pennylane/ops/op_math/sum.py
+++ b/pennylane/ops/op_math/sum.py
@@ -17,6 +17,7 @@ computing the sum of operations.
 """
 from copy import copy
 from typing import List
+from functools import reduce
 
 import numpy as np
 
@@ -204,6 +205,16 @@ class Sum(CompositeOp):
     def adjoint(self):
         return Sum(*(qml.adjoint(summand) for summand in self))
 
+    def _build_pauli_rep(self):
+        """PauliSentence representation of the Sum of operations."""
+        if all(
+            operand_pauli_reps := [
+                op._pauli_rep for op in self.operands  # pylint: disable=protected-access
+            ]
+        ):
+            return reduce((lambda a, b: a + b), operand_pauli_reps)
+        return None
+
     @classmethod
     def _simplify_summands(cls, summands: List[Operator]):
         """Reduces the depth of nested summands and groups equal terms together.
diff --git a/pennylane/ops/op_math/symbolicop.py b/pennylane/ops/op_math/symbolicop.py
index 3257fc305..8f4fb41d9 100644
--- a/pennylane/ops/op_math/symbolicop.py
+++ b/pennylane/ops/op_math/symbolicop.py
@@ -66,6 +66,7 @@ class SymbolicOp(Operator):
         self.hyperparameters["base"] = base
         self._id = id
         self.queue_idx = None
+        self._pauli_rep = None
 
         if do_queue:
             self.queue()
diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py
index 38346f28b..3821fb3d9 100644
--- a/pennylane/ops/qubit/non_parametric_ops.py
+++ b/pennylane/ops/qubit/non_parametric_ops.py
@@ -202,6 +202,10 @@ class PauliX(Observable, Operation):
 
     _queue_category = "_ops"
 
+    def __init__(self, *params, wires=None, do_queue=True, id=None):
+        super().__init__(*params, wires=wires, do_queue=do_queue, id=id)
+        self._pauli_rep = qml.pauli.PauliSentence({qml.pauli.PauliWord({self.wires[0]: "X"}): 1.0})
+
     def label(self, decimals=None, base_label=None, cache=None):
         return base_label or "X"
 
@@ -353,6 +357,10 @@ class PauliY(Observable, Operation):
 
     _queue_category = "_ops"
 
+    def __init__(self, *params, wires=None, do_queue=True, id=None):
+        super().__init__(*params, wires=wires, do_queue=do_queue, id=id)
+        self._pauli_rep = qml.pauli.PauliSentence({qml.pauli.PauliWord({self.wires[0]: "Y"}): 1.0})
+
     def label(self, decimals=None, base_label=None, cache=None):
         return base_label or "Y"
 
@@ -501,6 +509,10 @@ class PauliZ(Observable, Operation):
 
     _queue_category = "_ops"
 
+    def __init__(self, *params, wires=None, do_queue=True, id=None):
+        super().__init__(*params, wires=wires, do_queue=do_queue, id=id)
+        self._pauli_rep = qml.pauli.PauliSentence({qml.pauli.PauliWord({self.wires[0]: "Z"}): 1.0})
+
     def label(self, decimals=None, base_label=None, cache=None):
         return base_label or "Z"
 
diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py
index 5e73e74bd..a24cb7275 100644
--- a/pennylane/pauli/pauli_arithmetic.py
+++ b/pennylane/pauli/pauli_arithmetic.py
@@ -120,6 +120,11 @@ class PauliWord(dict):
         """Copy the PauliWord instance."""
         return PauliWord(dict(self.items()))
 
+    def __deepcopy__(self, memo):
+        res = self.__copy__()
+        memo[id(self)] = res
+        return res
+
     def __setitem__(self, key, item):
         """Restrict setting items after instantiation."""
         raise TypeError("PauliWord object does not support assignment")
@@ -254,11 +259,29 @@ class PauliSentence(dict):
 
         return larger_ps
 
+    def __copy__(self):
+        """Copy the PauliSentence instance."""
+        copied_ps = {}
+        for pw, coeff in self.items():
+            copied_ps[copy(pw)] = coeff
+        return PauliSentence(copied_ps)
+
+    def __deepcopy__(self, memo):
+        res = self.__copy__()
+        memo[id(self)] = res
+        return res
+
     def __mul__(self, other):
         """Multiply two Pauli sentences by iterating over each sentence and multiplying
         the Pauli words pair-wise"""
         final_ps = PauliSentence()
 
+        if len(self) == 0:
+            return copy(other)
+
+        if len(other) == 0:
+            return copy(self)
+
         for pw1 in self:
             for pw2 in other:
                 prod_pw, coeff = pw1 * pw2


Test patch:
diff --git a/tests/ops/op_math/test_composite.py b/tests/ops/op_math/test_composite.py
index 4536466b7..c8469614e 100644
--- a/tests/ops/op_math/test_composite.py
+++ b/tests/ops/op_math/test_composite.py
@@ -45,6 +45,9 @@ class ValidOp(CompositeOp):
     _op_symbol = "#"
     _math_op = None
 
+    def _build_pauli_rep(self):
+        return qml.pauli.PauliSentence({})
+
     @property
     def is_hermitian(self):
         return False
@@ -173,6 +176,11 @@ class TestConstruction:
         assert mapped_op[0].wires == Wires(5)
         assert mapped_op[1].wires == Wires(7)
 
+    def test_build_pauli_rep(self):
+        """Test the build_pauli_rep"""
+        op = ValidOp(*self.simple_operands)
+        assert op._build_pauli_rep() == qml.pauli.PauliSentence({})
+
 
 class TestMscMethods:
     """Test dunder and other visualizing methods."""
diff --git a/tests/ops/op_math/test_pow_op.py b/tests/ops/op_math/test_pow_op.py
index c3f75687f..304505034 100644
--- a/tests/ops/op_math/test_pow_op.py
+++ b/tests/ops/op_math/test_pow_op.py
@@ -357,6 +357,33 @@ class TestProperties:
         assert op.ndim_params == (0, 2)
         assert op.batch_size == 3
 
+    op_pauli_reps = (
+        (qml.PauliZ(wires=0), 1, qml.pauli.PauliSentence({qml.pauli.PauliWord({0: "Z"}): 1})),
+        (qml.PauliX(wires=1), 2, qml.pauli.PauliSentence({qml.pauli.PauliWord({}): 1})),  # identity
+        (qml.PauliY(wires="a"), 5, qml.pauli.PauliSentence({qml.pauli.PauliWord({"a": "Y"}): 1})),
+    )
+
+    @pytest.mark.parametrize("base, exp, rep", op_pauli_reps)
+    def test_pauli_rep(self, base, exp, rep, power_method):
+        """Test the pauli rep is produced as expected."""
+        op = power_method(base, exp)
+        assert op._pauli_rep == rep
+
+    def test_pauli_rep_is_none_for_bad_exponents(self, power_method):
+        """Test that the _pauli_rep is None if the exponent is not positive or non integer."""
+        base = qml.PauliX(wires=0)
+        exponents = [1.23, -2]
+
+        for exponent in exponents:
+            op = power_method(base, z=exponent)
+            assert op._pauli_rep is None
+
+    def test_pauli_rep_none_if_base_pauli_rep_none(self, power_method):
+        """Test that None is produced if the base op does not have a pauli rep"""
+        base = qml.RX(1.23, wires=0)
+        op = power_method(base, z=2)
+        assert op._pauli_rep is None
+
 
 class TestSimplify:
     """Test Pow simplify method and depth property."""
diff --git a/tests/ops/op_math/test_prod.py b/tests/ops/op_math/test_prod.py
index e9e5f8c58..0fbae773c 100644
--- a/tests/ops/op_math/test_prod.py
+++ b/tests/ops/op_math/test_prod.py
@@ -679,12 +679,7 @@ class TestProperties:
             """Dummy op with None queue category"""
 
             _queue_category = None
-
-            def __init__(self, wires):
-                self._wires = qml.wires.Wires([wires])
-
-            def num_wires(self):
-                return len(self.wires)
+            num_wires = 1
 
         prod_op = prod(qml.Identity(wires=0), DummyOp(wires=0))
         assert prod_op._queue_category is None
@@ -729,6 +724,63 @@ class TestProperties:
         diag_prod_op = Prod(qml.PauliZ(wires=0), qml.PauliZ(wires=1))
         assert diag_prod_op.diagonalizing_gates() == []
 
+    op_pauli_reps = (
+        (
+            qml.prod(qml.PauliX(wires=0), qml.PauliY(wires=1), qml.PauliZ(wires="a")),
+            qml.pauli.PauliSentence({qml.pauli.PauliWord({0: "X", 1: "Y", "a": "Z"}): 1}),
+        ),
+        (
+            qml.prod(qml.PauliX(wires=0), qml.PauliX(wires=0)),
+            qml.pauli.PauliSentence({qml.pauli.PauliWord({}): 1}),
+        ),
+        (
+            qml.prod(
+                qml.PauliX(wires=0),
+                qml.PauliY(wires=0),
+                qml.PauliZ(wires="a"),
+                qml.PauliZ(wires="a"),
+            ),
+            qml.pauli.PauliSentence({qml.pauli.PauliWord({0: "Z"}): 1j}),
+        ),
+    )
+
+    @pytest.mark.parametrize("op, rep", op_pauli_reps)
+    def test_pauli_rep(self, op, rep):
+        """Test that the pauli rep gives the expected result."""
+        assert op._pauli_rep == rep
+
+    def test_pauli_rep_none(self):
+        """Test that None is produced if any of the terms don't have a _pauli_rep property."""
+        op = qml.prod(qml.PauliX(wires=0), qml.RX(1.23, wires=1))
+        assert op._pauli_rep is None
+
+    op_pauli_reps_nested = (
+        (
+            qml.prod(
+                qml.pow(qml.prod(qml.PauliX(wires=0), qml.PauliY(wires=1)), z=3),
+                qml.pow(qml.prod(qml.PauliY(wires=0), qml.PauliZ(wires=2)), z=5),
+            ),
+            qml.pauli.PauliSentence({qml.pauli.PauliWord({0: "Z", 1: "Y", 2: "Z"}): 1j}),
+        ),  # prod + pow
+        (
+            qml.prod(
+                qml.s_prod(
+                    -2j,
+                    qml.prod(
+                        qml.s_prod(0.5, qml.PauliX(wires=0)), qml.s_prod(2, qml.PauliZ(wires=1))
+                    ),
+                ),
+                qml.s_prod(3, qml.PauliY(wires=0)),
+            ),
+            qml.pauli.PauliSentence({qml.pauli.PauliWord({0: "Z", 1: "Z"}): 6}),
+        ),  # prod + s_prod
+    )
+
+    @pytest.mark.parametrize("op, rep", op_pauli_reps_nested)
+    def test_pauli_rep_nested(self, op, rep):
+        """Test that the pauli rep gives the expected result."""
+        assert op._pauli_rep == rep
+
 
 class TestSimplify:
     """Test Prod simplify method and depth property."""
diff --git a/tests/ops/op_math/test_sprod.py b/tests/ops/op_math/test_sprod.py
index 6b096d733..4e750c93b 100644
--- a/tests/ops/op_math/test_sprod.py
+++ b/tests/ops/op_math/test_sprod.py
@@ -529,6 +529,32 @@ class TestProperties:
         assert op.label(decimals=2, cache=cache) == "-1.20*U(M0)"
         assert len(cache["matrices"]) == 1
 
+    op_pauli_reps = (
+        (
+            qml.s_prod(1.23, qml.PauliZ(wires=0)),
+            qml.pauli.PauliSentence({qml.pauli.PauliWord({0: "Z"}): 1.23}),
+        ),
+        (
+            qml.s_prod(-1j, qml.PauliX(wires=1)),
+            qml.pauli.PauliSentence({qml.pauli.PauliWord({1: "X"}): -1j}),
+        ),
+        (
+            qml.s_prod(1.23 - 4j, qml.PauliY(wires="a")),
+            qml.pauli.PauliSentence({qml.pauli.PauliWord({"a": "Y"}): 1.23 - 4j}),
+        ),
+    )
+
+    @pytest.mark.parametrize("op, rep", op_pauli_reps)
+    def test_pauli_rep(self, op, rep):
+        """Test the pauli rep is produced as expected."""
+        assert op._pauli_rep == rep
+
+    def test_pauli_rep_none_if_base_pauli_rep_none(self):
+        """Test that None is produced if the base op does not have a pauli rep"""
+        base = qml.RX(1.23, wires=0)
+        op = qml.s_prod(2, base)
+        assert op._pauli_rep is None
+
 
 class TestSimplify:
     """Test SProd simplify method and depth property."""
diff --git a/tests/ops/op_math/test_sum.py b/tests/ops/op_math/test_sum.py
index ae4986737..51be9193f 100644
--- a/tests/ops/op_math/test_sum.py
+++ b/tests/ops/op_math/test_sum.py
@@ -80,6 +80,10 @@ ops = (
 )
 
 
+def _get_pw(w, pauli_op):
+    return qml.pauli.PauliWord({w: pauli_op})
+
+
 def sum_using_dunder_method(*summands, do_queue=True, id=None):
     """Helper function which computes the sum of all the summands to invoke the
     __add__ dunder method."""
@@ -508,6 +512,131 @@ class TestProperties:
         assert np.allclose(eig_vals, true_eigvals)
         assert np.allclose(eig_vecs, true_eigvecs)
 
+    op_pauli_reps = (
+        (
+            qml.op_sum(qml.PauliX(wires=0), qml.PauliY(wires=0), qml.PauliZ(wires=0)),
+            qml.pauli.PauliSentence({_get_pw(0, "X"): 1, _get_pw(0, "Y"): 1, _get_pw(0, "Z"): 1}),
+        ),
+        (
+            qml.op_sum(qml.PauliX(wires=0), qml.PauliX(wires=0), qml.PauliZ(wires=0)),
+            qml.pauli.PauliSentence({_get_pw(0, "X"): 2, _get_pw(0, "Z"): 1}),
+        ),
+        (
+            qml.op_sum(
+                qml.PauliX(wires=0),
+                qml.PauliY(wires=1),
+                qml.PauliZ(wires="a"),
+                qml.PauliZ(wires="a"),
+            ),
+            qml.pauli.PauliSentence({_get_pw(0, "X"): 1, _get_pw(1, "Y"): 1, _get_pw("a", "Z"): 2}),
+        ),
+    )
+
+    @pytest.mark.parametrize("op, rep", op_pauli_reps)
+    def test_pauli_rep(self, op, rep):
+        """Test that the pauli rep gives the expected result."""
+        assert op._pauli_rep == rep
+
+    def test_pauli_rep_none(self):
+        """Test that None is produced if any of the summands don't have a _pauli_rep."""
+        op = qml.op_sum(qml.PauliX(wires=0), qml.RX(1.23, wires=1))
+        assert op._pauli_rep is None
+
+    op_pauli_reps_nested = (
+        (
+            qml.op_sum(
+                qml.pow(
+                    qml.op_sum(
+                        qml.pow(qml.PauliZ(wires=0), z=3),
+                        qml.pow(qml.PauliX(wires=1), z=2),
+                        qml.pow(qml.PauliY(wires=2), z=1),
+                    ),
+                    z=3,
+                ),
+                qml.PauliY(wires=2),
+            ),
+            qml.pauli.PauliSentence(
+                {
+                    qml.pauli.PauliWord({0: "Z"}): 7,
+                    qml.pauli.PauliWord({2: "Y"}): 8,
+                    qml.pauli.PauliWord({0: "Z", 2: "Y"}): 6,
+                    qml.pauli.PauliWord({}): 7,  # identity
+                }
+            ),
+        ),  # sum + pow
+        (
+            qml.prod(
+                qml.op_sum(
+                    qml.prod(
+                        qml.op_sum(qml.PauliX(wires=0), qml.PauliY(wires=1), qml.PauliZ(wires=2)),
+                        qml.op_sum(qml.PauliZ(wires=0), qml.PauliZ(wires=1), qml.PauliZ(wires=2)),
+                    ),
+                    qml.Identity(wires=1),
+                ),
+                qml.PauliY(wires=3),
+            ),
+            qml.pauli.PauliSentence(
+                {
+                    qml.pauli.PauliWord({0: "Y", 3: "Y"}): -1j,
+                    qml.pauli.PauliWord({0: "X", 1: "Z", 3: "Y"}): 1,
+                    qml.pauli.PauliWord({0: "X", 2: "Z", 3: "Y"}): 1,
+                    qml.pauli.PauliWord({0: "Z", 1: "Y", 3: "Y"}): 1,
+                    qml.pauli.PauliWord({1: "X", 3: "Y"}): 1j,
+                    qml.pauli.PauliWord({1: "Y", 2: "Z", 3: "Y"}): 1,
+                    qml.pauli.PauliWord({0: "Z", 2: "Z", 3: "Y"}): 1,
+                    qml.pauli.PauliWord({1: "Z", 2: "Z", 3: "Y"}): 1,
+                    qml.pauli.PauliWord({3: "Y"}): 2,
+                }
+            ),
+        ),  # sum + prod
+        (
+            qml.op_sum(
+                qml.s_prod(
+                    0.5,
+                    qml.op_sum(
+                        qml.s_prod(2j, qml.PauliX(wires=0)),
+                        qml.s_prod(-4, qml.PauliY(wires=1)),
+                    ),
+                ),
+                qml.s_prod(1.23 - 0.4j, qml.PauliZ(wires=2)),
+            ),
+            qml.pauli.PauliSentence(
+                {_get_pw(0, "X"): 1.0j, _get_pw(1, "Y"): -2.0, _get_pw(2, "Z"): 1.23 - 0.4j}
+            ),
+        ),  # sum + s_prod
+        (
+            qml.prod(
+                qml.s_prod(
+                    -2,
+                    qml.op_sum(
+                        qml.s_prod(1j, qml.PauliX(wires=0)),
+                        qml.PauliY(wires=1),
+                    ),
+                ),
+                qml.pow(
+                    qml.op_sum(
+                        qml.s_prod(3, qml.PauliZ(wires=0)),
+                        qml.PauliZ(wires=1),
+                    ),
+                    z=2,
+                ),
+            ),
+            qml.pauli.PauliSentence(
+                {
+                    qml.pauli.PauliWord({0: "X"}): -20j,
+                    qml.pauli.PauliWord({1: "Y"}): -20,
+                    qml.pauli.PauliWord({0: "Y", 1: "Z"}): -12,
+                    qml.pauli.PauliWord({0: "Z", 1: "X"}): -12j,
+                }
+            ),
+        ),  # mixed
+    )
+
+    @pytest.mark.parametrize("op, rep", op_pauli_reps_nested)
+    def test_pauli_rep_nested(self, op, rep):
+        """Test that the pauli rep gives the expected result."""
+        assert op._pauli_rep == rep
+
 
 class TestSimplify:
     """Test Sum simplify method and depth property."""
diff --git a/tests/ops/op_math/test_symbolic_op.py b/tests/ops/op_math/test_symbolic_op.py
index 7451fa5bc..57cdc45e7 100644
--- a/tests/ops/op_math/test_symbolic_op.py
+++ b/tests/ops/op_math/test_symbolic_op.py
@@ -161,6 +161,12 @@ class TestProperties:
         op = SymbolicOp(t)
         assert op.num_wires == 3
 
+    def test_pauli_rep(self):
+        """Test that pauli_rep is None by default"""
+        base = TempOperator("a")
+        op = SymbolicOp(base)
+        assert op._pauli_rep is None
+
 
 class TestQueuing:
     """Test that Symbolic Operators queue and update base metadata."""
diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py
index e0570bb81..761b7158b 100644
--- a/tests/ops/qubit/test_non_parametric_ops.py
+++ b/tests/ops/qubit/test_non_parametric_ops.py
@@ -1454,3 +1454,16 @@ def test_adjoint_method(op, tol):
         adj_op = adj_op.adjoint()
 
         assert adj_op.name == op.name
+
+
+op_pauli_rep = (
+    (qml.PauliX(wires=0), qml.pauli.PauliSentence({qml.pauli.PauliWord({0: "X"}): 1})),
+    (qml.PauliY(wires="a"), qml.pauli.PauliSentence({qml.pauli.PauliWord({"a": "Y"}): 1})),
+    (qml.PauliZ(wires=4), qml.pauli.PauliSentence({qml.pauli.PauliWord({4: "Z"}): 1})),
+    (qml.Identity(wires="target"), qml.pauli.PauliSentence({qml.pauli.PauliWord({}): 1})),
+)
+
+
+@pytest.mark.parametrize("op, rep", op_pauli_rep)
+def test_pauli_rep(op, rep):
+    assert op._pauli_rep == rep
diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py
index 8c5ae938f..487d6b6e4 100644
--- a/tests/pauli/test_pauli_arithmetic.py
+++ b/tests/pauli/test_pauli_arithmetic.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 """Unit Tests for the PauliWord and PauliSentence classes"""
 import pytest
-from copy import copy
+from copy import copy, deepcopy
 
 from scipy import sparse
 import pennylane as qml
@@ -85,8 +85,12 @@ class TestPauliWord:
     def test_copy(self, pw):
         """Test that the copy is identical to the original."""
         copy_pw = copy(pw)
+        deep_copy_pw = deepcopy(pw)
+
         assert copy_pw == pw
+        assert deep_copy_pw == pw
         assert copy_pw is not pw
+        assert deep_copy_pw is not pw
 
     tup_pws_wires = ((pw1, {1, 2}), (pw2, {"a", "b", "c"}), (pw3, {0, "b", "c"}), (pw4, set()))
 
@@ -268,6 +272,17 @@ class TestPauliSentence:
         """Test the correct wires are given for the PauliSentence."""
         assert ps.wires == wires
 
+    @pytest.mark.parametrize("ps", (ps1, ps2, ps3, ps4))
+    def test_copy(self, ps):
+        """Test that the copy is identical to the original."""
+        copy_ps = copy(ps)
+        deep_copy_ps = deepcopy(ps)
+
+        assert copy_ps == ps
+        assert deep_copy_ps == ps
+        assert copy_ps is not ps
+        assert deep_copy_ps is not ps
+
     tup_ps_mult = (  # computed by hand
         (
             ps1,
@@ -295,6 +310,9 @@ class TestPauliSentence:
             ),
         ),
         (ps3, ps4, ps3),
+        (ps4, ps3, ps3),
+        (ps1, ps5, ps1),
+        (ps5, ps1, ps1),
     )
 
     @pytest.mark.parametrize("ps1, ps2, res", tup_ps_mult)
diff --git a/tests/test_operation.py b/tests/test_operation.py
index a3d56c4a9..9d945f9b0 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -517,6 +517,16 @@ class TestOperatorConstruction:
         assert op.wires == Wires([0, 1, 2])
         assert mapped_op.wires == Wires([10, 1, 12])
 
+    def test_default_pauli_rep(self):
+        """Test that the default _pauli_rep attribute is None"""
+
+        class DummyOp(qml.operation.Operator):
+            r"""Dummy custom operator"""
+            num_wires = 1
+
+        op = DummyOp(wires=0)
+        assert op._pauli_rep is None
+
 
 class TestOperationConstruction:
     """Test custom operations construction."""