Source code for pennylane.transforms.tape_expand
# Copyright 2018-2021 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 tape expansion functions and stopping criteria to
generate such functions from."""
# pylint: disable=unused-argument,invalid-unary-operand-type, unsupported-binary-operation, no-member
import contextlib
import pennylane as qml
from pennylane.operation import (
gen_is_multi_term_hamiltonian,
has_gen,
has_grad_method,
has_nopar,
has_unitary_gen,
is_measurement,
is_trainable,
not_tape,
)
def _update_trainable_params(tape):
params = tape.get_parameters(trainable_only=False)
tape.trainable_params = qml.math.get_trainable_indices(params)
[docs]def create_expand_fn(depth, stop_at=None, device=None, docstring=None):
"""Create a function for expanding a tape to a given depth, and
with a specific stopping criterion. This is a wrapper around
:meth:`~.QuantumTape.expand`.
Args:
depth (int): Depth for the expansion
stop_at (callable): Stopping criterion. This must be a function with signature
``stop_at(obj)``, where ``obj`` is a *queueable* PennyLane object such as
:class:`~.Operation` or :class:`~.MeasurementProcess`. It must return a
boolean, indicating if the expansion should stop at this object.
device (pennylane.Device): Ensure that the expanded tape only uses native gates of the
given device.
docstring (str): docstring for the generated expansion function
Returns:
callable: Tape expansion function. The returned function accepts a :class:`~.QuantumTape`,
and returns an expanded :class:`~.QuantumTape`.
**Example**
Let us construct an expansion function that expands a tape in order to
decompose trainable multi-parameter gates. We allow for up to five expansion
steps, which can be controlled with the argument ``depth``.
The stopping criterion is easy to write as
>>> stop_at = ~(qml.operation.has_multipar & qml.operation.is_trainable)
Then the expansion function can be obtained via
>>> expand_fn = qml.transforms.create_expand_fn(depth=5, stop_at=stop_at)
We can test the newly generated function on an example tape:
.. code-block:: python
ops = [
qml.RX(0.2, wires=0),
qml.RX(qml.numpy.array(-2.4, requires_grad=True), wires=1),
qml.Rot(1.7, 0.92, -1.1, wires=0),
qml.Rot(*qml.numpy.array([-3.1, 0.73, 1.36], requires_grad=True), wires=1)
]
tape = qml.tape.QuantumTape(ops)
>>> new_tape = expand_fn(tape)
>>> print(qml.drawer.tape_text(tape, decimals=1))
0: ──RX(0.2)───Rot(1.7,0.9,-1.1)─┤
1: ──RX(-2.4)──Rot(-3.1,0.7,1.4)─┤
>>> print(qml.drawer.tape_text(new_tape, decimals=1))
0: ──RX(0.2)───Rot(1.7,0.9,-1.1)───────────────────┤
1: ──RX(-2.4)──RZ(-3.1)───────────RY(0.7)──RZ(1.4)─┤
"""
# pylint: disable=unused-argument
if device is not None:
if stop_at is None:
stop_at = device.stopping_condition
else:
stop_at &= device.stopping_condition
def expand_fn(tape, depth=depth, **kwargs):
with qml.QueuingManager.stop_recording():
if stop_at is None:
tape = tape.expand(depth=depth)
elif not all(stop_at(op) for op in tape.operations):
tape = tape.expand(depth=depth, stop_at=stop_at)
else:
return tape
_update_trainable_params(tape)
return tape
if docstring:
expand_fn.__doc__ = docstring
return expand_fn
_expand_multipar_doc = """Expand out a tape so that all its parametrized
operations have a single parameter.
This is achieved by decomposing all parametrized operations that do not have
a generator, up to maximum depth ``depth``.
For a sufficient ``depth``, it should always be possible to obtain a tape containing
only single-parameter operations.
Args:
tape (.QuantumTape): the input tape to expand
depth (int) : the maximum expansion depth
**kwargs: additional keyword arguments are ignored
Returns:
.QuantumTape: the expanded tape
"""
expand_multipar = create_expand_fn(
depth=10,
stop_at=not_tape | is_measurement | has_nopar | (has_gen & ~gen_is_multi_term_hamiltonian),
docstring=_expand_multipar_doc,
)
_expand_trainable_multipar_doc = """Expand out a tape so that all its trainable
operations have a single parameter.
This is achieved by decomposing all trainable operations that do not have
a generator, up to maximum depth ``depth``.
For a sufficient ``depth``, it should always be possible to obtain a tape containing
only single-parameter operations.
Args:
tape (.QuantumTape): the input tape to expand
depth (int) : the maximum expansion depth
**kwargs: additional keyword arguments are ignored
Returns:
.QuantumTape: the expanded tape
"""
expand_trainable_multipar = create_expand_fn(
depth=10,
stop_at=not_tape
| is_measurement
| has_nopar
| (~is_trainable)
| (has_gen & ~gen_is_multi_term_hamiltonian),
docstring=_expand_trainable_multipar_doc,
)
def create_expand_trainable_multipar(tape, use_tape_argnum=False):
"""Creates the expand_trainable_multipar expansion transform with an option to include argnums."""
if not use_tape_argnum:
return expand_trainable_multipar
# pylint: disable=protected-access
trainable_par_info = [tape.par_info[i] for i in tape.trainable_params]
trainable_ops = [info["op"] for info in trainable_par_info]
@qml.BooleanFn
def _is_trainable(obj):
return obj in trainable_ops
return create_expand_fn(
depth=10,
stop_at=not_tape
| is_measurement
| has_nopar
| (~_is_trainable)
| (has_gen & ~gen_is_multi_term_hamiltonian),
docstring=_expand_trainable_multipar_doc,
)
_expand_nonunitary_gen_doc = """Expand out a tape so that all its parametrized
operations have a unitary generator.
This is achieved by decomposing all parametrized operations that either do not have
a generator or have a non-unitary generator, up to maximum depth ``depth``.
For a sufficient ``depth``, it should always be possible to obtain a tape containing
only unitarily generated operations.
Args:
tape (.QuantumTape): the input tape to expand
depth (int) : the maximum expansion depth
**kwargs: additional keyword arguments are ignored
Returns:
.QuantumTape: the expanded tape
"""
expand_nonunitary_gen = create_expand_fn(
depth=10,
stop_at=not_tape | is_measurement | has_nopar | (has_gen & has_unitary_gen),
docstring=_expand_nonunitary_gen_doc,
)
_expand_invalid_trainable_doc = """Expand out a tape so that it supports differentiation
of requested operations.
This is achieved by decomposing all trainable operations that have
``Operation.grad_method=None`` until all resulting operations
have a defined gradient method, up to maximum depth ``depth``. Note that this
might not be possible, in which case the gradient rule will fail to apply.
Args:
tape (.QuantumTape): the input tape to expand
depth (int) : the maximum expansion depth
**kwargs: additional keyword arguments are ignored
Returns:
.QuantumTape: the expanded tape
"""
expand_invalid_trainable = create_expand_fn(
depth=10,
stop_at=not_tape | is_measurement | (~is_trainable) | has_grad_method,
docstring=_expand_invalid_trainable_doc,
)
_expand_invalid_trainable_doc_hadamard = """Expand out a tape so that it supports differentiation
of requested operations with the Hadamard test gradient.
This is achieved by decomposing all trainable operations that
are not in the Hadamard compatible list until all resulting operations
are in the list up to maximum depth ``depth``. Note that this
might not be possible, in which case the gradient rule will fail to apply.
Args:
tape (.QuantumTape): the input tape to expand
depth (int) : the maximum expansion depth
**kwargs: additional keyword arguments are ignored
Returns:
.QuantumTape: the expanded tape
"""
@qml.BooleanFn
def _is_hadamard_grad_compatible(obj):
"""Check if the operation is compatible with Hadamard gradient transform."""
return obj.name in hadamard_comp_list
hadamard_comp_list = [
"RX",
"RY",
"RZ",
"Rot",
"PhaseShift",
"U1",
"CRX",
"CRY",
"CRZ",
"IsingXX",
"IsingYY",
"IsingZZ",
]
expand_invalid_trainable_hadamard_gradient = create_expand_fn(
depth=10,
stop_at=not_tape
| is_measurement
| (~is_trainable)
| (_is_hadamard_grad_compatible & has_grad_method),
docstring=_expand_invalid_trainable_doc_hadamard,
)
@contextlib.contextmanager
def _custom_decomp_context(custom_decomps):
"""A context manager for applying custom decompositions of operations."""
# Creates an individual context
@contextlib.contextmanager
def _custom_decomposition(obj, fn):
# Covers the case where the user passes a string to indicate the Operator
if isinstance(obj, str):
obj = getattr(qml, obj)
original_decomp_method = obj.compute_decomposition
original_has_decomp_property = obj.has_decomposition
try:
# Explicitly set the new compute_decomposition method
obj.compute_decomposition = staticmethod(fn)
obj.has_decomposition = lambda obj: True
yield
finally:
obj.compute_decomposition = staticmethod(original_decomp_method)
obj.has_decomposition = original_has_decomp_property
# Loop through the decomposition dictionary and create all the contexts
try:
with contextlib.ExitStack() as stack:
for obj, fn in custom_decomps.items():
# We enter a new context for each decomposition the user passes
stack.enter_context(_custom_decomposition(obj, fn))
stack = stack.pop_all()
yield
finally:
stack.close()
[docs]def create_decomp_expand_fn(custom_decomps, dev, decomp_depth=10):
"""Creates a custom expansion function for a device that applies
a set of specified custom decompositions.
Args:
custom_decomps (Dict[Union(str, qml.operation.Operation), Callable]): Custom
decompositions to be applied by the device at runtime.
dev (pennylane.Device): A quantum device.
decomp_depth: The maximum depth of the expansion.
Returns:
Callable: A custom expansion function that a device can call to expand
its tapes within a context manager that applies custom decompositions.
**Example**
Suppose we would like a custom expansion function that decomposes all CNOTs
into CZs. We first define a decomposition function:
.. code-block:: python
def custom_cnot(wires):
return [
qml.Hadamard(wires=wires[1]),
qml.CZ(wires=[wires[0], wires[1]]),
qml.Hadamard(wires=wires[1])
]
We then create the custom function (passing a device, in order to pick up any
additional stopping criteria the expansion should have), and then register the
result as a custom function of the device:
>>> custom_decomps = {qml.CNOT : custom_cnot}
>>> expand_fn = qml.transforms.create_decomp_expand_fn(custom_decomps, dev)
>>> dev.custom_expand(expand_fn)
"""
custom_op_names = [op if isinstance(op, str) else op.__name__ for op in custom_decomps.keys()]
# Create a new expansion function; stop at things that do not have
# custom decompositions, or that satisfy the regular device stopping criteria
custom_fn = qml.transforms.create_expand_fn(
decomp_depth,
stop_at=qml.BooleanFn(lambda obj: obj.name not in custom_op_names),
device=dev,
)
# Finally, we set the device's custom_expand_fn to a new one that
# runs in a context where the decompositions have been replaced.
def custom_decomp_expand(self, circuit, max_expansion=decomp_depth):
with _custom_decomp_context(custom_decomps):
return custom_fn(circuit, max_expansion=max_expansion)
return custom_decomp_expand
def _create_decomp_preprocessing(custom_decomps, dev, decomp_depth=10):
"""Creates a custom preprocessing method for a device that applies
a set of specified custom decompositions.
Args:
custom_decomps (Dict[Union(str, qml.operation.Operation), Callable]): Custom
decompositions to be applied by the device at runtime.
dev (pennylane.devices.Device): A quantum device.
decomp_depth: The maximum depth of the expansion.
Returns:
Callable: A custom preprocessing method that a device can call to expand
its tapes.
**Example**
Suppose we would like a custom expansion function that decomposes all CNOTs
into CZs. We first define a decomposition function:
.. code-block:: python
def custom_cnot(wires):
return [
qml.Hadamard(wires=wires[1]),
qml.CZ(wires=[wires[0], wires[1]]),
qml.Hadamard(wires=wires[1])
]
We then create the custom function (passing a device, in order to pick up any
additional stopping criteria the expansion should have), and then register the
result as a custom function of the device:
>>> custom_decomps = {qml.CNOT : custom_cnot}
>>> new_preprocessing = _create_decomp_preprocessing(custom_decomps, dev)
>>> dev.preprocess = new_preprocessing
"""
def decomposer(op):
if isinstance(op, qml.ops.Controlled) and type(op.base) in custom_decomps:
op.base.compute_decomposition = custom_decomps[type(op.base)]
return op.decomposition()
if op.name in custom_decomps:
return custom_decomps[op.name](*op.data, wires=op.wires, **op.hyperparameters)
if type(op) in custom_decomps:
return custom_decomps[type(op)](*op.data, wires=op.wires, **op.hyperparameters)
return op.decomposition()
original_preprocess = dev.preprocess
# pylint: disable=cell-var-from-loop
def new_preprocess(execution_config=qml.devices.DefaultExecutionConfig):
program, config = original_preprocess(execution_config)
for container in program:
if container.transform == qml.devices.preprocess.decompose.transform:
container.kwargs["decomposer"] = decomposer
container.kwargs["max_expansion"] = decomp_depth
for cond in ["stopping_condition", "stopping_condition_shots"]:
# Devices that do not support native mid-circuit measurements
# will not have "stopping_condition_shots".
if cond in container.kwargs:
original_stopping_condition = container.kwargs[cond]
def stopping_condition(obj):
if obj.name in custom_decomps or type(obj) in custom_decomps:
return False
return original_stopping_condition(obj)
container.kwargs[cond] = stopping_condition
break
return program, config
return new_preprocess
[docs]@contextlib.contextmanager
def set_decomposition(custom_decomps, dev, decomp_depth=10):
"""Context manager for setting custom decompositions.
Args:
custom_decomps (Dict[Union(str, qml.operation.Operation), Callable]): Custom
decompositions to be applied by the device at runtime.
dev (pennylane.Device): A quantum device.
decomp_depth: The maximum depth of the expansion.
**Example**
Suppose we would like a custom expansion function that decomposes all CNOTs
into CZs. We first define a decomposition function:
.. code-block:: python
def custom_cnot(wires):
return [
qml.Hadamard(wires=wires[1]),
qml.CZ(wires=[wires[0], wires[1]]),
qml.Hadamard(wires=wires[1])
]
This context manager can be used to temporarily change a devices expansion
function to one that takes into account the custom decompositions.
.. code-block:: python
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit():
qml.CNOT(wires=[0, 1])
return qml.expval(qml.Z(0))
>>> print(qml.draw(circuit, level=None)())
0: ─╭●─┤ <Z>
1: ─╰X─┤
Now let's set up a context where the custom decomposition will be applied:
>>> with qml.transforms.set_decomposition({qml.CNOT : custom_cnot}, dev):
... print(qml.draw(circuit, wire_order=[0, 1])())
0: ────╭●────┤ <Z>
1: ──H─╰Z──H─┤
"""
if isinstance(dev, qml.devices.LegacyDeviceFacade):
dev = dev.target_device
original_custom_expand_fn = dev.custom_expand_fn
# Create a new expansion function; stop at things that do not have
# custom decompositions, or that satisfy the regular device stopping criteria
new_custom_expand_fn = create_decomp_expand_fn(
custom_decomps, dev, decomp_depth=decomp_depth
)
# Set the custom expand function within this context only
try:
dev.custom_expand(new_custom_expand_fn)
yield
finally:
dev.custom_expand_fn = original_custom_expand_fn
else:
original_preprocess = dev.preprocess
new_preprocess = _create_decomp_preprocessing(
custom_decomps, dev, decomp_depth=decomp_depth
)
try:
dev.preprocess = new_preprocess
yield
finally:
dev.preprocess = original_preprocess
_modules/pennylane/transforms/tape_expand
Download Python script
Download Notebook
View on GitHub