Skip to content

Commit

Permalink
[new opmath] Deprecation warnings and making `LinearCombination.compa…
Browse files Browse the repository at this point in the history
…re` more user friendly (#5504)

Removing `LinearCombination._obs_data`, which requires updating
`LinearCombination.compare`. While at it, also updating the doc string
to make it clear.

**Potential Drawbacks**

A comparison with an `Observable` that has no pauli_rep (i.e. involving
a `Hadamard`) will not work due to
https://github.com/PennyLaneAI/pennylane/blob/master/pennylane/ops/functions/equal.py#L160.
I consider this a separate issue for updating qml.equal accordingly.
Alternative solution is to give `Hadamard` a `pauli_rep`.

```
op1, op2 = (qml.ops.LinearCombination([1.0], [qml.Hadamard(0)]), qml.Hadamard(0))
op1, op2 = (qml.ops.LinearCombination([1.0], [qml.Hadamard(0) @ qml.Hadamard(1)]), qml.Hadamard(0) @ qml.Hadamard(1))
op1, op2 = (qml.ops.LinearCombination([1.0], [qml.Hadamard(0) @ X(1)]), qml.Hadamard(0) @ X(1))
```

[sc-59339]
  • Loading branch information
Qottmann authored Apr 24, 2024
1 parent 10d59e7 commit f504341
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 95 deletions.
3 changes: 3 additions & 0 deletions doc/development/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Pending deprecations
- Deprecated in v0.36
- Will be removed in v0.37

New operator arithmetic deprecations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* ``op.ops`` and ``op.coeffs`` will be deprecated in the future. Use ``op.terms()`` instead.

- Added and deprecated for ``Sum`` and ``Prod`` instances in v0.35
Expand Down
3 changes: 3 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,9 @@
* Since `default.mixed` does not support snapshots with measurements, attempting to do so will result in a `DeviceError` instead of getting the density matrix.
[(#5416)](https://github.com/PennyLaneAI/pennylane/pull/5416)

* `LinearCombination._obs_data` is removed. You can still use `LinearCombination.compare` to check mathematical equivalence between a `LinearCombination` and another operator.
[(#5504)](https://github.com/PennyLaneAI/pennylane/pull/5504)

<h3>Deprecations 👋</h3>

* `qml.load` is deprecated. Instead, please use the functions outlined in the *Importing workflows* quickstart guide, such as `qml.from_qiskit`.
Expand Down
73 changes: 18 additions & 55 deletions pennylane/ops/op_math/linear_combination.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
LinearCombination class
"""
# pylint: disable=too-many-arguments, protected-access, too-many-instance-attributes

import warnings
import itertools
import numbers
from copy import copy
Expand Down Expand Up @@ -324,58 +324,17 @@ def simplify(self, cutoff=1.0e-12):
coeffs, ops, pr = self._simplify_coeffs_ops(self.coeffs, self.ops, self.pauli_rep, cutoff)
return LinearCombination(coeffs, ops, _pauli_rep=pr)

def _obs_data(self):
r"""Extracts the data from a ``LinearCombination`` and serializes it in an order-independent fashion.
This allows for comparison between ``LinearCombination``s that are equivalent, but are defined with terms and tensors
expressed in different orders. For example, `qml.X(0) @ qml.Z(1)` and
`qml.Z(1) @ qml.X(0)` are equivalent observables with different orderings.
.. Note::
In order to store the data from each term of the ``LinearCombination`` in an order-independent serialization,
we make use of sets. Note that all data contained within each term must be immutable, hence the use of
strings and frozensets.
**Example**
>>> H = qml.ops.LinearCombination([1, 1], [qml.X(0) @ qml.X(1), qml.Z(0)])
>>> print(H._obs_data())
{(1, frozenset({('Prod', <Wires = [0, 1]>, ())})),
(1, frozenset({('PauliZ', <Wires = [0]>, ())}))}
"""
data = set()

coeffs_arr = qml.math.toarray(self.coeffs)
for co, op in zip(coeffs_arr, self.ops):
obs = op.non_identity_obs if isinstance(op, Tensor) else [op]
tensor = []
for ob in obs:
parameters = tuple(
str(param) for param in ob.parameters
) # Converts params into immutable type
if isinstance(ob, qml.GellMann):
parameters += (ob.hyperparameters["index"],)
tensor.append((ob.name, ob.wires, parameters))
data.add((co, frozenset(tensor)))

return data

def compare(self, other):
r"""Determines whether the operator is equivalent to another.
r"""Determines mathematical equivalence between operators
Currently only supported for :class:`~LinearCombination`, :class:`~.Observable`, or :class:`~.Tensor`.
LinearCombinations/observables are equivalent if they represent the same operator
(their matrix representations are equal), and they are defined on the same wires.
``LinearCombination`` and other operators are equivalent if they mathematically represent the same operator
(their matrix representations are equal), acting on the same wires.
.. Warning::
The compare method does **not** check if the matrix representation
of a :class:`~.Hermitian` observable is equal to an equivalent
observable expressed in terms of Pauli matrices, or as a
linear combination of Hermitians.
To do so would require the matrix form of LinearCombinations and Tensors
be calculated, which would drastically increase runtime.
This method does not compute explicit matrices but uses the underlyding operators and coefficients for comparisons. When both operators
consist purely of Pauli operators, and therefore have a valid ``op.pauli_rep``, the comparison is cheap.
When that is not the case (e.g. one of the operators contains a ``Hadamard`` gate), it can be more expensive as it involves mathematical simplification of both operators.
Returns:
(bool): True if equivalent.
Expand Down Expand Up @@ -407,16 +366,20 @@ def compare(self, other):
pr2.simplify()
return pr1 == pr2

if isinstance(other, (LinearCombination, qml.ops.Hamiltonian)):
if isinstance(other, (qml.ops.Hamiltonian, Tensor)):
warnings.warn(
f"Attempting to compare a legacy operator class instance {other} of type {type(other)} with {self} of type {type(self)}."
f"You are likely disabling/enabling new opmath in the same script or explicitly create legacy operator classes Tensor and ops.Hamiltonian."
f"Please visit https://docs.pennylane.ai/en/latest/introduction/new_opmath.html for more information and help troubleshooting.",
UserWarning,
)
op1 = self.simplify()
op2 = other.simplify()
return op1._obs_data() == op2._obs_data() # pylint: disable=protected-access

if isinstance(other, (Tensor, Observable)):
op1 = self.simplify()
return op1._obs_data() == {
(1, frozenset(other._obs_data())) # pylint: disable=protected-access
}
op2 = qml.operation.convert_to_opmath(op2)
op2 = qml.ops.LinearCombination(*op2.terms())

return qml.equal(op1, op2)

op1 = self.simplify()
op2 = other.simplify()
Expand Down
66 changes: 26 additions & 40 deletions tests/ops/op_math/test_linear_combination.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ def test_isinstance_Hamiltonian(self):
assert isinstance(H, qml.Hamiltonian)


@pytest.mark.filterwarnings(
"ignore:Using 'qml.ops.Hamiltonian' with new operator arithmetic is deprecated"
)
def test_mixed_legacy_warning_Hamiltonian():
"""Test that mixing legacy ops and LinearCombination.compare raises a warning"""
op1 = qml.ops.LinearCombination([0.5, 0.5], [X(0) @ X(1), qml.Hadamard(0)])
op2 = qml.ops.Hamiltonian([0.5, 0.5], [qml.operation.Tensor(X(0), X(1)), qml.Hadamard(0)])

with pytest.warns(UserWarning, match="Attempting to compare a legacy operator class instance"):
res = op1.compare(op2)

assert res


def test_mixed_legacy_warning_Tensor():
"""Test that mixing legacy ops and LinearCombination.compare raises a warning"""
op1 = qml.ops.LinearCombination([1.0], [X(0) @ qml.Hadamard(1)])
op2 = qml.operation.Tensor(X(0), qml.Hadamard(1))

with pytest.warns(UserWarning, match="Attempting to compare a legacy operator class instance"):
res = op1.compare(op2)

assert res


# Make test data in different interfaces, if installed
COEFFS_PARAM_INTERFACE = [
([-0.05, 0.17], 1.7, "autograd"),
Expand Down Expand Up @@ -704,50 +729,11 @@ def test_simplify_while_queueing(self):
# check that the simplified LinearCombination is in the queue
assert q.get_info(H) is not None

def test_data(self):
"""Tests the obs_data method"""
# pylint: disable=protected-access

H = qml.ops.LinearCombination(
[1, 1, 0.5],
[Z(0), Z(0) @ X(1), X(2) @ qml.Identity(1)],
)
data = H._obs_data()

expected = {
(0.5, frozenset({("Prod", qml.wires.Wires([2, 1]), ())})),
(1.0, frozenset({("PauliZ", qml.wires.Wires(0), ())})),
(1.0, frozenset({("Prod", qml.wires.Wires([0, 1]), ())})),
}

assert data == expected

def test_data_gell_mann(self):
"""Tests that the obs_data method for LinearCombinations with qml.GellMann
observables includes the Gell-Mann index."""
H = qml.ops.LinearCombination(
[1, -1, 0.5],
[
qml.GellMann(wires=0, index=3),
qml.GellMann(wires=0, index=3) @ qml.GellMann(wires=1, index=1),
qml.GellMann(wires=2, index=2),
],
)
data = H._obs_data()

expected = {
(-1.0, frozenset({("Prod", qml.wires.Wires([0, 1]), ())})),
(0.5, frozenset({("GellMann", qml.wires.Wires(2), (2,))})),
(1.0, frozenset({("GellMann", qml.wires.Wires(0), (3,))})),
}

assert data == expected

COMPARE_WITH_OPS = (
(qml.ops.LinearCombination([0.5], [X(0) @ X(1)]), qml.s_prod(0.5, X(0) @ X(1))),
(qml.ops.LinearCombination([0.5], [X(0) + X(1)]), qml.s_prod(0.5, qml.sum(X(0), X(1)))),
(qml.ops.LinearCombination([1.0], [X(0)]), X(0)),
(qml.ops.LinearCombination([1.0], [qml.Hadamard(0)]), qml.Hadamard(0)),
# (qml.ops.LinearCombination([1.0], [qml.Hadamard(0)]), qml.Hadamard(0)), # TODO fix qml.equal check for Observables having to be the same type
(qml.ops.LinearCombination([1.0], [X(0) @ X(1)]), X(0) @ X(1)),
)

Expand Down

0 comments on commit f504341

Please sign in to comment.