Skip to content

Commit

Permalink
make the pauli_rep operator property public (#4915)
Browse files Browse the repository at this point in the history
**Context:**
The `pauli_rep` property is widely used, and should be made public

**Description of the Change:**
- Introduce the `pauli_rep` read-only property to the `Operator` class
- Change all (read) references to `op._pauli_rep` into `op.pauli_rep`
(the public version)

**Benefits:**
We're committing to having `pauli_rep` be part of the public API, making
it less scary to lean on while moving forward with other improvements
(eg. combining all the hamiltonian expansion transforms)

**Possible Drawbacks:**
We might want to change things related to it again, and it would require
more careful work because it's public. I'm not so worried about this
because it has existed (and been used) for a while now.

**Related GitHub Issues:**
previously attempted in #4017 
[sc-37494]

---------

Co-authored-by: Christina Lee <christina@xanadu.ai>
  • Loading branch information
2 people authored and mudit2812 committed Jan 19, 2024
1 parent a51003c commit 96b089b
Show file tree
Hide file tree
Showing 22 changed files with 58 additions and 58 deletions.
4 changes: 4 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<h3>Breaking changes 💔</h3>

* The transforms submodule `qml.transforms.qcut` becomes its own module `qml.qcut`.
Expand Down
4 changes: 2 additions & 2 deletions pennylane/devices/qubit/adjoint_jacobian.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion pennylane/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pennylane/ops/op_math/composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pennylane/ops/op_math/pow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
12 changes: 4 additions & 8 deletions pennylane/ops/op_math/prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions pennylane/ops/op_math/sprod.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
16 changes: 6 additions & 10 deletions pennylane/ops/op_math/sum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 2 additions & 3 deletions pennylane/ops/op_math/symbolicop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
4 changes: 2 additions & 2 deletions pennylane/pauli/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,15 +340,15 @@ 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)


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)
Expand Down
2 changes: 1 addition & 1 deletion pennylane/pauli/pauli_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
4 changes: 2 additions & 2 deletions pennylane/qchem/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}."
)
Expand Down
4 changes: 2 additions & 2 deletions tests/devices/default_qubit/test_default_qubit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)])
Expand All @@ -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)])
Expand Down
4 changes: 2 additions & 2 deletions tests/ops/op_math/test_composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
)

Expand Down
6 changes: 3 additions & 3 deletions tests/ops/op_math/test_pow_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions tests/ops/op_math/test_prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
(
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions tests/ops/op_math/test_sprod.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
6 changes: 3 additions & 3 deletions tests/ops/op_math/test_sum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
(
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions tests/ops/op_math/test_symbolic_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/ops/qubit/test_non_parametric_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion tests/pauli/test_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
8 changes: 4 additions & 4 deletions tests/test_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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."""
Expand Down

0 comments on commit 96b089b

Please sign in to comment.