Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pow Symbolic Operator #2621

Merged
merged 22 commits into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pennylane/math/single_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def _i(name):
ar.register_function("builtins", "block_diag", lambda x: _scipy_block_diag(*x))
ar.register_function("numpy", "gather", lambda x, indices: x[np.array(indices)])
ar.register_function("numpy", "unstack", list)

ar.register_function("builtins", "unstack", list)


Expand Down
2 changes: 2 additions & 0 deletions pennylane/ops/op_math/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@

from .adjoint_class import Adjoint
from .adjoint_constructor import adjoint

from .pow_class import Pow
336 changes: 336 additions & 0 deletions pennylane/ops/op_math/pow_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
# 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 submodule defines the symbolic operation that stands for the power of an operator.
"""
from copy import copy
from scipy.linalg import fractional_matrix_power

from pennylane.operation import (
DecompositionUndefinedError,
SparseMatrixUndefinedError,
PowUndefinedError,
Operator,
Operation,
Observable,
expand_matrix,
)
from pennylane.queuing import QueuingContext, apply
from pennylane.wires import Wires

from pennylane import math as qmlmath

_superscript = str.maketrans("0123456789.+-", "⁰¹²³⁴⁵⁶⁷⁸⁹⋅⁺⁻")


# pylint: disable=no-member
class PowOperation(Operation):
"""Operation-specific methods and properties for the ``Pow`` class.
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

Dynamically mixed in based on the provided base operator. If the base operator is an
Operation, this class will be mixed in.

albi3ro marked this conversation as resolved.
Show resolved Hide resolved
When we no longer rely on certain functionality through `Operation`, we can get rid of this
class.
"""

# until we add gradient support
grad_method = None

def inv(self):
self.hyperparameters["z"] *= -1
self._name = f"{self.base.name}**{self.z}"
return self

@property
def inverse(self):
return False

@inverse.setter
def inverse(self, boolean):
if boolean is True:
raise NotImplementedError("The inverse can not be set for a power operator")
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

@property
def base_name(self):
return self._name

@property
def name(self):
return self._name

# pylint: disable=missing-function-docstring
@property
def basis(self):
return self.base.basis

@property
def control_wires(self):
return self.base.control_wires


class Pow(Operator):
"""Symbolic operator denoting an operator raised to a power.

Args:
base (~.operation.Operator): the operator to be raised to a power
z=1 (float): the exponent

**Example**

>>> sqrt_x = Pow(qml.PauliX(0), 0.5)
>>> sqrt_x.decomposition()
[SX(wires=[0])]
>>> qml.matrix(sqrt_x)
array([[0.5+0.5j, 0.5-0.5j],
[0.5-0.5j, 0.5+0.5j]])
>>> qml.matrix(qml.SX(0))
array([[0.5+0.5j, 0.5-0.5j],
[0.5-0.5j, 0.5+0.5j]])
>>> qml.matrix(Pow(qml.T(0), 1.234))
array([[1. +0.j , 0. +0.j ],
[0. +0.j , 0.56597465+0.82442265j]])
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

"""

_operation_type = None # type if base inherits from operation and not observable
_operation_observable_type = None # type if base inherits from both operation and observable
_observable_type = None # type if base inherits from observable and not oepration

# pylint: disable=unused-argument
def __new__(cls, base=None, z=1, do_queue=True, id=None):
"""Mixes in parents based on inheritance structure of base.

Though all the types will be named "Pow", their *identity* and location in memory will be different
based on ``base``'s inheritance. We cache the different types in private class variables so that:

