diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 6d7d2fb373d..9f314673fc7 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -284,6 +284,10 @@
* `TRX`, `TRY`, and `TRZ` are now differentiable via backprop on `default.qutrit`
[(#4790)](https://github.com/PennyLaneAI/pennylane/pull/4790)
+* Operators now define a `pauli_rep` property, an instance of `PauliSentence`, defaulting
+ to `None` if the operator has not defined it (or has no definition in the pauli basis).
+ [(#4915)](https://github.com/PennyLaneAI/pennylane/pull/4915)
+
Breaking changes 💔
* The transforms submodule `qml.transforms.qcut` becomes its own module `qml.qcut`.
diff --git a/pennylane/devices/qubit/adjoint_jacobian.py b/pennylane/devices/qubit/adjoint_jacobian.py
index 1b8fa68a6df..7258c78a098 100644
--- a/pennylane/devices/qubit/adjoint_jacobian.py
+++ b/pennylane/devices/qubit/adjoint_jacobian.py
@@ -231,8 +231,8 @@ def adjoint_vjp(tape: QuantumTape, cotangents: Tuple[Number], state=None):
if len(new_cotangents) == 0:
return tuple(0.0 for _ in tape.trainable_params)
obs = qml.dot(new_cotangents, new_observables)
- if obs._pauli_rep is not None:
- flat_bra = obs._pauli_rep.dot(ket.flatten(), wire_order=list(range(tape.num_wires)))
+ if obs.pauli_rep is not None:
+ flat_bra = obs.pauli_rep.dot(ket.flatten(), wire_order=list(range(tape.num_wires)))
bra = flat_bra.reshape(ket.shape)
else:
bra = apply_operation(obs, ket)
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 9e328194ad1..37602f29f49 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -1228,6 +1228,11 @@ def hyperparameters(self):
self._hyperparameters = {}
return self._hyperparameters
+ @property
+ def pauli_rep(self):
+ """A :class:`~.PauliSentence` representation of the Operator, or ``None`` if it doesn't have one."""
+ return self._pauli_rep
+
@property
def is_hermitian(self):
"""This property determines if an operator is hermitian."""
@@ -1473,7 +1478,7 @@ def map_wires(self, wire_map: dict):
"""
new_op = copy.copy(self)
new_op._wires = Wires([wire_map.get(wire, wire) for wire in self.wires])
- if (p_rep := new_op._pauli_rep) is not None:
+ if (p_rep := new_op.pauli_rep) is not None:
new_op._pauli_rep = p_rep.map_wires(wire_map)
return new_op
diff --git a/pennylane/ops/op_math/composite.py b/pennylane/ops/op_math/composite.py
index f319bda29a8..f38bc930a19 100644
--- a/pennylane/ops/op_math/composite.py
+++ b/pennylane/ops/op_math/composite.py
@@ -352,7 +352,7 @@ def map_wires(self, wire_map: dict):
for attr, value in vars(self).items():
if attr not in {"data", "operands", "_wires"}:
setattr(new_op, attr, value)
- if (p_rep := new_op._pauli_rep) is not None:
+ if (p_rep := new_op.pauli_rep) is not None:
new_op._pauli_rep = p_rep.map_wires(wire_map)
return new_op
diff --git a/pennylane/ops/op_math/pow.py b/pennylane/ops/op_math/pow.py
index 06e04ce07a7..089bb93feae 100644
--- a/pennylane/ops/op_math/pow.py
+++ b/pennylane/ops/op_math/pow.py
@@ -357,7 +357,7 @@ def adjoint(self):
def simplify(self) -> Union["Pow", Identity]:
# try using pauli_rep:
- if pr := self._pauli_rep:
+ if pr := self.pauli_rep:
pr.simplify()
return pr.operation(wire_order=self.wires)
diff --git a/pennylane/ops/op_math/prod.py b/pennylane/ops/op_math/prod.py
index bff36d6069c..50307501edc 100644
--- a/pennylane/ops/op_math/prod.py
+++ b/pennylane/ops/op_math/prod.py
@@ -308,8 +308,8 @@ def matrix(self, wire_order=None):
return math.expand_matrix(full_mat, self.wires, wire_order=wire_order)
def sparse_matrix(self, wire_order=None):
- if self._pauli_rep: # Get the sparse matrix from the PauliSentence representation
- return self._pauli_rep.to_mat(wire_order=wire_order or self.wires, format="csr")
+ if self.pauli_rep: # Get the sparse matrix from the PauliSentence representation
+ return self.pauli_rep.to_mat(wire_order=wire_order or self.wires, format="csr")
if self.has_overlapping_wires or self.num_wires > MAX_NUM_WIRES_KRON_PRODUCT:
gen = ((op.sparse_matrix(), op.wires) for op in self)
@@ -353,11 +353,7 @@ def arithmetic_depth(self) -> int:
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
- ]
- ):
+ if all(operand_pauli_reps := [op.pauli_rep for op in self.operands]):
return reduce(lambda a, b: a * b, operand_pauli_reps)
return None
@@ -378,7 +374,7 @@ def _simplify_factors(self, factors: Tuple[Operator]) -> Tuple[complex, Operator
def simplify(self) -> Union["Prod", Sum]:
# try using pauli_rep:
- if pr := self._pauli_rep:
+ if pr := self.pauli_rep:
pr.simplify()
return pr.operation(wire_order=self.wires)
diff --git a/pennylane/ops/op_math/sprod.py b/pennylane/ops/op_math/sprod.py
index a9feeb45d80..9c39245c5af 100644
--- a/pennylane/ops/op_math/sprod.py
+++ b/pennylane/ops/op_math/sprod.py
@@ -244,8 +244,8 @@ def sparse_matrix(self, wire_order=None):
Returns:
:class:`scipy.sparse._csr.csr_matrix`: sparse matrix representation
"""
- if self._pauli_rep: # Get the sparse matrix from the PauliSentence representation
- return self._pauli_rep.to_mat(wire_order=wire_order or self.wires, format="csr")
+ if self.pauli_rep: # Get the sparse matrix from the PauliSentence representation
+ return self.pauli_rep.to_mat(wire_order=wire_order or self.wires, format="csr")
mat = self.base.sparse_matrix(wire_order=wire_order).multiply(self.scalar)
mat.eliminate_zeros()
return mat
@@ -293,7 +293,7 @@ def simplify(self) -> Operator:
.Operator: simplified operator
"""
# try using pauli_rep:
- if pr := self._pauli_rep:
+ if pr := self.pauli_rep:
pr.simplify()
return pr.operation(wire_order=self.wires)
diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py
index b0c52f7ba4e..7816d08e875 100644
--- a/pennylane/ops/op_math/sum.py
+++ b/pennylane/ops/op_math/sum.py
@@ -168,8 +168,8 @@ def hash(self):
@property
def is_hermitian(self):
"""If all of the terms in the sum are hermitian, then the Sum is hermitian."""
- if self._pauli_rep is not None:
- coeffs_list = list(self._pauli_rep.values())
+ if self.pauli_rep is not None:
+ coeffs_list = list(self.pauli_rep.values())
if not math.is_abstract(coeffs_list[0]):
return not any(math.iscomplex(c) for c in coeffs_list)
@@ -221,8 +221,8 @@ def matrix(self, wire_order=None):
return math.expand_matrix(reduced_mat, sum_wires, wire_order=wire_order)
def sparse_matrix(self, wire_order=None):
- if self._pauli_rep: # Get the sparse matrix from the PauliSentence representation
- return self._pauli_rep.to_mat(wire_order=wire_order or self.wires, format="csr")
+ if self.pauli_rep: # Get the sparse matrix from the PauliSentence representation
+ return self.pauli_rep.to_mat(wire_order=wire_order or self.wires, format="csr")
gen = ((op.sparse_matrix(), op.wires) for op in self)
@@ -252,11 +252,7 @@ def adjoint(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
- ]
- ):
+ if all(operand_pauli_reps := [op.pauli_rep for op in self.operands]):
new_rep = qml.pauli.PauliSentence()
for operand_rep in operand_pauli_reps:
for pw, coeff in operand_rep.items():
@@ -295,7 +291,7 @@ def _simplify_summands(cls, summands: List[Operator]):
def simplify(self, cutoff=1.0e-12) -> "Sum": # pylint: disable=arguments-differ
# try using pauli_rep:
- if pr := self._pauli_rep:
+ if pr := self.pauli_rep:
pr.simplify()
return pr.operation(wire_order=self.wires)
diff --git a/pennylane/ops/op_math/symbolicop.py b/pennylane/ops/op_math/symbolicop.py
index 916bbc8a422..34a137af2e0 100644
--- a/pennylane/ops/op_math/symbolicop.py
+++ b/pennylane/ops/op_math/symbolicop.py
@@ -139,11 +139,10 @@ def hash(self):
)
def map_wires(self, wire_map: dict):
- # pylint:disable=protected-access
new_op = copy(self)
new_op.hyperparameters["base"] = self.base.map_wires(wire_map=wire_map)
- if (p_rep := new_op._pauli_rep) is not None:
- new_op._pauli_rep = p_rep.map_wires(wire_map)
+ if (p_rep := new_op.pauli_rep) is not None:
+ new_op._pauli_rep = p_rep.map_wires(wire_map) # pylint:disable=protected-access
return new_op
diff --git a/pennylane/pauli/conversion.py b/pennylane/pauli/conversion.py
index 56f5db46313..9944deb07a3 100644
--- a/pennylane/pauli/conversion.py
+++ b/pennylane/pauli/conversion.py
@@ -340,7 +340,7 @@ def pauli_sentence(op):
Returns:
.PauliSentence: the PauliSentence representation of an arithmetic operator or Hamiltonian
"""
- if (ps := op._pauli_rep) is not None: # pylint: disable=protected-access
+ if (ps := op.pauli_rep) is not None:
return ps
return _pauli_sentence(op)
@@ -348,7 +348,7 @@ def pauli_sentence(op):
def is_pauli_sentence(op):
"""Returns True of the operator is a PauliSentence and False otherwise."""
- if op._pauli_rep is not None: # pylint: disable=protected-access
+ if op.pauli_rep is not None:
return True
if isinstance(op, Hamiltonian):
return all(is_pauli_word(o) for o in op.ops)
diff --git a/pennylane/pauli/pauli_interface.py b/pennylane/pauli/pauli_interface.py
index 19574753f6a..4a5f37fbd93 100644
--- a/pennylane/pauli/pauli_interface.py
+++ b/pennylane/pauli/pauli_interface.py
@@ -82,7 +82,7 @@ def _pw_prefactor_ham(observable: Hamiltonian):
@_pauli_word_prefactor.register(Prod)
@_pauli_word_prefactor.register(SProd)
def _pw_prefactor_prod_sprod(observable: Union[Prod, SProd]):
- ps = observable._pauli_rep # pylint:disable=protected-access
+ ps = observable.pauli_rep
if ps is not None and len(ps) == 1:
return list(ps.values())[0]
diff --git a/pennylane/qchem/convert.py b/pennylane/qchem/convert.py
index 2648aaebc7d..e1a8d00aaf3 100644
--- a/pennylane/qchem/convert.py
+++ b/pennylane/qchem/convert.py
@@ -17,7 +17,7 @@
import warnings
from itertools import product
-# pylint: disable= import-outside-toplevel
+# pylint: disable= import-outside-toplevel,no-member
import pennylane as qml
from pennylane import numpy as np
from pennylane.operation import Tensor, active_new_opmath
@@ -259,7 +259,7 @@ def _pennylane_to_openfermion(coeffs, ops, wires=None):
f"but got {op}."
) from e
- elif (ps := op._pauli_rep) is None: # pylint: disable=protected-access
+ elif (ps := op.pauli_rep) is None:
raise ValueError(
f"Expected a Pennylane operator with a valid Pauli word representation, but got {op}."
)
diff --git a/tests/devices/default_qubit/test_default_qubit.py b/tests/devices/default_qubit/test_default_qubit.py
index 2722b5b9e9f..6d4f2cfe90e 100644
--- a/tests/devices/default_qubit/test_default_qubit.py
+++ b/tests/devices/default_qubit/test_default_qubit.py
@@ -798,7 +798,7 @@ def f(dev, scale, n_wires=10, offset=0.1, style="sum"):
t2 = 6.2 * qml.prod(*(qml.PauliY(i) for i in range(n_wires)))
H = t1 + t2
if style == "hamiltonian":
- H = H._pauli_rep.hamiltonian() # pylint: disable=protected-access
+ H = H.pauli_rep.hamiltonian()
elif style == "hermitian":
H = qml.Hermitian(H.matrix(), wires=H.wires)
qs = qml.tape.QuantumScript(ops, [qml.expval(H)])
@@ -813,7 +813,7 @@ def f_hashable(scale, n_wires=10, offset=0.1, style="sum"):
t2 = 6.2 * qml.prod(*(qml.PauliY(i) for i in range(n_wires)))
H = t1 + t2
if style == "hamiltonian":
- H = H._pauli_rep.hamiltonian() # pylint: disable=protected-access
+ H = H.pauli_rep.hamiltonian()
elif style == "hermitian":
H = qml.Hermitian(H.matrix(), wires=H.wires)
qs = qml.tape.QuantumScript(ops, [qml.expval(H)])
diff --git a/tests/ops/op_math/test_composite.py b/tests/ops/op_math/test_composite.py
index f974ef3dc0a..7ffd93e5d9d 100644
--- a/tests/ops/op_math/test_composite.py
+++ b/tests/ops/op_math/test_composite.py
@@ -200,8 +200,8 @@ def test_map_wires(self):
assert mapped_op.wires == Wires([5, 7])
assert mapped_op[0].wires == Wires(5)
assert mapped_op[1].wires == Wires(7)
- assert mapped_op._pauli_rep is not diag_op._pauli_rep
- assert mapped_op._pauli_rep == qml.pauli.PauliSentence(
+ assert mapped_op.pauli_rep is not diag_op.pauli_rep
+ assert mapped_op.pauli_rep == qml.pauli.PauliSentence(
{qml.pauli.PauliWord({5: "X", 7: "Y"}): 1}
)
diff --git a/tests/ops/op_math/test_pow_op.py b/tests/ops/op_math/test_pow_op.py
index 828f453b743..415b283b80f 100644
--- a/tests/ops/op_math/test_pow_op.py
+++ b/tests/ops/op_math/test_pow_op.py
@@ -390,7 +390,7 @@ def test_different_batch_sizes_raises_error(self, power_method):
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 # pylint: disable=protected-access
+ 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."""
@@ -399,13 +399,13 @@ def test_pauli_rep_is_none_for_bad_exponents(self, power_method):
for exponent in exponents:
op = power_method(base, z=exponent)
- assert op._pauli_rep is None # pylint: disable=protected-access
+ 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 # pylint: disable=protected-access
+ assert op.pauli_rep is None
class TestSimplify:
diff --git a/tests/ops/op_math/test_prod.py b/tests/ops/op_math/test_prod.py
index 074ec74fc8b..31bf1242c7b 100644
--- a/tests/ops/op_math/test_prod.py
+++ b/tests/ops/op_math/test_prod.py
@@ -894,12 +894,12 @@ def test_diagonalizing_gates(self):
@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
+ 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
+ assert op.pauli_rep is None
op_pauli_reps_nested = (
(
@@ -926,7 +926,7 @@ def test_pauli_rep_none(self):
@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
+ assert op.pauli_rep == rep
class TestSimplify:
diff --git a/tests/ops/op_math/test_sprod.py b/tests/ops/op_math/test_sprod.py
index bc219a491c6..8ea66dfc6d1 100644
--- a/tests/ops/op_math/test_sprod.py
+++ b/tests/ops/op_math/test_sprod.py
@@ -742,13 +742,13 @@ def test_label_cache(self):
@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 # pylint: disable=protected-access
+ 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 # pylint: disable=protected-access
+ assert op.pauli_rep is None
def test_batching_properties(self):
"""Test the batching properties and methods."""
diff --git a/tests/ops/op_math/test_sum.py b/tests/ops/op_math/test_sum.py
index e869f49dc44..850c72df0bb 100644
--- a/tests/ops/op_math/test_sum.py
+++ b/tests/ops/op_math/test_sum.py
@@ -546,12 +546,12 @@ def test_eigendecompostion(self):
@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 # pylint: disable=protected-access
+ 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.sum(qml.PauliX(wires=0), qml.RX(1.23, wires=1))
- assert op._pauli_rep is None # pylint: disable=protected-access
+ assert op.pauli_rep is None
op_pauli_reps_nested = (
(
@@ -646,7 +646,7 @@ def test_pauli_rep_none(self):
@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 # pylint: disable=protected-access
+ assert op.pauli_rep == rep
class TestSimplify:
diff --git a/tests/ops/op_math/test_symbolic_op.py b/tests/ops/op_math/test_symbolic_op.py
index dc86a30a21f..3608c159617 100644
--- a/tests/ops/op_math/test_symbolic_op.py
+++ b/tests/ops/op_math/test_symbolic_op.py
@@ -84,8 +84,8 @@ def test_map_wires():
assert op.base.wires == Wires("a")
assert mapped_op.wires == Wires(5)
assert mapped_op.base.wires == Wires(5)
- assert mapped_op._pauli_rep is not op._pauli_rep
- assert mapped_op._pauli_rep == qml.pauli.PauliSentence({qml.pauli.PauliWord({5: "X"}): 1})
+ assert mapped_op.pauli_rep is not op.pauli_rep
+ assert mapped_op.pauli_rep == qml.pauli.PauliSentence({qml.pauli.PauliWord({5: "X"}): 1})
class TestProperties:
@@ -189,7 +189,7 @@ def test_pauli_rep(self):
"""Test that pauli_rep is None by default"""
base = Operator("a")
op = SymbolicOp(base)
- assert op._pauli_rep is None # pylint:disable=protected-access
+ assert op.pauli_rep is None
class TestQueuing:
diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py
index 55786fcae76..775de28f4dc 100644
--- a/tests/ops/qubit/test_non_parametric_ops.py
+++ b/tests/ops/qubit/test_non_parametric_ops.py
@@ -1240,4 +1240,4 @@ def test_adjoint_method(op):
@pytest.mark.parametrize("op, rep", op_pauli_rep)
def test_pauli_rep(op, rep):
# pylint: disable=protected-access
- assert op._pauli_rep == rep
+ assert op.pauli_rep == rep
diff --git a/tests/pauli/test_conversion.py b/tests/pauli/test_conversion.py
index ca6bf3ee855..435ff74b7c1 100644
--- a/tests/pauli/test_conversion.py
+++ b/tests/pauli/test_conversion.py
@@ -510,7 +510,7 @@ def test_operator(self, op, ps):
@pytest.mark.parametrize("op, ps", operator_ps)
def test_operator_private_ps(self, op, ps):
"""Test that a correct pauli sentence is computed when passing an arithmetic operator and not
- relying on the saved op._pauli_rep attribute."""
+ relying on the saved op.pauli_rep attribute."""
assert qml.pauli.conversion._pauli_sentence(op) == ps # pylint: disable=protected-access
error_ps = (
diff --git a/tests/test_operation.py b/tests/test_operation.py
index ea92de0e849..261538543db 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -204,7 +204,7 @@ class DummyOp(qml.operation.Operator):
num_wires = 1
op = DummyOp(wires=0)
- assert op._pauli_rep is None
+ assert op.pauli_rep is None
def test_list_or_tuple_params_casted_into_numpy_array(self):
"""Test that list parameters are casted into numpy arrays."""
@@ -691,8 +691,8 @@ class DummyOp(qml.operation.Operator):
assert op is not mapped_op
assert op.wires == Wires([0, 1, 2])
assert mapped_op.wires == Wires([10, 11, 12])
- assert mapped_op._pauli_rep is not op._pauli_rep
- assert mapped_op._pauli_rep == qml.pauli.PauliSentence(
+ assert mapped_op.pauli_rep is not op.pauli_rep
+ assert mapped_op.pauli_rep == qml.pauli.PauliSentence(
{
qml.pauli.PauliWord({10: "X", 11: "Y", 12: "Z"}): 1.1,
qml.pauli.PauliWord({10: "Z", 11: "X", 12: "Y"}): 2.2,
@@ -1452,7 +1452,7 @@ def test_pauli_rep(self):
X = qml.PauliX(0)
Y = qml.PauliY(2)
t = Tensor(X, Y)
- assert t._pauli_rep is None
+ assert t.pauli_rep is None
def test_has_matrix(self):
"""Test that the Tensor class has a ``has_matrix`` static attribute set to True."""