Source code for pennylane.ops.op_math.linear_combination
# Copyright 2024 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
LinearCombination class
"""
import itertools
import numbers
# pylint: disable=too-many-arguments, protected-access, too-many-instance-attributes
import warnings
from copy import copy
from typing import Union
import pennylane as qml
from pennylane.operation import Observable, Operator, Tensor, convert_to_opmath
from .sum import Sum
[docs]class LinearCombination(Sum):
r"""Operator representing a linear combination of operators.
The ``LinearCombination`` is represented as a linear combination of other operators, e.g.,
:math:`\sum_{k=0}^{N-1} c_k O_k`, where the :math:`c_k` are trainable parameters.
Args:
coeffs (tensor_like): coefficients of the ``LinearCombination`` expression
observables (Iterable[Observable]): observables in the ``LinearCombination`` expression, of same length as ``coeffs``
grouping_type (str): If not ``None``, compute and store information on how to group commuting
observables upon initialization. This information may be accessed when a :class:`~.QNode` containing this
``LinearCombination`` is executed on devices. The string refers to the type of binary relation between Pauli words.
Can be ``'qwc'`` (qubit-wise commuting), ``'commuting'``, or ``'anticommuting'``.
method (str): The graph colouring heuristic to use in solving minimum clique cover for grouping, which
can be ``'lf'`` (Largest First), ``'rlf'`` (Recursive Largest First), ``'dsatur'`` (Degree of Saturation), or
``'gis'`` (IndependentSet). Defaults to ``'lf'``. Ignored if ``grouping_type=None``.
id (str): name to be assigned to this ``LinearCombination`` instance
.. seealso:: `rustworkx.ColoringStrategy <https://www.rustworkx.org/apiref/rustworkx.ColoringStrategy.html#coloringstrategy>`_
for more information on the ``('lf', 'dsatur', 'gis')`` strategies.
**Example:**
A ``LinearCombination`` can be created by simply passing the list of coefficients
as well as the list of observables:
>>> coeffs = [0.2, -0.543]
>>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)]
>>> H = qml.ops.LinearCombination(coeffs, obs)
>>> print(H)
0.2 * (X(0) @ Z(1)) + -0.543 * (Z(0) @ Hadamard(wires=[2]))
The coefficients can be a trainable tensor, for example:
>>> coeffs = tf.Variable([0.2, -0.543], dtype=tf.double)
>>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)]
>>> H = qml.ops.LinearCombination(coeffs, obs)
>>> print(H)
0.2 * (X(0) @ Z(1)) + -0.543 * (Z(0) @ Hadamard(wires=[2]))
A ``LinearCombination`` can store information on which commuting observables should be measured together in
a circuit:
>>> obs = [qml.X(0), qml.X(1), qml.Z(0)]
>>> coeffs = np.array([1., 2., 3.])
>>> H = qml.ops.LinearCombination(coeffs, obs, grouping_type='qwc')
>>> H.grouping_indices
((0, 1), (2,))
This attribute can be used to compute groups of coefficients and observables:
>>> grouped_coeffs = [coeffs[list(indices)] for indices in H.grouping_indices]
>>> grouped_obs = [[H.ops[i] for i in indices] for indices in H.grouping_indices]
>>> grouped_coeffs
[array([1., 2.]), array([3.])]
>>> grouped_obs
[[X(0), X(1)], [Z(0)]]
Devices that evaluate a ``LinearCombination`` expectation by splitting it into its local observables can
use this information to reduce the number of circuits evaluated.
Note that one can compute the ``grouping_indices`` for an already initialized ``LinearCombination`` by
using the :func:`compute_grouping <pennylane.ops.LinearCombination.compute_grouping>` method.
"""
num_wires = qml.operation.AnyWires
grad_method = "A" # supports analytic gradients
batch_size = None
ndim_params = None # could be (0,) * len(coeffs), but it is not needed. Define at class-level
def _flatten(self):
# note that we are unable to restore grouping type or method without creating new properties
return self.terms(), (self.grouping_indices,)
@classmethod
def _unflatten(cls, data, metadata):
return cls(data[0], data[1], _grouping_indices=metadata[0])
# pylint: disable=arguments-differ
@classmethod
def _primitive_bind_call(cls, coeffs, observables, _pauli_rep=None, **kwargs):
return cls._primitive.bind(*coeffs, *observables, **kwargs, n_obs=len(observables))
def __init__(
self,
coeffs,
observables: list[Operator],
grouping_type=None,
method="lf",
_grouping_indices=None,
_pauli_rep=None,
id=None,
):
if isinstance(observables, Operator):
raise ValueError(
"observables must be an Iterable of Operator's, and not an Operator itself."
)
if qml.math.shape(coeffs)[0] != len(observables):
raise ValueError(
"Could not create valid LinearCombination; "
"number of coefficients and operators does not match."
)
if _pauli_rep is None:
_pauli_rep = self._build_pauli_rep_static(coeffs, observables)
self._coeffs = coeffs
self._ops = [convert_to_opmath(op) for op in observables]
self._hyperparameters = {"ops": self._ops}
with qml.QueuingManager.stop_recording():
operands = tuple(qml.s_prod(c, op) for c, op in zip(coeffs, observables))
super().__init__(
*operands,
grouping_type=grouping_type,
method=method,
id=id,
_grouping_indices=_grouping_indices,
_pauli_rep=_pauli_rep,
)
@staticmethod
def _build_pauli_rep_static(coeffs, observables):
"""PauliSentence representation of the Sum of operations."""
if all(pauli_reps := [op.pauli_rep for op in observables]):
new_rep = qml.pauli.PauliSentence()
for c, ps in zip(coeffs, pauli_reps):
for pw, coeff in ps.items():
new_rep[pw] += coeff * c
return new_rep
return None
def _check_batching(self):
"""Override for LinearCombination, batching is not yet supported."""
@property
def coeffs(self):
"""Return the coefficients defining the LinearCombination.
Returns:
Iterable[float]): coefficients in the LinearCombination expression
"""
return self._coeffs
@property
def ops(self):
"""Return the operators defining the LinearCombination.
Returns:
Iterable[Observable]): observables in the LinearCombination expression
"""
return self._ops
[docs] def terms(self):
r"""Retrieve the coefficients and operators of the ``LinearCombination``.
Returns:
tuple[list[tensor_like or float], list[.Operation]]: list of coefficients :math:`c_i`
and list of operations :math:`O_i`
**Example**
>>> coeffs = [1., 2., 3.]
>>> ops = [X(0), X(0) @ X(1), X(1) @ X(2)]
>>> op = qml.ops.LinearCombination(coeffs, ops)
>>> op.terms()
([1.0, 2.0, 3.0], [X(0), X(0) @ X(1), X(1) @ X(2)])
"""
return self.coeffs, self.ops
[docs] def compute_grouping(self, grouping_type="qwc", method="lf"):
"""
Compute groups of operators and coefficients corresponding to commuting
observables of this ``LinearCombination``.
.. note::
If grouping is requested, the computed groupings are stored as a list of list of indices
in ``LinearCombination.grouping_indices``.
Args:
grouping_type (str): The type of binary relation between Pauli words used to compute
the grouping. Can be ``'qwc'``, ``'commuting'``, or ``'anticommuting'``.
Defaults to ``'qwc'``.
method (str): The graph colouring heuristic to use in solving minimum clique cover for
grouping, which can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest
First). Defaults to ``'lf'``.
**Example**
.. code-block:: python
import pennylane as qml
a = qml.X(0)
b = qml.prod(qml.X(0), qml.X(1))
c = qml.Z(0)
obs = [a, b, c]
coeffs = [1.0, 2.0, 3.0]
op = qml.ops.LinearCombination(coeffs, obs)
>>> op.grouping_indices is None
True
>>> op.compute_grouping(grouping_type="qwc")
>>> op.grouping_indices
((2,), (0, 1))
"""
if not self.pauli_rep:
raise ValueError("Cannot compute grouping for Sums containing non-Pauli operators.")
_, ops = self.terms()
self._grouping_indices = qml.pauli.compute_partition_indices(
ops, grouping_type=grouping_type, method=method
)
@property
def wires(self):
r"""The sorted union of wires from all operators.
Returns:
(Wires): Combined wires present in all terms, sorted.
"""
return self._wires
@property
def name(self):
return "LinearCombination"
@staticmethod
@qml.QueuingManager.stop_recording()
def _simplify_coeffs_ops(coeffs, ops, pr, cutoff=1.0e-12):
"""Simplify coeffs and ops
Returns:
coeffs, ops, pauli_rep"""
if len(ops) == 0:
return [], [], pr
# try using pauli_rep:
if pr is not None:
if len(pr) == 0:
return [], [], pr
# collect coefficients and ops
new_coeffs = []
new_ops = []
for pw, coeff in pr.items():
pw_op = pw.operation(wire_order=pr.wires)
new_ops.append(pw_op)
new_coeffs.append(coeff)
return new_coeffs, new_ops, pr
if len(ops) == 1:
return coeffs, [ops[0].simplify()], pr
op_as_sum = qml.dot(coeffs, ops)
op_as_sum = op_as_sum.simplify(cutoff)
new_coeffs, new_ops = op_as_sum.terms()
return new_coeffs, new_ops, pr
[docs] 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)
[docs] def compare(self, other):
r"""Determines mathematical equivalence between operators
``LinearCombination`` and other operators are equivalent if they mathematically represent the same operator
(their matrix representations are equal), acting on the same wires.
.. Warning::
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.
**Examples**
>>> H = qml.ops.LinearCombination(
... [0.5, 0.5],
... [qml.PauliZ(0) @ qml.PauliY(1), qml.PauliY(1) @ qml.PauliZ(0) @ qml.Identity("a")]
... )
>>> obs = qml.PauliZ(0) @ qml.PauliY(1)
>>> print(H.compare(obs))
True
>>> H1 = qml.ops.LinearCombination([1, 1], [qml.PauliX(0), qml.PauliZ(1)])
>>> H2 = qml.ops.LinearCombination([1, 1], [qml.PauliZ(0), qml.PauliX(1)])
>>> H1.compare(H2)
False
>>> ob1 = qml.ops.LinearCombination([1], [qml.PauliX(0)])
>>> ob2 = qml.Hermitian(np.array([[0, 1], [1, 0]]), 0)
>>> ob1.compare(ob2)
False
"""
if isinstance(other, (Operator)):
if (pr1 := self.pauli_rep) is not None and (pr2 := other.pauli_rep) is not None:
pr1.simplify()
pr2.simplify()
return pr1 == pr2
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/stable/news/new_opmath.html for more information and help troubleshooting.",
UserWarning,
)
op1 = self.simplify()
op2 = other.simplify()
op2 = qml.operation.convert_to_opmath(op2)
op2 = qml.ops.LinearCombination(*op2.terms())
return qml.equal(op1, op2)
op1 = self.simplify()
op2 = other.simplify()
return qml.equal(op1, op2)
raise ValueError(
"Can only compare a LinearCombination, and a LinearCombination/Observable/Tensor."
)
def __matmul__(self, other: Operator) -> Operator:
"""The product operation between Operator objects."""
if isinstance(other, LinearCombination):
coeffs1 = self.coeffs
ops1 = self.ops
shared_wires = qml.wires.Wires.shared_wires([self.wires, other.wires])
if len(shared_wires) > 0:
raise ValueError(
"LinearCombinations can only be multiplied together if they act on "
"different sets of wires"
)
coeffs2 = other.coeffs
ops2 = other.ops
coeffs = qml.math.kron(coeffs1, coeffs2)
ops_list = itertools.product(ops1, ops2)
terms = [qml.prod(t[0], t[1], lazy=False) for t in ops_list]
return qml.ops.LinearCombination(coeffs, terms)
if isinstance(other, Operator):
if other.arithmetic_depth == 0:
new_ops = [op @ other for op in self.ops]
# build new pauli rep using old pauli rep
if (pr1 := self.pauli_rep) is not None and (pr2 := other.pauli_rep) is not None:
new_pr = pr1 @ pr2
else:
new_pr = None
return LinearCombination(self.coeffs, new_ops, _pauli_rep=new_pr)
return qml.prod(self, other)
return NotImplemented
def __add__(self, H: Union[numbers.Number, Operator]) -> Operator:
r"""The addition operation between a LinearCombination and a LinearCombination/Tensor/Observable."""
ops = copy(self.ops)
self_coeffs = self.coeffs
if isinstance(H, numbers.Number) and H == 0:
return self
if isinstance(H, (LinearCombination, qml.ops.Hamiltonian)):
coeffs = qml.math.concatenate([self_coeffs, H.coeffs], axis=0)
ops.extend(H.ops)
if (pr1 := self.pauli_rep) is not None and (pr2 := H.pauli_rep) is not None:
_pauli_rep = pr1 + pr2
else:
_pauli_rep = None
return qml.ops.LinearCombination(coeffs, ops, _pauli_rep=_pauli_rep)
if isinstance(H, Operator):
coeffs = qml.math.concatenate(
[self_coeffs, qml.math.cast_like([1.0], self_coeffs)], axis=0
)
ops.append(H)
return qml.ops.LinearCombination(coeffs, ops)
return NotImplemented
__radd__ = __add__
def __mul__(self, a: Union[int, float, complex]) -> "LinearCombination":
r"""The scalar multiplication operation between a scalar and a LinearCombination."""
if isinstance(a, (int, float, complex)):
self_coeffs = self.coeffs
coeffs = qml.math.multiply(a, self_coeffs)
return qml.ops.LinearCombination(coeffs, self.ops)
return NotImplemented
__rmul__ = __mul__
def __sub__(self, H: Observable) -> Observable:
r"""The subtraction operation between a LinearCombination and a LinearCombination/Tensor/Observable."""
if isinstance(H, (LinearCombination, qml.ops.Hamiltonian, Tensor, Observable)):
return self + qml.s_prod(-1.0, H, lazy=False)
return NotImplemented
[docs] def queue(
self, context: Union[qml.QueuingManager, qml.queuing.AnnotatedQueue] = qml.QueuingManager
):
"""Queues a ``qml.ops.LinearCombination`` instance"""
if qml.QueuingManager.recording():
for o in self.ops:
context.remove(o)
context.append(self)
return self
[docs] def eigvals(self):
"""Return the eigenvalues of the specified operator.
This method uses pre-stored eigenvalues for standard observables where
possible and stores the corresponding eigenvectors from the eigendecomposition.
Returns:
array: array containing the eigenvalues of the operator
"""
eigvals = []
for ops in self.overlapping_ops:
if len(ops) == 1:
eigvals.append(
qml.utils.expand_vector(ops[0].eigvals(), list(ops[0].wires), list(self.wires))
)
else:
tmp_composite = Sum(*ops) # only change compared to CompositeOp.eigvals()
eigvals.append(
qml.utils.expand_vector(
tmp_composite.eigendecomposition["eigval"],
list(tmp_composite.wires),
list(self.wires),
)
)
return self._math_op(
qml.math.asarray(eigvals, like=qml.math.get_deep_interface(eigvals)), axis=0
)
[docs] def diagonalizing_gates(self):
r"""Sequence of gates that diagonalize the operator in the computational basis.
Given the eigendecomposition :math:`O = U \Sigma U^{\dagger}` where
:math:`\Sigma` is a diagonal matrix containing the eigenvalues,
the sequence of diagonalizing gates implements the unitary :math:`U^{\dagger}`.
The diagonalizing gates rotate the state into the eigenbasis
of the operator.
A ``DiagGatesUndefinedError`` is raised if no representation by decomposition is defined.
.. seealso:: :meth:`~.Operator.compute_diagonalizing_gates`.
Returns:
list[.Operator] or None: a list of operators
"""
diag_gates = []
for ops in self.overlapping_ops:
if len(ops) == 1:
diag_gates.extend(ops[0].diagonalizing_gates())
else:
tmp_sum = Sum(*ops) # only change compared to CompositeOp.diagonalizing_gates()
eigvecs = tmp_sum.eigendecomposition["eigvec"]
diag_gates.append(
qml.QubitUnitary(
qml.math.transpose(qml.math.conj(eigvecs)), wires=tmp_sum.wires
)
)
return diag_gates
[docs] def map_wires(self, wire_map: dict):
"""Returns a copy of the current ``LinearCombination`` with its wires changed according to the given
wire map.
Args:
wire_map (dict): dictionary containing the old wires as keys and the new wires as values
Returns:
.LinearCombination: new ``LinearCombination``
"""
coeffs, ops = self.terms()
new_ops = tuple(op.map_wires(wire_map) for op in ops)
new_op = LinearCombination(coeffs, new_ops)
new_op.grouping_indices = self._grouping_indices
return new_op
if LinearCombination._primitive is not None:
@LinearCombination._primitive.def_impl
def _(*args, n_obs, **kwargs):
coeffs = args[:n_obs]
observables = args[n_obs:]
return type.__call__(LinearCombination, coeffs, observables, **kwargs)
_modules/pennylane/ops/op_math/linear_combination
Download Python script
Download Notebook
View on GitHub