Source code for pennylane.ops.op_math.adjoint
# 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 adjoint of an operator.
"""
from functools import lru_cache, partial, wraps
from typing import Callable, overload
import pennylane as qml
from pennylane.compiler import compiler
from pennylane.math import conj, moveaxis, transpose
from pennylane.operation import Observable, Operation, Operator
from pennylane.queuing import QueuingManager
from pennylane.tape import make_qscript
from .symbolicop import SymbolicOp
@overload
def adjoint(fn: Operator, lazy: bool = True) -> Operator: ...
@overload
def adjoint(fn: Callable, lazy: bool = True) -> Callable: ...
[docs]def adjoint(fn, lazy=True):
"""Create the adjoint of an Operator or a function that applies the adjoint of the provided function.
:func:`~.qjit` compatible.
Args:
fn (function or :class:`~.operation.Operator`): A single operator or a quantum function that
applies quantum operations.
Keyword Args:
lazy=True (bool): If the transform is behaving lazily, all operations are wrapped in a ``Adjoint`` class
and handled later. If ``lazy=False``, operation-specific adjoint decompositions are first attempted.
Setting ``lazy=False`` is not supported when used with :func:`~.qjit`.
Returns:
(function or :class:`~.operation.Operator`): If an Operator is provided, returns an Operator that is the adjoint.
If a function is provided, returns a function with the same call signature that returns the Adjoint of the
provided function.
.. note::
The adjoint and inverse are identical for unitary gates, but not in general. For example, quantum channels and
observables may have different adjoint and inverse operators.
.. note::
When used with :func:`~.qjit`, this function only supports the Catalyst compiler.
See :func:`catalyst.adjoint` for more details.
Please see the Catalyst :doc:`quickstart guide <catalyst:dev/quick_start>`,
as well as the :doc:`sharp bits and debugging tips <catalyst:dev/sharp_bits>`
page for an overview of the differences between Catalyst and PennyLane.
.. note::
This function supports a batched operator:
>>> op = qml.adjoint(qml.RX([1, 2, 3], wires=0))
>>> qml.matrix(op).shape
(3, 2, 2)
But it doesn't support batching of operators:
>>> op = qml.adjoint([qml.RX(1, wires=0), qml.RX(2, wires=0)])
ValueError: The object [RX(1, wires=[0]), RX(2, wires=[0])] of type <class 'list'> is not callable.
This error might occur if you apply adjoint to a list of operations instead of a function or template.
.. seealso:: :class:`~.ops.op_math.Adjoint` and :meth:`.Operator.adjoint`
**Example**
The adjoint transform can accept a single operator.
>>> @qml.qnode(qml.device('default.qubit', wires=1))
... def circuit2(y):
... qml.adjoint(qml.RY(y, wires=0))
... return qml.expval(qml.Z(0))
>>> print(qml.draw(circuit2)("y"))
0: ──RY(y)†─┤ <Z>
>>> print(qml.draw(circuit2, level="device")(0.1))
0: ──RY(-0.10)─┤ <Z>
The adjoint transforms can also be used to apply the adjoint of
any quantum function. In this case, ``adjoint`` accepts a single function and returns
a function with the same call signature.
We can create a QNode that applies the ``my_ops`` function followed by its adjoint:
.. code-block:: python3
def my_ops(a, wire):
qml.RX(a, wires=wire)
qml.SX(wire)
dev = qml.device('default.qubit', wires=1)
@qml.qnode(dev)
def circuit(a):
my_ops(a, wire=0)
qml.adjoint(my_ops)(a, wire=0)
return qml.expval(qml.Z(0))
Printing this out, we can see that the inverse quantum
function has indeed been applied:
>>> print(qml.draw(circuit)(0.2))
0: ──RX(0.20)──SX──SX†──RX(0.20)†─┤ <Z>
**Example with compiler**
The adjoint used in a compilation context can be applied on control flow.
.. code-block:: python
dev = qml.device("lightning.qubit", wires=1)
@qml.qjit
@qml.qnode(dev)
def workflow(theta, n, wires):
def func():
@qml.for_loop(0, n, 1)
def loop_fn(i):
qml.RX(theta, wires=wires)
loop_fn()
qml.adjoint(func)()
return qml.probs()
>>> workflow(jnp.pi/2, 3, 0)
array([0.5, 0.5])
.. warning::
The Catalyst adjoint function does not support performing the adjoint
of quantum functions that contain mid-circuit measurements.
.. details::
:title: Lazy Evaluation
When ``lazy=False``, the function first attempts operation-specific decomposition of the
adjoint via the :meth:`.Operator.adjoint` method. Only if an Operator doesn't have
an :meth:`.Operator.adjoint` method is the object wrapped with the :class:`~.ops.op_math.Adjoint`
wrapper class.
>>> qml.adjoint(qml.Z(0), lazy=False)
Z(0)
>>> qml.adjoint(qml.RX, lazy=False)(1.0, wires=0)
RX(-1.0, wires=[0])
>>> qml.adjoint(qml.S, lazy=False)(0)
Adjoint(S)(wires=[0])
"""
if active_jit := compiler.active_compiler():
available_eps = compiler.AvailableCompilers.names_entrypoints
ops_loader = available_eps[active_jit]["ops"].load()
return ops_loader.adjoint(fn, lazy=lazy)
return create_adjoint_op(fn, lazy)
def create_adjoint_op(fn, lazy):
"""Main logic for qml.adjoint, but allows bypassing the compiler dispatch if needed."""
if qml.math.is_abstract(fn):
return Adjoint(fn)
if isinstance(fn, Operator):
return Adjoint(fn) if lazy else _single_op_eager(fn, update_queue=True)
if callable(fn):
if qml.capture.enabled():
return _capture_adjoint_transform(fn, lazy=lazy)
return _adjoint_transform(fn, lazy=lazy)
raise ValueError(
f"The object {fn} of type {type(fn)} is not callable. "
"This error might occur if you apply adjoint to a list "
"of operations instead of a function or template."
)
@lru_cache # only create the first time requested
def _get_adjoint_qfunc_prim():
"""See capture/explanations.md : Higher Order primitives for more information on this code."""
# if capture is enabled, jax should be installed
import jax # pylint: disable=import-outside-toplevel
adjoint_prim = jax.core.Primitive("adjoint_transform")
adjoint_prim.multiple_results = True
@adjoint_prim.def_impl
def _(*args, jaxpr, lazy, n_consts):
consts = args[:n_consts]
args = args[n_consts:]
with qml.queuing.AnnotatedQueue() as q:
jax.core.eval_jaxpr(jaxpr, consts, *args)
ops, _ = qml.queuing.process_queue(q)
for op in reversed(ops):
adjoint(op, lazy=lazy)
return []
@adjoint_prim.def_abstract_eval
def _(*_, **__):
return []
return adjoint_prim
def _capture_adjoint_transform(qfunc: Callable, lazy=True) -> Callable:
"""Capture compatible way of performing an adjoint transform."""
# note that this logic is tested in `tests/capture/test_nested_plxpr.py`
import jax # pylint: disable=import-outside-toplevel
adjoint_prim = _get_adjoint_qfunc_prim()
@wraps(qfunc)
def new_qfunc(*args, **kwargs):
jaxpr = jax.make_jaxpr(partial(qfunc, **kwargs))(*args)
adjoint_prim.bind(
*jaxpr.consts, *args, jaxpr=jaxpr.jaxpr, lazy=lazy, n_consts=len(jaxpr.consts)
)
return new_qfunc
def _adjoint_transform(qfunc: Callable, lazy=True) -> Callable:
# default adjoint transform when capture is not enabled.
@wraps(qfunc)
def wrapper(*args, **kwargs):
qscript = make_qscript(qfunc)(*args, **kwargs)
if lazy:
adjoint_ops = [Adjoint(op) for op in reversed(qscript.operations)]
else:
adjoint_ops = [_single_op_eager(op) for op in reversed(qscript.operations)]
return adjoint_ops[0] if len(adjoint_ops) == 1 else adjoint_ops
return wrapper
def _single_op_eager(op: Operator, update_queue: bool = False) -> Operator:
if op.has_adjoint:
adj = op.adjoint()
if update_queue:
QueuingManager.remove(op)
QueuingManager.append(adj)
return adj
return Adjoint(op)
# pylint: disable=too-many-public-methods
[docs]class Adjoint(SymbolicOp):
"""
The Adjoint of an operator.
Args:
base (~.operation.Operator): The operator that is adjointed.
.. seealso:: :func:`~.adjoint`, :meth:`~.operation.Operator.adjoint`
This is a *developer*-facing class, and the :func:`~.adjoint` transform should be used to
construct instances
of this class.
**Example**
>>> op = Adjoint(qml.S(0))
>>> op.name
'Adjoint(S)'
>>> qml.matrix(op)
array([[1.-0.j, 0.-0.j],
[0.-0.j, 0.-1.j]])
>>> qml.generator(Adjoint(qml.RX(1.0, wires=0)))
(X(0), 0.5)
>>> Adjoint(qml.RX(1.234, wires=0)).data
(1.234,)
.. details::
:title: Developer Details
This class mixes in parent classes based on the inheritance tree of the provided ``Operator``.
For example, when provided an ``Operation``, the instance will inherit from ``Operation`` and
the ``AdjointOperation`` mixin.
>>> op = Adjoint(qml.RX(1.234, wires=0))
>>> isinstance(op, qml.operation.Operation)
True
>>> isinstance(op, AdjointOperation)
True
>>> op.grad_method
'A'
If the base class is an ``Observable`` instead, the ``Adjoint`` will be an ``Observable`` as
well.
>>> op = Adjoint(1.0 * qml.X(0))
>>> isinstance(op, qml.operation.Observable)
True
>>> isinstance(op, qml.operation.Operation)
False
>>> Adjoint(qml.X(0)) @ qml.Y(1)
(Adjoint(X(0))) @ Y(1)
"""
def _flatten(self):
return (self.base,), tuple()
@classmethod
def _unflatten(cls, data, _):
return cls(data[0])
# pylint: disable=unused-argument
def __new__(cls, base=None, id=None):
"""Returns an uninitialized type with the necessary mixins.
If the ``base`` is an ``Operation``, this will return an instance of ``AdjointOperation``.
If ``Observable`` but not ``Operation``, it will be ``AdjointObs``.
And if both, it will be an instance of ``AdjointOpObs``.
"""
if isinstance(base, Operation):
if isinstance(base, Observable):
return object.__new__(AdjointOpObs)
# not an observable
return object.__new__(AdjointOperation)
if isinstance(base, Observable):
return object.__new__(AdjointObs)
return object.__new__(Adjoint)
def __init__(self, base=None, id=None):
self._name = f"Adjoint({base.name})"
super().__init__(base, id=id)
def __repr__(self):
return f"Adjoint({self.base})"
@property
def ndim_params(self):
return self.base.ndim_params
[docs] def label(self, decimals=None, base_label=None, cache=None):
base_label = self.base.label(decimals, base_label, cache=cache)
return (
f"({base_label})†"
if self.base.arithmetic_depth > 0 and len(base_label) > 1
else f"{base_label}†"
)
[docs] def matrix(self, wire_order=None):
if isinstance(self.base, qml.ops.Hamiltonian):
base_matrix = qml.matrix(self.base, wire_order=wire_order)
else:
base_matrix = self.base.matrix(wire_order=wire_order)
return moveaxis(conj(base_matrix), -2, -1)
# pylint: disable=arguments-differ
[docs] def sparse_matrix(self, wire_order=None, format="csr"):
base_matrix = self.base.sparse_matrix(wire_order=wire_order)
return transpose(conj(base_matrix)).asformat(format=format)
# pylint: disable=arguments-renamed, invalid-overridden-method
@property
def has_decomposition(self):
return self.base.has_adjoint or self.base.has_decomposition
[docs] def decomposition(self):
if self.base.has_adjoint:
return [self.base.adjoint()]
base_decomp = self.base.decomposition()
return [Adjoint(op) for op in reversed(base_decomp)]
[docs] def eigvals(self):
# Cannot define ``compute_eigvals`` because Hermitian only defines ``eigvals``
return conj(self.base.eigvals())
# pylint: disable=arguments-renamed, invalid-overridden-method
@property
def has_diagonalizing_gates(self):
return self.base.has_diagonalizing_gates
# pylint: disable=arguments-renamed, invalid-overridden-method
@property
def has_adjoint(self):
return True
[docs] def simplify(self):
base = self.base.simplify()
if self.base.has_adjoint:
return base.adjoint().simplify()
return Adjoint(base=base.simplify())
# pylint: disable=no-member
class AdjointOperation(Adjoint, Operation):
"""This mixin class is dynamically added to an ``Adjoint`` instance if the provided base class
is an ``Operation``.
.. warning::
This mixin class should never be initialized independent of ``Adjoint``.
Overriding the dunder method ``__new__`` in ``Adjoint`` allows us to customize the creation of
an instance and dynamically add in parent classes.
.. note:: Once the ``Operation`` class does not contain any unique logic any more, this mixin
class can be removed.
"""
def __new__(cls, *_, **__):
return object.__new__(cls)
@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
def single_qubit_rot_angles(self):
omega, theta, phi = self.base.single_qubit_rot_angles()
return [-phi, -theta, -omega]
@property
def grad_method(self):
return self.base.grad_method
# pylint: disable=missing-function-docstring
@property
def grad_recipe(self):
return self.base.grad_recipe
@property
def parameter_frequencies(self):
return self.base.parameter_frequencies
# pylint: disable=arguments-renamed, invalid-overridden-method
@property
def has_generator(self):
return self.base.has_generator
def generator(self):
return -1 * self.base.generator()
class AdjointObs(Adjoint, Observable):
"""A child of :class:`~.Adjoint` that also inherits from :class:`~.Observable`."""
def __new__(cls, *_, **__):
return object.__new__(cls)
# pylint: disable=too-many-ancestors
class AdjointOpObs(AdjointOperation, Observable):
"""A child of :class:`~.AdjointOperation` that also inherits from :class:`~.Observable."""
def __new__(cls, *_, **__):
return object.__new__(cls)
_modules/pennylane/ops/op_math/adjoint
Download Python script
Download Notebook
View on GitHub