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 13 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
23 changes: 23 additions & 0 deletions pennylane/ops/op_math/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2018-2022 Xanadu Quantum Technologies Inc.
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

# 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 module contains classes and functions for Operator arithmetic.

.. currentmodule:: pennylane.ops.op_math
.. autosummary::
:toctree: api

"""

from .pow_class import Pow
285 changes: 285 additions & 0 deletions pennylane/ops/op_math/pow_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
# 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 indicates the power of an operator.
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
"""
from copy import copy
from scipy.linalg import fractional_matrix_power

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

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 matrix.
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

albi3ro marked this conversation as resolved.
Show resolved Hide resolved
"""

# 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=0 (float): the exponent
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

**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]])
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
>>> 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=0, 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:
class_bases = (PowOperation, Pow, Observable, Operation)
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
cls._operation_observable_type = type("Pow", class_bases, dict(cls.__dict__))
return object.__new__(cls._operation_observable_type)

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

if isinstance(base, Observable):
if cls._observable_type is None:
class_bases = (Pow, Observable)
cls._observable_type = type("Pow", class_bases, 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=0, 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

# pylint: disable=arguments-differ
@staticmethod
def compute_matrix(*params, base=None, z=0):
base_matrix = base.compute_matrix(*params, **base.hyperparameters)
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

if isinstance(z, int):
return qmlmath.linalg.matrix_power(base_matrix, z)
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

return fractional_matrix_power(base_matrix, z)
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

# 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)]
# what if z is an int and less than 0?
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
# 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):
if isinstance(self.z, int):
return self.base.diagonalizing_gates()
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
# does this hold for non-integer z?
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
return super().diagonalizing_gates()

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

def generator(self):
return self.z * self.base.generator()
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

@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
"""
return self.base._queue_category # pylint: disable=protected-access
2 changes: 1 addition & 1 deletion pennylane/tape/tape.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False):
getattr(new_tape, queue).append(obj)
continue

if isinstance(obj, (qml.operation.Operation, qml.measurements.MeasurementProcess)):
if isinstance(obj, (qml.operation.Operator, qml.measurements.MeasurementProcess)):
# Object is an operation; query it for its expansion
try:
obj = obj.expand()
Expand Down
Loading