Source code for pennylane.ops.op_math.sum
# Copyright 2018-2022 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.
"""
This file contains the implementation of the Sum class which contains logic for
computing the sum of operations.
"""
import itertools
import warnings
from copy import copy
from functools import reduce
from typing import List
import numpy as np
import pennylane as qml
from pennylane import math
from pennylane.operation import Operator
from pennylane.ops.qubit import Hamiltonian
from pennylane.queuing import QueuingManager
from .composite import CompositeOp
[docs]def op_sum(*summands, do_queue=True, id=None, lazy=True):
"""This function is deprecated and will be removed soon. Please use :func:`sum` instead."""
warnings.warn(
"The `op_sum` function is deprecated and will be removed soon. Please use `sum` instead.",
UserWarning,
)
return sum(*summands, do_queue=do_queue, id=id, lazy=lazy)
[docs]def sum(*summands, do_queue=True, id=None, lazy=True):
r"""Construct an operator which is the sum of the given operators.
Args:
summands (tuple[~.operation.Operator]): the operators we want to sum together.
Keyword Args:
do_queue (bool): determines if the sum operator will be queued (currently not supported).
Default is True.
id (str or None): id for the Sum operator. Default is None.
lazy=True (bool): If ``lazy=False``, a simplification will be performed such that when any
of the operators is already a sum operator, its operands (summands) will be used instead.
Returns:
~ops.op_math.Sum: The operator representing the sum of summands.
.. note::
This operator supports batched operands:
>>> op = qml.sum(qml.RX(np.array([1, 2, 3]), wires=0), qml.PauliX(1))
>>> op.matrix().shape
(3, 4, 4)
But it doesn't support batching of operators:
>>> op = qml.sum(np.array([qml.RX(0.4, 0), qml.RZ(0.3, 0)]), qml.PauliZ(0))
AttributeError: 'numpy.ndarray' object has no attribute 'wires'
.. seealso:: :class:`~.ops.op_math.Sum`
**Example**
>>> summed_op = qml.sum(qml.PauliX(0), qml.PauliZ(0))
>>> summed_op
PauliX(wires=[0]) + PauliZ(wires=[0])
>>> summed_op.matrix()
array([[ 1, 1],
[ 1, -1]])
"""
if len(summands) == 1 and isinstance(summands[0], qml.QNodeCollection):
return qml.collections.sum(summands[0])
if lazy:
return Sum(*summands, do_queue=do_queue, id=id)
summands_simp = Sum(
*itertools.chain.from_iterable([op if isinstance(op, Sum) else [op] for op in summands]),
do_queue=do_queue,
id=id,
)
if do_queue:
for op in summands:
QueuingManager.remove(op)
return summands_simp
[docs]class Sum(CompositeOp):
r"""Symbolic operator representing the sum of operators.
Args:
summands (tuple[~.operation.Operator]): a tuple of operators which will be summed together.
Keyword Args:
do_queue (bool): determines if the sum operator will be queued. Default is True.
id (str or None): id for the sum operator. Default is None.
.. note::
Currently this operator can not be queued in a circuit as an operation, only measured terminally.
.. note::
This operator supports batched operands:
>>> op = qml.sum(qml.RX(np.array([1, 2, 3]), wires=0), qml.PauliX(1))
>>> op.matrix().shape
(3, 4, 4)
But it doesn't support batching of operators:
>>> op = qml.sum(np.array([qml.RX(0.4, 0), qml.RZ(0.3, 0)]), qml.PauliZ(0))
AttributeError: 'numpy.ndarray' object has no attribute 'wires'
.. seealso:: :func:`~.ops.op_math.sum`
**Example**
>>> summed_op = Sum(qml.PauliX(0), qml.PauliZ(0))
>>> summed_op
PauliX(wires=[0]) + PauliZ(wires=[0])
>>> qml.matrix(summed_op)
array([[ 1, 1],
[ 1, -1]])
>>> summed_op.terms()
([1.0, 1.0], (PauliX(wires=[0]), PauliZ(wires=[0])))
.. details::
:title: Usage Details
We can combine parameterized operators, and support sums between operators acting on
different wires.
>>> summed_op = Sum(qml.RZ(1.23, wires=0), qml.Identity(wires=1))
>>> summed_op.matrix()
array([[1.81677345-0.57695852j, 0. +0.j ,
0. +0.j , 0. +0.j ],
[0. +0.j , 1.81677345-0.57695852j,
0. +0.j , 0. +0.j ],
[0. +0.j , 0. +0.j ,
1.81677345+0.57695852j, 0. +0.j ],
[0. +0.j , 0. +0.j ,
0. +0.j , 1.81677345+0.57695852j]])
The Sum operation can also be measured inside a qnode as an observable.
If the circuit is parameterized, then we can also differentiate through the
sum observable.
.. code-block:: python
sum_op = Sum(qml.PauliX(0), qml.PauliZ(1))
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev, diff_method="best")
def circuit(weights):
qml.RX(weights[0], wires=0)
qml.RY(weights[1], wires=1)
qml.CNOT(wires=[0, 1])
qml.RX(weights[2], wires=1)
return qml.expval(sum_op)
>>> weights = qnp.array([0.1, 0.2, 0.3], requires_grad=True)
>>> qml.grad(circuit)(weights)
tensor([-0.09347337, -0.18884787, -0.28818254], requires_grad=True)
"""
_op_symbol = "+"
_math_op = math.sum
@property
def hash(self):
# Since addition is always commutative, we do not need to sort
return hash(("Sum", frozenset(o.hash for o in self.operands)))
@property
def is_hermitian(self):
"""If all of the terms in the sum are hermitian, then the Sum is hermitian."""
return (
all(s.is_hermitian for s in self)
if self._pauli_rep is None
else not any(qml.math.iscomplex(val) for val in self._pauli_rep.values())
)
[docs] def terms(self):
r"""Representation of the operator as a linear combination of other operators.
.. math:: O = \sum_i c_i O_i
A ``TermsUndefinedError`` is raised if no representation by terms is defined.
Returns:
tuple[list[tensor_like or float], list[.Operation]]: list of coefficients :math:`c_i`
and list of operations :math:`O_i`
"""
return [1.0] * len(self), list(self)
[docs] def matrix(self, wire_order=None):
r"""Representation of the operator as a matrix in the computational basis.
If ``wire_order`` is provided, the numerical representation considers the position of the
operator's wires in the global wire order. Otherwise, the wire order defaults to the
operator's wires.
If the matrix depends on trainable parameters, the result
will be cast in the same autodifferentiation framework as the parameters.
A ``MatrixUndefinedError`` is raised if the matrix representation has not been defined.
.. seealso:: :meth:`~.Operator.compute_matrix`
Args:
wire_order (Iterable): global wire order, must contain all wire labels from the
operator's wires
Returns:
tensor_like: matrix representation
"""
gen = (
(qml.matrix(op) if isinstance(op, Hamiltonian) else op.matrix(), op.wires)
for op in self
)
reduced_mat, sum_wires = math.reduce_matrices(gen, reduce_func=math.add)
wire_order = wire_order or self.wires
return math.expand_matrix(reduced_mat, sum_wires, wire_order=wire_order)
[docs] def sparse_matrix(self, wire_order=None):
gen = ((op.sparse_matrix(), op.wires) for op in self)
reduced_mat, sum_wires = math.reduce_matrices(gen, reduce_func=math.add)
wire_order = wire_order or self.wires
return math.expand_matrix(reduced_mat, sum_wires, wire_order=wire_order)
@property
def _queue_category(self): # don't queue Sum instances because it may not be unitary!
"""Used for sorting objects into their respective lists in `QuantumTape` objects.
This property is a temporary solution that should not exist long-term and should not be
used outside of ``QuantumTape._process_queue``.
Returns: None
"""
return None
# pylint: disable=arguments-renamed, invalid-overridden-method
@property
def has_adjoint(self):
return True
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
]
):
return reduce((lambda a, b: a + b), operand_pauli_reps)
return None
@classmethod
def _simplify_summands(cls, summands: List[Operator]):
"""Reduces the depth of nested summands and groups equal terms together.
Args:
summands (List[~.operation.Operator]): summands list to simplify
Returns:
.SumSummandsGrouping: Class containing the simplified and grouped summands.
"""
new_summands = _SumSummandsGrouping()
for summand in summands:
# This code block is not needed but it speeds things up when having a lot of stacked Sums
if isinstance(summand, Sum):
sum_summands = cls._simplify_summands(summands=summand.operands)
for op_hash, [coeff, sum_summand] in sum_summands.queue.items():
new_summands.add(summand=sum_summand, coeff=coeff, op_hash=op_hash)
continue
simplified_summand = summand.simplify()
if isinstance(simplified_summand, Sum):
sum_summands = cls._simplify_summands(summands=simplified_summand.operands)
for op_hash, [coeff, sum_summand] in sum_summands.queue.items():
new_summands.add(summand=sum_summand, coeff=coeff, op_hash=op_hash)
else:
new_summands.add(summand=simplified_summand)
return new_summands
[docs] def simplify(self, cutoff=1.0e-12) -> "Sum": # pylint: disable=arguments-differ
# try using pauli_rep:
if pr := self._pauli_rep:
pr.simplify()
return pr.operation(wire_order=self.wires)
new_summands = self._simplify_summands(summands=self.operands).get_summands(cutoff=cutoff)
if new_summands:
return Sum(*new_summands) if len(new_summands) > 1 else new_summands[0]
return qml.s_prod(0, qml.Identity(self.wires))
@classmethod
def _sort(cls, op_list, wire_map: dict = None) -> List[Operator]:
"""Sort algorithm that sorts a list of sum summands by their wire indices.
Args:
op_list (List[.Operator]): list of operators to be sorted
wire_map (dict): Dictionary containing the wire values as keys and its indexes as values.
Defaults to None.
Returns:
List[.Operator]: sorted list of operators
"""
if isinstance(op_list, tuple):
op_list = list(op_list)
def _sort_key(op: Operator) -> tuple:
"""Sorting key used in the `sorted` python built-in function.
Args:
op (.Operator): Operator.
Returns:
Tuple[int, int, str]: Tuple containing the minimum wire value, the number of wires
and the string of the operator. This tuple is used to compare different operators
in the sorting algorithm.
"""
wires = op.wires
if wire_map is not None:
wires = wires.map(wire_map)
return np.min(wires), len(wires), str(op)
return sorted(op_list, key=_sort_key)
class _SumSummandsGrouping:
"""Utils class used for grouping sum summands together."""
def __init__(self):
self.queue = {} # {hash: [coeff, summand]}
def add(self, summand: Operator, coeff=1, op_hash=None):
"""Add operator to the summands dictionary.
If the operator hash is already in the dictionary, the coefficient is increased instead.
Args:
summand (Operator): operator to add to the summands dictionary
coeff (int, optional): Coefficient of the operator. Defaults to 1.
op_hash (int, optional): Hash of the operator. Defaults to None.
"""
if isinstance(summand, qml.ops.SProd): # pylint: disable=no-member
coeff = summand.scalar if coeff == 1 else summand.scalar * coeff
self.add(summand=summand.base, coeff=coeff)
else:
op_hash = summand.hash if op_hash is None else op_hash
if op_hash in self.queue:
self.queue[op_hash][0] += coeff
else:
self.queue[op_hash] = [copy(coeff), summand]
def get_summands(self, cutoff=1.0e-12):
"""Get summands list.
All summands with a coefficient less than cutoff are ignored.
Args:
cutoff (float, optional): Cutoff value. Defaults to 1.0e-12.
"""
new_summands = []
for coeff, summand in self.queue.values():
if coeff == 1:
new_summands.append(summand)
elif abs(coeff) > cutoff:
new_summands.append(qml.s_prod(coeff, summand))
return new_summands
_modules/pennylane/ops/op_math/sum
Download Python script
Download Notebook
View on GitHub