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."""