Source code for pennylane.transforms.op_transforms
# 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 module contains the op_transform decorator.
"""
# pylint: disable=protected-access
import functools
import inspect
import os
import warnings
import pennylane as qml
class OperationTransformError(Exception):
"""Raised when there is an error with the op_transform logic"""
[docs]class op_transform:
r"""Convert a function that applies to operators into a functional transform.
This allows the operator function to be used across PennyLane
on both instantiated operators as well as quantum functions.
By default, this decorator creates functional transforms that
accept a single operator. However, you can also register how the
transform acts on multiple operators. Once this is defined,
the transform can be used anywhere in PennyLane --- at the operator
level for operator arithmetic, or at the qfunc/QNode level.
.. warning::
This is an experimental feature, and is subject to change.
Args:
fn (function): The function to register as the operator transform.
It can have an arbitrary number of arguments, but the first argument
**must** be the input operator.
**Example**
Consider an operator function that computes the trace of an operator:
.. code-block:: python
@qml.op_transform
def trace(op):
try:
return qml.math.real(qml.math.sum(op.eigvals()))
except qml.operation.EigvalsUndefinedError:
return qml.math.real(qml.math.trace(op.matrix()))
We can use this function as written:
>>> op = qml.RX(0.5, wires=0)
>>> trace(op)
1.9378248434212895
By using the ``op_transform`` decorator, we also enable it to be used
as a functional transform:
>>> trace(qml.RX)(0.5, wires=0)
1.9378248434212895
Note that if we apply our function to an operation that does not define its
matrix or eigenvalues representation, we get an error:
>>> weights = np.array([[[0.7, 0.6, 0.5], [0.1, 0.2, 0.3]]])
>>> trace(qml.StronglyEntanglingLayers(weights, wires=[0, 1]))
pennylane.operation.EigvalsUndefinedError
During handling of the above exception, another exception occurred:
pennylane.operation.MatrixUndefinedError
The most powerful reason for using ``op_transform`` is the ability to define
how the transform behaves if applied to a datastructure that supports multiple
operations, such as a qfunc, tape, or QNode.
We do this by defining a tape transform:
.. code-block:: python
@trace.tape_transform
def trace(tape):
tr = qml.math.trace(qml.matrix(tape))
return qml.math.real(tr)
We can now apply this transform directly to a qfunc:
>>> def circuit(x, y):
... qml.RX(x, wires=0)
... qml.Hadamard(wires=1)
... qml.CNOT(wires=[0, 1])
... qml.CRY(y, wires=[1, 0])
>>> trace(circuit)(0.1, 0.8)
1.4124461636742214
Our example above, applying our function to an operation that does not
define the matrix or eigenvalues, will now work, since PennyLane will
decompose the operation automatically into multiple operations:
>>> trace(qml.StronglyEntanglingLayers)(weights, wires=[0, 1])
0.4253851061350833
.. note::
If the operator transform takes additional (optional) transform parameters,
then the registered tape transform should take the same transform parameters.
E.g., consider a transform that takes the transform parameter ``lower``:
.. code-block:: python
@qml.op_transform
def name(op, lower=True):
return op.name().lower() if lower else op.name()
@name.tape_transform
def name(tape, lower=True):
return [name(op, lower=lower) for op in tape.operations]
If the transformation has purely quantum output, we can register the tape transformation
as a qfunc transformation in addition:
.. code-block:: python
@qml.op_transform
def simplify_rotation(op):
if op.name == "Rot":
params = op.parameters
wires = op.wires
if qml.math.allclose(params, 0):
return
if qml.math.allclose(params[1:2], 0):
return qml.RZ(params[0], wires)
return op
@simplify_rotation.tape_transform
@qml.qfunc_transform
def simplify_rotation(tape):
for op in tape:
if op.name == "Rot":
simplify_rotation(op)
else:
qml.apply(op)
We can now use this combined operator and quantum function transform in compilation pipelines:
.. code-block:: python
@qml.qnode(dev)
@qml.compile(pipeline=[simplify_rotation])
def circuit(weights):
ansatz(weights)
qml.CNOT(wires=[0, 1])
qml.Rot(0.0, 0.0, 0.0, wires=0)
return qml.expval(qml.PauliX(1))
"""
def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument
if os.environ.get("SPHINX_BUILD") == "1":
# If called during a Sphinx documentation build,
# simply return the original function rather than
# instantiating the object. This allows the signature to
# be correctly displayed in the documentation.
warnings.warn(
"Operator transformations have been disabled, as a Sphinx "
"build has been detected via SPHINX_BUILD='1'. If this is not the "
"case, please set the environment variable SPHINX_BUILD='0'.",
UserWarning,
)
args[0].tape_transform = lambda x: x
return args[0]
return super().__new__(cls)
def __init__(self, fn):
if not callable(fn):
raise OperationTransformError(
f"The operator function to register, {fn}, "
"does not appear to be a valid Python function or callable."
)
self._fn = fn
self._sig = inspect.signature(fn).parameters
self._tape_fn = None
functools.update_wrapper(self, fn)
def __call__(self, *targs, **tkwargs):
obj = None
if targs:
# assume the first argument passed to the transform
# is the object we wish to transform
obj, *targs = targs
if isinstance(obj, (qml.operation.Operator, qml.tape.QuantumScript)) or callable(obj):
return self._create_wrapper(obj, *targs, **tkwargs)
# Input is not an operator nor a QNode nor a quantum tape nor a qfunc.
# Assume Python decorator syntax:
#
# op_func = op_transform(op_func)
# result = op_func(*transform_args)(obj)(*obj_args)
#
# or
#
# @op_func(*transform_args)
# @qml.qnode(dev)
# def circuit(...):
# ...
# result = circuit(*qnode_args)
# Prepend the input to the transform args,
# and create a wrapper function.
if obj is not None:
targs = (obj,) + tuple(targs)
def wrapper(obj):
return self._create_wrapper(obj, *targs, **tkwargs)
return wrapper
[docs] def fn(self, obj, *args, **kwargs):
"""Evaluate the underlying operator transform function.
If a corresponding tape transform for the operator has been registered
using the :attr:`.op_transform.tape_transform` decorator,
then if an exception is raised while calling the transform function,
this method will attempt to decompose the provided object for the tape
transform.
Args:
obj (.Operator, pennylane.QNode, .QuantumTape, or Callable): An operator, quantum node, tape,
or function that applies quantum operations.
*args: positional arguments to pass to the function
**kwargs: keyword arguments to pass to the function
Returns:
any: the result of evaluating the transform
"""
try:
return self._fn(obj, *args, **kwargs)
except Exception as e1: # pylint: disable=broad-except
try:
# attempt to decompose the operation and call
# the tape transform function if defined
return self.tape_fn(obj.expand(), *args, **kwargs)
except (
AttributeError,
qml.operation.OperatorPropertyUndefined,
OperationTransformError,
) as e:
# if obj.expand() does not exist, a required operation property was not found,
# or the tape transform function does not exist, simply raise the original exception
raise e1 from e
[docs] def tape_fn(self, obj, *args, **kwargs):
"""The tape transform function.
This is the function that is called if a datastructure is passed
that contains multiple operations.
Args:
obj (pennylane.QNode, .QuantumTape, or Callable): A quantum node, tape,
or function that applies quantum operations.
*args: positional arguments to pass to the function
**kwargs: keyword arguments to pass to the function
Returns:
any: the result of evaluating the transform
Raises:
.OperationTransformError: if no tape transform function is defined
.. seealso:: :meth:`.op_transform.tape_transform`
"""
if self._tape_fn is None:
raise OperationTransformError(
"This transform does not support tapes or QNodes with multiple operations."
)
return self._tape_fn(obj, *args, **kwargs)
@property
def is_qfunc_transform(self):
"""bool: Returns ``True`` if the operator transform is also a qfunc transform.
That is, it maps one or more quantum operations to one or more quantum operations, allowing
the output of the transform to be used as a quantum function.
.. seealso:: :func:`~.qfunc_transform`
"""
return isinstance(getattr(self._tape_fn, "tape_fn", None), qml.single_tape_transform)
[docs] def tape_transform(self, fn):
"""Register a tape transformation to enable the operator transform
to apply to datastructures containing multiple operations, such as QNodes, qfuncs,
and tapes.
.. note::
The registered tape transform should have the same parameters as the
original operation transform function.
.. note::
If the transformation maps a tape to a tape (or equivalently, a qfunc to a qfunc)
then the transformation is simultaneously a :func:`~.qfunc_transform`, and
can be declared as such. This enables additional functionality, for example
the ability to use the transform in a compilation pipeline.
Args:
fn (callable): The function to register as the tape transform. This function
should accept a :class:`~.QuantumTape` as the first argument.
**Example**
.. code-block:: python
@qml.op_transform
def name(op, lower=False):
if lower:
return op.name.lower()
return op.name
@name.tape_transform
def name(tape, lower=True):
return [name(op, lower=lower) for op in tape.operations]
We can now use this function on a qfunc, tape, or QNode:
>>> def circuit(x, y):
... qml.RX(x, wires=0)
... qml.Hadamard(wires=1)
... qml.CNOT(wires=[0, 1])
... qml.CRY(y, wires=[1, 0])
>>> name(circuit, lower=True)(0.1, 0.8)
['rx', 'hadamard', 'cnot', 'cry']
If the transformation has purely quantum output, we can register the tape transformation
as a qfunc transformation in addition:
.. code-block:: python
@qml.op_transform
def simplify_rotation(op):
if op.name == "Rot":
params = op.parameters
wires = op.wires
if qml.math.allclose(params, 0):
return
if qml.math.allclose(params[1:2], 0):
return qml.RZ(params[0], wires)
return op
@simplify_rotation.tape_transform
@qml.qfunc_transform
def simplify_rotation(tape):
for op in tape:
if op.name == "Rot":
simplify_rotation(op)
else:
qml.apply(op)
We can now use this combined operator and quantum function transform in compilation pipelines:
.. code-block:: python
@qml.qnode(dev)
@qml.compile(pipeline=[simplify_rotation])
def circuit(weights):
ansatz(weights)
qml.CNOT(wires=[0, 1])
qml.Rot(0.0, 0.0, 0.0, wires=0)
return qml.expval(qml.PauliX(1))
"""
self._tape_fn = fn
return self
def _create_wrapper(self, obj, *targs, wire_order=None, **tkwargs):
"""Create a wrapper function that, when evaluated, transforms
``obj`` according to transform arguments ``*targs`` and ``**tkwargs``
"""
if isinstance(obj, qml.operation.Operator):
# Input is a single operation.
# op_transform(obj, *transform_args)
if wire_order is not None:
tkwargs["wire_order"] = wire_order
wrapper = self.fn(obj, *targs, **tkwargs)
elif isinstance(obj, qml.tape.QuantumScript):
# Input is a quantum tape. Get the quantum tape.
tape, verified_wire_order = self._make_tape(obj, wire_order)
if wire_order is not None:
tkwargs["wire_order"] = verified_wire_order
wrapper = self.tape_fn(tape, *targs, **tkwargs)
elif callable(obj):
# Input is a QNode, or qfunc (including single-operation qfuncs).
# Get the quantum tape.
def wrapper(*args, **kwargs):
nonlocal wire_order
tape, verified_wire_order = self._make_tape(obj, wire_order, *args, **kwargs)
# HOTFIX: some operator transforms return a tape containing
# a single transformed operator. As a result, for now we need
# to treat a tape with a single operation as a single operation.
# if len(getattr(tape, "operations", [])) == 1 and self._tape_fn is None:
# tape = tape.operations[0]
if wire_order is not None or (
"wire_order" in self._sig and isinstance(obj, qml.QNode)
):
# Use the verified wire order if:
# - wire_order was passed to the transform
# - The object is a QNode, and the function takes a wire_order argument
tkwargs["wire_order"] = verified_wire_order
if isinstance(tape, qml.operation.Operator):
return self.fn(tape, *targs, **tkwargs)
if self.is_qfunc_transform:
# we must evaluate the qfunc transform at the original
# function arguments
return self.tape_fn(obj, *kwargs, **tkwargs)(*args, **kwargs)
return self.tape_fn(tape, *targs, **tkwargs)
else:
raise OperationTransformError(
"Input is not an Operator, tape, QNode, or quantum function"
)
return wrapper
@staticmethod
def _make_tape(obj, wire_order, *args, **kwargs):
"""Given an input object, which may be:
- an object such as a tape or a operation, or
- a callable such as a QNode or a quantum function
(alongside the callable arguments ``args`` and ``kwargs``),
this function constructs and returns the tape/operation
represented by the object.
The ``wire_order`` argument determines whether a custom wire ordering
should be used. If not provided, the wire ordering defaults to the
objects wire ordering accessed via ``obj.wires``.
Returns:
tuple[.QuantumTape, Wires]: returns the tape and the verified wire order
"""
if isinstance(obj, qml.QNode):
# user passed a QNode, get the tape
obj.construct(args, kwargs)
tape = obj.qtape
wires = obj.device.wires
elif isinstance(obj, qml.tape.QuantumScript):
# user passed a tape
tape = obj
wires = tape.wires
elif inspect.isclass(obj) and issubclass(obj, qml.operation.Operator):
with qml.QueuingManager.stop_recording():
tape = obj(*args, **kwargs)
wires = tape.wires
elif callable(obj):
# user passed something that is callable but not a tape or QNode.
tape = qml.tape.make_qscript(obj)(*args, **kwargs)
wires = tape.wires
# raise exception if it is not a quantum function
if len(tape.operations) == 0 and len(tape.measurements) == 0:
raise OperationTransformError("Quantum function contains no quantum operations")
# if no wire ordering is specified, take wire list from tape/device
wire_order = wires if wire_order is None else qml.wires.Wires(wire_order)
# check that all wire labels in the circuit are contained in wire_order
if not set(tape.wires).issubset(wire_order):
raise OperationTransformError(
f"Wires in circuit {tape.wires.tolist()} are inconsistent with "
f"those in wire_order {wire_order.tolist()}"
)
return tape, wire_order
_modules/pennylane/transforms/op_transforms
Download Python script
Download Notebook
View on GitHub