# Source code for pennylane.ops.op_math.sprod

# Copyright 2018-2022 Xanadu Quantum Technologies Inc.

# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# Unless required by applicable law or agreed to in writing, software
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
"""
This file contains the implementation of the SProd class which contains logic for
computing the scalar product of operations.
"""
from copy import copy
from typing import Union

import pennylane as qml
import pennylane.math as qnp
from pennylane.operation import Operator, TermsUndefinedError, convert_to_opmath
from pennylane.ops.op_math.pow import Pow
from pennylane.ops.op_math.sum import Sum
from pennylane.queuing import QueuingManager

from .symbolicop import ScalarSymbolicOp

[docs]def s_prod(scalar, operator, lazy=True, id=None):
r"""Construct an operator which is the scalar product of the
given scalar and operator provided.

Args:
scalar (float or complex): the scale factor being multiplied to the operator.
operator (~.operation.Operator): the operator which will get scaled.

Keyword Args:
lazy=True (bool): If lazy=False and the operator is already a scalar product operator, the scalar provided will simply be combined with the existing scaling factor.
id (str or None): id for the scalar product operator. Default is None.
Returns:
~ops.op_math.SProd: The operator representing the scalar product.

.. note::

This operator supports a batched base, a batched coefficient and a combination of both:

>>> op = qml.s_prod(scalar=4, operator=qml.RX([1, 2, 3], wires=0))
>>> qml.matrix(op).shape
(3, 2, 2)
>>> op = qml.s_prod(scalar=[1, 2, 3], operator=qml.RX(1, wires=0))
>>> qml.matrix(op).shape
(3, 2, 2)
>>> op = qml.s_prod(scalar=[4, 5, 6], operator=qml.RX([1, 2, 3], wires=0))
>>> qml.matrix(op).shape
(3, 2, 2)

But it doesn't support batching of operators:

>>> op = qml.s_prod(scalar=4, operator=[qml.RX(1, wires=0), qml.RX(2, wires=0)])
AttributeError: 'list' object has no attribute 'batch_size'

.. seealso:: :class:~.ops.op_math.SProd and :class:~.ops.op_math.SymbolicOp

**Example**

>>> sprod_op = s_prod(2.0, qml.X(0))
>>> sprod_op
2.0 * X(0)
>>> sprod_op.matrix()
array([[ 0., 2.],
[ 2., 0.]])
"""
operator = convert_to_opmath(operator)
if lazy or not isinstance(operator, SProd):
return SProd(scalar, operator, id=id)

sprod_op = SProd(scalar=scalar * operator.scalar, base=operator.base, id=id)
QueuingManager.remove(operator)
return sprod_op

[docs]class SProd(ScalarSymbolicOp):
r"""Arithmetic operator representing the scalar product of an
operator with the given scalar.

Args:
scalar (float or complex): the scale factor being multiplied to the operator.
base (~.operation.Operator): the operator which will get scaled.

Keyword Args:
id (str or None): id for the scalar product operator. Default is None.

.. note::
Currently this operator can not be queued in a circuit as an operation, only measured terminally.

.. seealso:: :func:~.ops.op_math.s_prod

**Example**

>>> sprod_op = SProd(1.23, qml.X(0))
>>> sprod_op
1.23 * X(0)
>>> qml.matrix(sprod_op)
array([[0.  , 1.23],
[1.23, 0.  ]])
>>> sprod_op.terms()
([1.23], [PauliX(wires=[0]])

.. details::
:title: Usage Details

The SProd operation can also be measured inside a qnode as an observable.
If the circuit is parameterized, then we can also differentiate through the observable.

.. code-block:: python

dev = qml.device("default.qubit", wires=1)

@qml.qnode(dev, diff_method="best")
def circuit(scalar, theta):
qml.RX(theta, wires=0)

>>> scalar, theta = (1.2, 3.4)
(array(-0.68362956), array(0.21683382))

"""

_name = "SProd"

def _flatten(self):
return (self.scalar, self.base), tuple()

@classmethod
def _unflatten(cls, data, _):
return cls(data[0], data[1])

def __init__(
self, scalar: Union[int, float, complex], base: Operator, id=None, _pauli_rep=None
):
super().__init__(base=base, scalar=scalar, id=id)