"""

if isinstance(base, Operation):
if isinstance(base, Observable):
if cls._operation_observable_type is None:
base_classes = (PowOperation, Pow, Observable, Operation)
cls._operation_observable_type = type("Pow", base_classes, dict(cls.__dict__))
return object.__new__(cls._operation_observable_type)

# not an observable
if cls._operation_type is None:
base_classes = (PowOperation, Pow, Operation)
cls._operation_type = type("Pow", base_classes, dict(cls.__dict__))
return object.__new__(cls._operation_type)

if isinstance(base, Observable):
if cls._observable_type is None:
base_classes = (Pow, Observable)
cls._observable_type = type("Pow", base_classes, dict(cls.__dict__))
return object.__new__(cls._observable_type)

return object.__new__(Pow)

# pylint: disable=attribute-defined-outside-init
def __copy__(self):
# this method needs to be overwritten becuase the base must be copied too.
copied_op = object.__new__(type(self))
# copied_op must maintain inheritance structure of self
# For example, it must keep AdjointOperation if self has it
# this way preserves inheritance structure

for attr, value in vars(self).items():
if attr != "_hyperparameters":
setattr(copied_op, attr, value)
copied_op._hyperparameters = copy(self._hyperparameters)
copied_op._hyperparameters["base"] = copy(self.base)

return copied_op

# pylint: disable=super-init-not-called
def __init__(self, base=None, z=1, do_queue=True, id=None):

# incorporate base inverse attribute into the exponent
if getattr(base, "inverse", False):
base.inverse = False
z *= -1
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

self.hyperparameters["base"] = base
self.hyperparameters["z"] = z
self._id = id
self.queue_idx = None

self._name = f"{self.base.name}**{z}"

if do_queue:
self.queue()

@property
def base(self):
"""The operator that is raised to a power."""
return self.hyperparameters["base"]

@property
def z(self):
"""The exponent."""
return self.hyperparameters["z"]

@property
def data(self):
"""Trainable parameters that the operator depends on."""
return self.base.data

@data.setter
def data(self, new_data):
"""Allows us to set base operation parameters."""
self.base.data = new_data

@property
def parameters(self):
return self.base.parameters

@property
def num_params(self):
return self.base.num_params

@property
def wires(self):
return self.base.wires

# pylint: disable=protected-access
@property
def _wires(self):
return self.base._wires

# pylint: disable=protected-access
@_wires.setter
def _wires(self, new_wires):
# used in a couple places that want to update the wires of an operator
# we should create a better way to set new wires in the future
self.base._wires = new_wires
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

@property
def num_wires(self):
return len(self.wires)

def queue(self, context=QueuingContext):
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
context.safe_update_info(self.base, owner=self)
context.append(self, owns=self.base)

return self

def label(self, decimals=None, base_label=None, cache=None):
z_string = format(self.z).translate(_superscript)
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
return self.base.label(decimals, base_label, cache=cache) + z_string

# pylint: disable=arguments-renamed, invalid-overridden-method
@property
def has_matrix(self):
return self.base.has_matrix

@property
def is_hermitian(self):
return self.base.is_hermitian

def matrix(self, wire_order=None):
base_matrix = self.base.matrix()

if isinstance(self.z, int):
mat = qmlmath.linalg.matrix_power(base_matrix, self.z)
else:
mat = fractional_matrix_power(base_matrix, self.z)

if wire_order is None or self.wires == Wires(wire_order):
return mat

return expand_matrix(mat, wires=self.wires, wire_order=wire_order)

# pylint: disable=arguments-differ
@staticmethod
def compute_sparse_matrix(*params, base=None, z=0):
if isinstance(z, int):
base_matrix = base.compute_sparse_matrix(*params, **base.hyperparameters)
return base_matrix**z
raise SparseMatrixUndefinedError

def decomposition(self):
try:
return self.base.pow(self.z)
except PowUndefinedError as e:
if isinstance(self.z, int) and self.z > 0:
if QueuingContext.recording():
return [apply(self.base) for _ in range(self.z)]
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
return [self.base.__copy__() for _ in range(self.z)]
# TODO: consider: what if z is an int and less than 0?
# do we want Pow(base, -1) to be a "more fundamental" op
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
raise DecompositionUndefinedError from e

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`.

The diagonalizing gates of an operator to a power is the same as the diagonalizing
gates as the original operator. As we can see,

.. math::

O^2 = U \Sigma U^{\dagger} U \Sigma U^{\dagger} = U \Sigma^2 U^{\dagger}

This formula can be extended to inversion and any rational number.

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()

def eigvals(self):
base_eigvals = self.base.eigvals()
return [value**self.z for value in base_eigvals]

def generator(self):
r"""Generator of an operator that is in single-parameter-form.

The generator of a power operator is ``z`` times the generator of the
base matrix.

.. math::

U(\phi)^z = e^{i\phi (z G)}

See also :func:`~.generator`
"""
return self.z * self.base.generator()

@property
def _queue_category(self):
"""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
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
used outside of ``QuantumTape._process_queue``.

Returns ``_queue_cateogory`` for base operator.
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

Options are:
* `"_prep"`
* `"_ops"`
* `"_measurements"`
* `None`
"""
return self.base._queue_category # pylint: disable=protected-access
Loading