if _pauli_rep:
self._pauli_rep = _pauli_rep
elif (base_pauli_rep := getattr(self.base, "pauli_rep", None)) and (
self.batch_size is None
):
scalar = copy(self.scalar)

pr = {pw: qnp.dot(coeff, scalar) for pw, coeff in base_pauli_rep.items()}
self._pauli_rep = qml.pauli.PauliSentence(pr)
else:
self._pauli_rep = None

def __repr__(self):
"""Constructor-call-like representation."""
if isinstance(self.base, qml.ops.CompositeOp):
return f"{self.scalar} * ({self.base})"
return f"{self.scalar} * {self.base}"

[docs]    def label(self, decimals=None, base_label=None, cache=None):
"""The label produced for the SProd op."""
scalar_val = (
f"{self.scalar}"
if decimals is None
else format(qml.math.toarray(self.scalar), f".{decimals}f")
)

return base_label or f"{scalar_val}*{self.base.label(decimals=decimals, cache=cache)}"

@property
def num_params(self):
"""Number of trainable parameters that the operator depends on.
Usually 1 + the number of trainable parameters for the base op.

Returns:
int: number of trainable parameters
"""
return 1 + self.base.num_params

[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

"""
try:
base_coeffs, base_ops = self.base.terms()
return [self.scalar * coeff for coeff in base_coeffs], base_ops
except TermsUndefinedError:
return [self.scalar], [self.base]

@property
def is_hermitian(self):
"""If the base operator is hermitian and the scalar is real,
then the scalar product operator is hermitian."""
return self.base.is_hermitian and not qml.math.iscomplex(self.scalar)

# pylint: disable=arguments-renamed,invalid-overridden-method
@property
def has_diagonalizing_gates(self):
"""Bool: Whether the Operator returns defined diagonalizing gates."""
return self.base.has_diagonalizing_gates

[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
"""
return self.base.diagonalizing_gates()

[docs]    def eigvals(self):
r"""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.
"""
base_eigs = self.base.eigvals()
if qml.math.get_interface(self.scalar) == "torch" and self.scalar.requires_grad:
base_eigs = qml.math.convert_like(base_eigs, self.scalar)
return self.scalar * base_eigs

[docs]    def sparse_matrix(self, wire_order=None):
"""Computes, by default, a scipy.sparse.csr_matrix representation of this Tensor.

This is useful for larger qubit numbers, where the dense matrix becomes very large, while
consisting mostly of zero entries.

Args:
wire_order (Iterable): Wire labels that indicate the order of wires according to which the matrix
is constructed. If not provided, self.wires is used.

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")
mat = self.base.sparse_matrix(wire_order=wire_order).multiply(self.scalar)
mat.eliminate_zeros()
return mat

@property
def has_matrix(self):
"""Bool: Whether or not the Operator returns a defined matrix."""
return isinstance(self.base, qml.ops.Hamiltonian) or self.base.has_matrix

@staticmethod
def _matrix(scalar, mat):
return scalar * mat

@property
def _queue_category(self):  # don't queue scalar prods as they might 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

[docs]    def pow(self, z):
"""Returns the operator raised to a given power."""
return [SProd(scalar=self.scalar**z, base=Pow(base=self.base, z=z))]

"""Create an operation that is the adjoint of this one.

Adjointed operations are the conjugated and transposed version of the
original operation. Adjointed ops are equivalent to the inverted operation for unitary
gates.

Returns:
"""

# pylint: disable=too-many-return-statements
[docs]    def simplify(self) -> Operator:
"""Reduce the depth of nested operators to the minimum.

Returns:
.Operator: simplified operator
"""
# try using pauli_rep:
if pr := self.pauli_rep:
pr.simplify()
return pr.operation(wire_order=self.wires)

if self.scalar == 1:
return self.base.simplify()
if isinstance(self.base, SProd):
scalar = self.scalar * self.base.scalar
if scalar == 1:
return self.base.base.simplify()
return SProd(scalar=scalar, base=self.base.base.simplify())

new_base = self.base.simplify()
if isinstance(new_base, Sum):
return Sum(
*(SProd(scalar=self.scalar, base=summand).simplify() for summand in new_base)
)
if isinstance(new_base, SProd):
return SProd(scalar=self.scalar, base=new_base).simplify()
return SProd(scalar=self.scalar, base=new_base)


Using PennyLane

Release news

Development

API

Internals