From d62d80277fe432678625cba21bed81f17745a7ef Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 7 Mar 2024 15:07:45 -0500 Subject: [PATCH 1/8] Remove deprecated old transform functionalities --- doc/development/deprecations.rst | 17 +- doc/releases/changelog-dev.md | 5 + pennylane/__init__.py | 3 - pennylane/gradients/__init__.py | 3 +- pennylane/gradients/gradient_transform.py | 153 ----- pennylane/gradients/hessian_transform.py | 158 ----- pennylane/ops/functions/eigvals.py | 6 +- pennylane/ops/functions/matrix.py | 8 +- pennylane/transforms/__init__.py | 23 +- pennylane/transforms/batch_transform.py | 425 ------------- pennylane/transforms/op_transforms.py | 522 ---------------- pennylane/transforms/qfunc_transforms.py | 436 -------------- pennylane/transforms/zx/converter.py | 6 +- tests/ops/functions/test_eigvals.py | 4 +- tests/ops/functions/test_matrix.py | 8 +- tests/transforms/test_batch_transform.py | 692 ---------------------- tests/transforms/test_op_transform.py | 585 ------------------ tests/transforms/test_qfunc_transform.py | 519 ---------------- tests/transforms/test_zx.py | 4 +- 19 files changed, 35 insertions(+), 3542 deletions(-) delete mode 100644 pennylane/transforms/op_transforms.py delete mode 100644 pennylane/transforms/qfunc_transforms.py delete mode 100644 tests/transforms/test_op_transform.py delete mode 100644 tests/transforms/test_qfunc_transform.py diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst index 3e8022b6369..ced98be0505 100644 --- a/doc/development/deprecations.rst +++ b/doc/development/deprecations.rst @@ -43,14 +43,6 @@ Pending deprecations - Deprecated in v0.35 - Will raise an error in v0.36 -* ``single_tape_transform``, ``batch_transform``, ``qfunc_transform``, and ``op_transform`` are - deprecated. Instead switch to using the new ``qml.transform`` function. Please refer to - `the transform docs `_ - to see how this can be done. - - - Deprecated in v0.34 - - Will be removed in v0.36 - * ``PauliWord`` and ``PauliSentence`` no longer use ``*`` for matrix and tensor products, but instead use ``@`` to conform with the PennyLane convention. @@ -64,6 +56,15 @@ Pending deprecations Completed deprecation cycles ---------------------------- +* ``single_tape_transform``, ``batch_transform``, ``qfunc_transform``, ``op_transform``, + `` gradient_transform`` and ``hessian_transform`` are deprecated. Instead switch to using the new + ``qml.transform`` function. Please refer to + `the transform docs `_ + to see how this can be done. + + - Deprecated in v0.34 + - Removed in v0.36 + * ``MeasurementProcess.name`` and ``MeasurementProcess.data`` have been deprecated, as they contain dummy values that are no longer needed. diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index df6b1113b6b..37f889c0f46 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -62,6 +62,11 @@ * The contents of ``qml.interfaces`` is moved inside ``qml.workflow``. The old import path no longer exists. [(#5329)](https://github.com/PennyLaneAI/pennylane/pull/5329) +* ``single_tape_transform``, ``batch_transform``, ``qfunc_transform``, ``op_transform``, `` gradient_transform`` + and ``hessian_transform`` are removed. Instead, switch to using the new ``qml.transform`` function. Please refer to + `the transform docs `_ + to see how this can be done. +

Deprecations 👋

* ``qml.load`` is deprecated. Instead, please use the functions outlined in the *Importing workflows* quickstart guide, such as ``qml.from_qiskit``. diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 280f20908ce..6eed9004466 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -90,9 +90,6 @@ compile, defer_measurements, dynamic_one_shot, - qfunc_transform, - op_transform, - single_tape_transform, quantum_monte_carlo, apply_controlled_Q, commutation_dag, diff --git a/pennylane/gradients/__init__.py b/pennylane/gradients/__init__.py index 0e6b6a8f459..a008854f7f6 100644 --- a/pennylane/gradients/__init__.py +++ b/pennylane/gradients/__init__.py @@ -338,8 +338,7 @@ def my_custom_gradient(tape: qml.tape.QuantumTape, **kwargs) -> (Sequence[qml.ta from . import pulse_gradient from . import pulse_gradient_odegen -from .gradient_transform import gradient_transform, SUPPORTED_GRADIENT_KWARGS -from .hessian_transform import hessian_transform +from .gradient_transform import SUPPORTED_GRADIENT_KWARGS from .finite_difference import finite_diff, finite_diff_coeffs from .parameter_shift import param_shift from .parameter_shift_cv import param_shift_cv diff --git a/pennylane/gradients/gradient_transform.py b/pennylane/gradients/gradient_transform.py index f7d604f3324..6b3d9a9ef58 100644 --- a/pennylane/gradients/gradient_transform.py +++ b/pennylane/gradients/gradient_transform.py @@ -441,156 +441,3 @@ def _reshape(x): if not cjac_is_tuple: return tuple(tdot(qml.math.stack(q), qml.math.stack(cjac)) for q in qjac) return tuple(tuple(tdot(qml.math.stack(q), c) for c in cjac if c is not None) for q in qjac) - - -class gradient_transform(qml.batch_transform): # pragma: no cover - """Decorator for defining quantum gradient transforms. - - Quantum gradient transforms are a specific case of :class:`~.batch_transform`. - All quantum gradient transforms accept a tape, and output - a batch of tapes to be independently executed on a quantum device, alongside - a post-processing function that returns the result. - - Args: - expand_fn (function): An expansion function (if required) to be applied to the - input tape before the gradient computation takes place. If not provided, - the default expansion function simply expands all operations that - have ``Operation.grad_method=None`` until all resulting operations - have a defined gradient method. - differentiable (bool): Specifies whether the gradient transform is differentiable or - not. A transform may be non-differentiable if it does not use an - autodiff framework for its tensor manipulations. In such a case, setting - ``differentiable=False`` instructs the decorator - to mark the output as 'constant', reducing potential overhead. - hybrid (bool): Specifies whether classical processing inside a QNode - should be taken into account when transforming a QNode. - - - If ``True``, and classical processing is detected and this - option is set to ``True``, the Jacobian of the classical - processing will be computed and included. When evaluated, the - returned Jacobian will be with respect to the QNode arguments. - - - If ``False``, any internal QNode classical processing will be - **ignored**. When evaluated, the returned Jacobian will be with - respect to the **gate** arguments, and not the QNode arguments. - - Supported gradient transforms must be of the following form: - - .. code-block:: python - - @gradient_transform - def my_custom_gradient(tape, argnum=None, **kwargs): - ... - return gradient_tapes, processing_fn - - where: - - - ``tape`` (*QuantumTape*): the input quantum tape to compute the gradient of - - - ``argnum`` (*int* or *list[int]* or *None*): Which trainable parameters of the tape - to differentiate with respect to. If not provided, the derivatives with respect to all - trainable inputs of the tape should be returned (``tape.trainable_params``). - - - ``gradient_tapes`` (*list[QuantumTape]*): is a list of output tapes to be evaluated. - If this list is empty, no quantum evaluations will be made. - - - ``processing_fn`` is a processing function to be applied to the output of the evaluated - ``gradient_tapes``. It should accept a list of numeric results with length ``len(gradient_tapes)``, - and return the Jacobian matrix. - - Once defined, the quantum gradient transform can be used as follows: - - >>> gradient_tapes, processing_fn = my_custom_gradient(tape, *gradient_kwargs) - >>> res = execute(tapes, dev, interface="autograd", gradient_fn=qml.gradients.param_shift) - >>> jacobian = processing_fn(res) - - Alternatively, gradient transforms can be applied directly to QNodes, - in which case the execution is implicit: - - >>> fn = my_custom_gradient(qnode, *gradient_kwargs) - >>> fn(weights) # transformed function takes the same arguments as the QNode - 1.2629730888100839 - - .. note:: - - The input tape might have parameters of various types, including - NumPy arrays, JAX Arrays, and TensorFlow and PyTorch tensors. - - If the gradient transform is written in a autodiff-compatible manner, either by - using a framework such as Autograd or TensorFlow, or by using ``qml.math`` for - tensor manipulation, then higher-order derivatives will also be supported. - - Alternatively, you may use the ``tape.unwrap()`` context manager to temporarily - convert all tape parameters to NumPy arrays and floats: - - >>> with tape.unwrap(): - ... params = tape.get_parameters() # list of floats - """ - - def __repr__(self): - return f"" # pylint: disable=no-member - - def __init__( - self, transform_fn, expand_fn=expand_invalid_trainable, differentiable=True, hybrid=True - ): - self.hybrid = hybrid - super().__init__(transform_fn, expand_fn=expand_fn, differentiable=differentiable) - - def default_qnode_wrapper(self, qnode, targs, tkwargs): # pylint: disable=too-many-statements - # Here, we overwrite the QNode execution wrapper in order - # to take into account that classical processing may be present - # inside the QNode. - hybrid = tkwargs.pop("hybrid", self.hybrid) - _wrapper = super().default_qnode_wrapper(qnode, targs, tkwargs) - - def jacobian_wrapper( - *args, **kwargs - ): # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements - argnums = tkwargs.get("argnums", None) - - interface = qml.math.get_interface(*args) - trainable_params = qml.math.get_trainable_indices(args) - - if interface == "jax" and tkwargs.get("argnum", None): - raise qml.QuantumFunctionError( - "argnum does not work with the Jax interface. You should use argnums instead." - ) - - if interface == "jax" and not trainable_params: - if argnums is None: - argnums_ = [0] - - else: - argnums_ = [argnums] if isinstance(argnums, int) else argnums - - params = qml.math.jax_argnums_to_tape_trainable( - qnode, argnums_, self.expand_fn, args, kwargs - ) - argnums_ = qml.math.get_trainable_indices(params) - kwargs["argnums"] = argnums_ - - elif not trainable_params: - warnings.warn( - "Attempted to compute the gradient of a QNode with no trainable parameters. " - "If this is unintended, please add trainable parameters in accordance with " - "the chosen auto differentiation framework." - ) - return () - - qjac = _wrapper(*args, **kwargs) - - if not hybrid: - return qjac - - kwargs.pop("shots", False) - - # Special case where we apply a Jax transform (jacobian e.g.) on the gradient transform and argnums are - # defined on the outer transform and therefore on the args. - argnum_cjac = trainable_params or argnums if interface == "jax" else None - cjac = qml.gradients.classical_jacobian( - qnode, argnum=argnum_cjac, expand_fn=self.expand_fn - )(*args, **kwargs) - - return _contract_qjac_with_cjac(qjac, cjac, qnode.tape) # pragma: no cover - - return jacobian_wrapper diff --git a/pennylane/gradients/hessian_transform.py b/pennylane/gradients/hessian_transform.py index b5819a7b9df..1273debf570 100644 --- a/pennylane/gradients/hessian_transform.py +++ b/pennylane/gradients/hessian_transform.py @@ -64,161 +64,3 @@ def _process_jacs(jac, qhess): hess.append(qh) return tuple(hess) if len(hess) > 1 else hess[0] - - -class hessian_transform(qml.batch_transform): # pragma: no cover - """Decorator for defining quantum Hessian transforms. - - Quantum Hessian transforms are a specific case of :class:`~.batch_transform`s, - similar to the :class:`~.gradient_transform`. Hessian transforms compute the - second derivative of a quantum function. - All quantum Hessian transforms accept a tape, and output a batch of tapes to - be independently executed on a quantum device, alongside a post-processing - function to return the result. - - Args: - expand_fn (function): An expansion function (if required) to be applied to the - input tape before the Hessian computation takes place. If not provided, - the default expansion function simply expands all operations that - have ``Operation.grad_method=None`` until all resulting operations - have a defined gradient method. - differentiable (bool): Specifies whether the Hessian transform is differentiable - or not. A transform may be non-differentiable if it does not use an autodiff - framework for its tensor manipulations. In such a case, setting - ``differentiable=False`` instructs the decorator to mark the output as - 'constant', reducing potential overhead. - hybrid (bool): Specifies whether classical processing inside a QNode - should be taken into account when transforming a QNode. - - - If ``True``, and classical processing is detected, the Jacobian of the - classical processing will be computed and included. When evaluated, the - returned Hessian will be with respect to the QNode arguments. - - - If ``False``, any internal QNode classical processing will be **ignored**. - When evaluated, the returned Hessian will be with respect to the **gate** - arguments, and not the QNode arguments. - - Supported Hessian transforms must be of the following form: - - .. code-block:: python - - @hessian_transform - def my_custom_hessian(tape, **kwargs): - ... - return hessian_tapes, processing_fn - - where: - - - ``tape`` (*QuantumTape*): the input quantum tape to compute the Hessian of - - - ``hessian_tapes`` (*list[QuantumTape]*): is a list of output tapes to be - evaluated. If this list is empty, no quantum evaluations will be made. - - - ``processing_fn`` is a processing function to be applied to the output of the - evaluated ``hessian_tapes``. It should accept a list of numeric results with - length ``len(hessian_tapes)``, and return the Hessian matrix. - - Once defined, the quantum Hessian transform can be used as follows: - - >>> hessian_tapes, processing_fn = my_custom_hessian(tape, *hessian_kwargs) - >>> res = execute(tapes, dev, interface="autograd", gradient_fn=qml.gradients.param_shift) - >>> jacobian = processing_fn(res) - - Alternatively, Hessian transforms can be applied directly to QNodes, in which case - the execution is implicit: - - >>> fn = my_custom_hessian(qnode, *hessian_kwargs) - >>> fn(weights) # transformed function takes the same arguments as the QNode - 1.2629730888100839 - - .. note:: - - The input tape might have parameters of various types, including NumPy arrays, - JAX Arrays, and TensorFlow and PyTorch tensors. - - If the Hessian transform is written in a autodiff-compatible manner, either by - using a framework such as Autograd or TensorFlow, or by using ``qml.math`` for - tensor manipulation, then higher-order derivatives will also be supported. - - Alternatively, you may use the ``tape.unwrap()`` context manager to temporarily - convert all tape parameters to NumPy arrays and floats: - - >>> with tape.unwrap(): - ... params = tape.get_parameters() # list of floats - """ - - def __init__( - self, transform_fn, expand_fn=expand_invalid_trainable, differentiable=True, hybrid=True - ): - self.hybrid = hybrid - super().__init__(transform_fn, expand_fn=expand_fn, differentiable=differentiable) - - def default_qnode_wrapper(self, qnode, targs, tkwargs): - # Here, we overwrite the QNode execution wrapper in order to take into account - # that classical processing may be present inside the QNode. - hybrid = tkwargs.pop("hybrid", self.hybrid) - argnums = tkwargs.get("argnums", None) - - old_interface = qnode.interface - - _wrapper = super().default_qnode_wrapper(qnode, targs, tkwargs) - cjac_fn = qml.gradients.classical_jacobian(qnode, argnum=argnums, expand_fn=self.expand_fn) - - def hessian_wrapper(*args, **kwargs): # pylint: disable=too-many-branches - if argnums is not None: - argnums_ = [argnums] if isinstance(argnums, int) else argnums - - params = qml.math.jax_argnums_to_tape_trainable( - qnode, argnums_, self.expand_fn, args, kwargs - ) - argnums_ = qml.math.get_trainable_indices(params) - kwargs["argnums"] = argnums_ - - if not qml.math.get_trainable_indices(args) and not argnums: - warnings.warn( - "Attempted to compute the Hessian of a QNode with no trainable parameters. " - "If this is unintended, please add trainable parameters in accordance with " - "the chosen auto differentiation framework." - ) - return () - - qhess = _wrapper(*args, **kwargs) - - if old_interface == "auto": - qnode.interface = "auto" - - if not hybrid: - return qhess - - if len(qnode.tape.measurements) == 1: - qhess = (qhess,) - - kwargs.pop("shots", False) - - if argnums is None and qml.math.get_interface(*args) == "jax": - cjac = qml.gradients.classical_jacobian( - qnode, argnum=qml.math.get_trainable_indices(args), expand_fn=self.expand_fn - )(*args, **kwargs) - else: - cjac = cjac_fn(*args, **kwargs) - - has_single_arg = False - if not isinstance(cjac, tuple): - has_single_arg = True - cjac = (cjac,) - - # The classical Jacobian for each argument has shape: - # (# gate_args, *qnode_arg_shape) - # The Jacobian needs to be contracted twice with the quantum Hessian of shape: - # (*qnode_output_shape, # gate_args, # gate_args) - # The result should then have the shape: - # (*qnode_output_shape, *qnode_arg_shape, *qnode_arg_shape) - hessians = [] - for jac in cjac: - if jac is not None: - hess = _process_jacs(jac, qhess) - hessians.append(hess) - - return hessians[0] if has_single_arg else tuple(hessians) - - return hessian_wrapper diff --git a/pennylane/ops/functions/eigvals.py b/pennylane/ops/functions/eigvals.py index 1146c2371d7..8403683b88f 100644 --- a/pennylane/ops/functions/eigvals.py +++ b/pennylane/ops/functions/eigvals.py @@ -22,7 +22,7 @@ import scipy import pennylane as qml -from pennylane.transforms.op_transforms import OperationTransformError +from pennylane.transforms import TransformError from pennylane import transform from pennylane.typing import TensorLike @@ -111,9 +111,7 @@ def circuit(theta): """ if not isinstance(op, qml.operation.Operator): if not isinstance(op, (qml.tape.QuantumScript, qml.QNode)) and not callable(op): - raise OperationTransformError( - "Input is not an Operator, tape, QNode, or quantum function" - ) + raise TransformError("Input is not an Operator, tape, QNode, or quantum function") return _eigvals_tranform(op, k=k, which=which) if isinstance(op, qml.Hamiltonian): diff --git a/pennylane/ops/functions/matrix.py b/pennylane/ops/functions/matrix.py index f1455aec37c..7961d633fb4 100644 --- a/pennylane/ops/functions/matrix.py +++ b/pennylane/ops/functions/matrix.py @@ -20,7 +20,7 @@ from warnings import warn import pennylane as qml -from pennylane.transforms.op_transforms import OperationTransformError +from pennylane.transforms import TransformError from pennylane import transform from pennylane.typing import TensorLike from pennylane.operation import Operator @@ -200,9 +200,7 @@ def circuit(): if wire_order is None: warn(_wire_order_none_warning, qml.PennyLaneDeprecationWarning) else: - raise OperationTransformError( - "Input is not an Operator, tape, QNode, or quantum function" - ) + raise TransformError("Input is not an Operator, tape, QNode, or quantum function") return _matrix_transform(op, wire_order=wire_order) if isinstance(op, qml.operation.Tensor) and wire_order is not None: @@ -225,7 +223,7 @@ def _matrix_transform( raise qml.operation.MatrixUndefinedError if wire_order and not set(tape.wires).issubset(wire_order): - raise OperationTransformError( + raise TransformError( f"Wires in circuit {list(tape.wires)} are inconsistent with " f"those in wire_order {list(wire_order)}" ) diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index b4fb95285b4..ca16ac1b180 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -19,6 +19,8 @@ Custom transforms ----------------- +.. _transforms_custom_transforms: + :func:`qml.transform ` can be used to define custom transformations that work with PennyLane QNodes; such transformations can map a circuit to one or many new circuits alongside associated classical post-processing. @@ -163,21 +165,6 @@ ~transforms.core.transform_dispatcher ~transforms.core.transform_program -Old transforms framework ------------------------- - -These utility functions were previously used to create transforms in PennyLane and are now -deprecated. It is now recommended to use :class:`qml.transform ` -for the creation of custom transforms. - -.. autosummary:: - :toctree: api - - ~single_tape_transform - ~batch_transform - ~qfunc_transform - ~op_transform - Transforming circuits --------------------- @@ -286,9 +273,7 @@ def circuit(x, y): """ # Import the decorators first to prevent circular imports when used in other transforms from .core import transform, TransformError -from .batch_transform import batch_transform, map_batch_transform -from .qfunc_transforms import make_tape, single_tape_transform, qfunc_transform -from .op_transforms import op_transform +from .batch_transform import map_batch_transform from .batch_params import batch_params from .batch_input import batch_input from .batch_partial import batch_partial @@ -337,3 +322,5 @@ def circuit(x, y): from .transpile import transpile from .zx import to_zx, from_zx from .broadcast_expand import broadcast_expand + +from pennylane.tape import make_qscript as make_tape diff --git a/pennylane/transforms/batch_transform.py b/pennylane/transforms/batch_transform.py index ac40ccf8f91..6f8fc968e4a 100644 --- a/pennylane/transforms/batch_transform.py +++ b/pennylane/transforms/batch_transform.py @@ -29,431 +29,6 @@ QuantumTapeBatch = Tuple[qml.tape.QuantumScript] -class batch_transform: - r"""Class for registering a tape transform that takes a tape, and outputs - a batch of tapes to be independently executed on a quantum device. - - .. warning:: - - Use of ``batch_transform`` to create a custom transform is deprecated. Instead - switch to using the new :func:`transform` function. Follow the instructions - `here `_ - for further details - - Examples of such transforms include quantum gradient shift rules (such - as finite-differences and the parameter-shift rule) and metrics such as - the quantum Fisher information matrix. - - Args: - transform_fn (function): The function to register as the batch tape transform. - It can have an arbitrary number of arguments, but the first argument - **must** be the input tape. - expand_fn (function): An expansion function (if required) to be applied to the - input tape before the transformation takes place. - It **must** take the same input arguments as ``transform_fn``. - differentiable (bool): Specifies whether the transform is differentiable or - not. A transform may be non-differentiable for several reasons: - - - It does not use an autodiff framework for its tensor manipulations; - - It returns a non-differentiable or non-numeric quantity, such as - a boolean, string, or integer. - - In such a case, setting ``differentiable=False`` instructs the decorator - to mark the output as 'constant', reducing potential overhead. - - **Example** - - A valid batch tape transform is a function that satisfies the following: - - - The first argument must be a tape. - - - Depending on the structure of this input tape, various quantum operations, functions, - and templates may be called. - - - Any internal classical processing should use the ``qml.math`` module to ensure - the transform is differentiable. - - - The transform should return a tuple containing: - - * Multiple transformed tapes to be executed on a device. - * A classical processing function for post-processing the executed tape results. - This processing function should have the signature ``f(list[tensor_like]) → Any``. - If ``None``, no classical processing is applied to the results. - - For example: - - .. code-block:: python - - @qml.batch_transform - def my_transform(tape, a, b): - '''Generates two tapes, one with all RX replaced with RY, - and the other with all RX replaced with RZ.''' - - ops1 = [] - ops2 = [] - - # loop through all operations on the input tape - for op in tape.operations: - if op.name == "RX": - wires = op.wires - param = op.parameters[0] - - ops1.append(qml.RY(a * qml.math.abs(param), wires=wires)) - ops2.append(qml.RZ(b * qml.math.abs(param), wires=wires)) - else: - ops1.append(op) - ops2.append(op) - - tape1 = qml.tape.QuantumTape(ops1, tape.measurements) - tape2 = qml.tape.QuantumTape(ops2, tape.measurements) - - def processing_fn(results): - return qml.math.sum(qml.math.stack(results)) - - return [tape1, tape2], processing_fn - - We can apply this transform to a quantum tape: - - >>> ops = [qml.Hadamard(wires=0), qml.RX(-0.5, wires=0)] - >>> tape = qml.tape.QuantumTape(ops, [qml.expval(qml.X(0))]) - >>> tapes, fn = my_transform(tape, 0.65, 2.5) - >>> print(qml.drawer.tape_text(tapes[0], decimals=2)) - 0: ──H──RY(0.33)─┤ - >>> print(qml.drawer.tape_text(tapes[1], decimals=2)) - 0: ──H──RZ(1.25)─┤ - - We can execute these tapes manually: - - >>> dev = qml.device("default.qubit", wires=1) - >>> res = qml.execute(tapes, dev, interface="autograd", gradient_fn=qml.gradients.param_shift) - >>> print(res) - [0.9476507264148154, 0.31532236239526856] - - Applying the processing function, we retrieve the end result of the transform: - - >>> print(fn(res)) - 1.2629730888100839 - - Alternatively, we may also transform a QNode directly, using either - decorator syntax: - - >>> @my_transform(0.65, 2.5) - ... @qml.qnode(dev) - ... def circuit(x): - ... qml.Hadamard(wires=0) - ... qml.RX(x, wires=0) - ... return qml.expval(qml.X(0)) - >>> print(circuit(-0.5)) - 1.2629730888100839 - - or by transforming an existing QNode: - - >>> @qml.qnode(dev) - ... def circuit(x): - ... qml.Hadamard(wires=0) - ... qml.RX(x, wires=0) - ... return qml.expval(qml.X(0)) - >>> circuit = my_transform(circuit, 0.65, 2.5) - >>> print(circuit(-0.5)) - 1.2629730888100839 - - Batch tape transforms are fully differentiable: - - >>> x = np.array(-0.5, requires_grad=True) - >>> gradient = qml.grad(circuit)(x) - >>> print(gradient) - 2.5800122591960153 - - .. details:: - :title: Usage Details - - **Expansion functions** - - Tape expansion, decomposition, or manipulation may always be - performed within the custom batch transform. However, by specifying - a separate expansion function, PennyLane will be possible to access - this separate expansion function where needed via - - >>> my_transform.expand_fn - - The provided ``expand_fn`` must have the same input arguments as - ``transform_fn`` and return a ``tape``. Following the example above: - - .. code-block:: python - - def expand_fn(tape, a, b): - stopping_crit = lambda obj: obj.name!="PhaseShift" - return tape.expand(depth=10, stop_at=stopping_crit) - - my_transform = batch_transform(my_transform, expand_fn) - - Note that: - - - the transform arguments ``a`` and ``b`` must be passed to - the expansion function, and - - the expansion function must return a single tape. - """ - - 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( - "Batch 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].custom_qnode_wrapper = lambda x: x - return args[0] - - return super().__new__(cls) - - def __init__(self, transform_fn, expand_fn=None, differentiable=True): - if not callable(transform_fn): - raise ValueError( - f"The batch transform function to register, {transform_fn}, " - "does not appear to be a valid Python function or callable." - ) - - warnings.warn( - "Use of `batch_transform` to create a custom transform is deprecated. Instead " - "switch to using the new qml.transform function. Follow the instructions here for " - "further details: https://docs.pennylane.ai/en/stable/code/qml_transforms.html#custom-transforms.", - qml.PennyLaneDeprecationWarning, - ) - self.transform_fn = transform_fn - self.expand_fn = expand_fn - self.differentiable = differentiable - self.qnode_wrapper = self.default_qnode_wrapper - functools.update_wrapper(self, transform_fn) - - def custom_qnode_wrapper(self, fn): - """Register a custom QNode execution wrapper function - for the batch transform. - - **Example** - - .. code-block:: python - - def my_transform(tape, *targs, **tkwargs): - ... - return tapes, processing_fn - - @my_transform.custom_qnode_wrapper - def my_custom_qnode_wrapper(self, qnode, targs, tkwargs): - def wrapper_fn(*args, **kwargs): - # construct QNode - qnode.construct(args, kwargs) - # apply transform to QNode's tapes - tapes, processing_fn = self.construct(qnode.qtape, *targs, **tkwargs) - # execute tapes and return processed result - ... - return processing_fn(results) - return wrapper_fn - - The custom QNode execution wrapper must have arguments - ``self`` (the batch transform object), ``qnode`` (the input QNode - to transform and execute), ``targs`` and ``tkwargs`` (the transform - arguments and keyword arguments respectively). - - It should return a callable object that accepts the *same* arguments - as the QNode, and returns the transformed numerical result. - - The default :meth:`~.default_qnode_wrapper` method may be called - if only pre- or post-processing dependent on QNode arguments is required: - - .. code-block:: python - - @my_transform.custom_qnode_wrapper - def my_custom_qnode_wrapper(self, qnode, targs, tkwargs): - transformed_qnode = self.default_qnode_wrapper(qnode) - - def wrapper_fn(*args, **kwargs): - args, kwargs = pre_process(args, kwargs) - res = transformed_qnode(*args, **kwargs) - ... - return ... - return wrapper_fn - """ - self.qnode_wrapper = types.MethodType(fn, self) - - def default_qnode_wrapper(self, qnode, targs, tkwargs): - """A wrapper method that takes a QNode and transform arguments, - and returns a function that 'wraps' the QNode execution. - - The returned function should accept the same keyword arguments as - the QNode, and return the output of applying the tape transform - to the QNode's constructed tape. - """ - transform_max_diff = tkwargs.pop("max_diff", None) - - if "shots" in inspect.signature(qnode.func).parameters: - raise ValueError( - "Detected 'shots' as an argument of the quantum function to transform. " - "The 'shots' argument name is reserved for overriding the number of shots " - "taken by the device." - ) - - def _wrapper(*args, **kwargs): - shots = kwargs.pop("shots", False) - - argnums = kwargs.pop("argnums", None) - - if argnums: - tkwargs["argnums"] = argnums # pragma: no cover - - old_interface = qnode.interface - - if old_interface == "auto": - qnode.interface = qml.math.get_interface(*args, *list(kwargs.values())) - - qnode.construct(args, kwargs) - tapes, processing_fn = self.construct(qnode.qtape, *targs, **tkwargs) - - interface = qnode.interface - execute_kwargs = getattr(qnode, "execute_kwargs", {}).copy() - max_diff = execute_kwargs.pop("max_diff", 2) - max_diff = transform_max_diff or max_diff - - gradient_fn = getattr(qnode, "gradient_fn", qnode.diff_method) - gradient_kwargs = getattr(qnode, "gradient_kwargs", {}) - - if interface is None or not self.differentiable: - gradient_fn = None - - if old_interface == "auto": - qnode.interface = "auto" - - res = qml.execute( - tapes, - device=qnode.device, - gradient_fn=gradient_fn, - interface=interface, - max_diff=max_diff, - override_shots=shots, - gradient_kwargs=gradient_kwargs, - **execute_kwargs, - ) - - return processing_fn(res) - - return _wrapper - - def __call__(self, *targs, **tkwargs): - qnode = None - - if targs: - qnode, *targs = targs - - if isinstance(qnode, qml.Device): - # Input is a quantum device. - # dev = some_transform(dev, *transform_args) - return self._device_wrapper(*targs, **tkwargs)(qnode) - - if isinstance(qnode, qml.tape.QuantumScript): - # Input is a quantum tape. - # tapes, fn = some_transform(tape, *transform_args) - return self._tape_wrapper(*targs, **tkwargs)(qnode) - - if isinstance(qnode, qml.QNode): - # Input is a QNode: - # result = some_transform(qnode, *transform_args)(*qnode_args) - wrapper = self.qnode_wrapper(qnode, targs, tkwargs) - wrapper = functools.wraps(qnode)(wrapper) - - def _construct(args, kwargs): - qnode.construct(args, kwargs) - return self.construct(qnode.qtape, *targs, **tkwargs) - - wrapper.construct = _construct - - else: - # Input is not a QNode nor a quantum tape nor a device. - # Assume Python decorator syntax: - # - # result = some_transform(*transform_args)(qnode)(*qnode_args) - # - # or - # - # @some_transform(*transform_args) - # @qml.qnode(dev) - # def circuit(...): - # ... - # result = circuit(*qnode_args) - - # Prepend the input to the transform args, - # and create a wrapper function. - if qnode is not None: - targs = (qnode,) + tuple(targs) - - def wrapper(qnode): - if isinstance(qnode, qml.Device): - return self._device_wrapper(*targs, **tkwargs)(qnode) - - if isinstance(qnode, qml.tape.QuantumScript): - return self._tape_wrapper(*targs, **tkwargs)(qnode) - - _wrapper = self.qnode_wrapper(qnode, targs, tkwargs) - _wrapper = functools.wraps(qnode)(_wrapper) - - def _construct(args, kwargs): - qnode.construct(args, kwargs) - return self.construct(qnode.qtape, *targs, **tkwargs) - - _wrapper.construct = _construct - return _wrapper - - wrapper.tape_fn = functools.partial(self.transform_fn, *targs, **tkwargs) - wrapper.expand_fn = self.expand_fn - wrapper.differentiable = self.differentiable - return wrapper - - def construct(self, tape, *targs, **tkwargs): - """Applies the batch tape transform to an input tape. - - Args: - tape (.QuantumTape): the tape to be transformed - *args: positional arguments to pass to the tape transform - **kwargs: keyword arguments to pass to the tape transform - - Returns: - tuple[list[tapes], callable]: list of transformed tapes - to execute and a post-processing function. - """ - expand = tkwargs.pop("_expand", True) - argnums = tkwargs.pop("argnums", None) - - if expand and self.expand_fn is not None: - tape = self.expand_fn(tape, *targs, **tkwargs) - - if argnums is not None: - tape.trainable_params = argnums # pragma: no cover - tapes, processing_fn = self.transform_fn(tape, *targs, **tkwargs) - - if processing_fn is None: - - def processing_fn(x): - return x - - return tapes, processing_fn - - def _device_wrapper(self, *targs, **tkwargs): - def _wrapper(dev): - new_dev = copy.deepcopy(dev) - new_dev.batch_transform = lambda tape: self.construct(tape, *targs, **tkwargs) - return new_dev - - return _wrapper - - def _tape_wrapper(self, *targs, **tkwargs): - return lambda tape: self.construct(tape, *targs, **tkwargs) - - def map_batch_transform( transform: Callable, tapes: QuantumTapeBatch ) -> Tuple[QuantumTapeBatch, PostprocessingFn]: diff --git a/pennylane/transforms/op_transforms.py b/pennylane/transforms/op_transforms.py deleted file mode 100644 index f3c1c6d464a..00000000000 --- a/pennylane/transforms/op_transforms.py +++ /dev/null @@ -1,522 +0,0 @@ -# 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""" - - -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:: - - Use of ``op_transform`` to create a custom transform is deprecated. Instead - switch to using the new :func:`transform` function. Follow the instructions - `here `_ - for further details - - 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.X(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." - ) - - warnings.warn( - "Use of `op_transform` to create a custom transform is deprecated. Instead " - "switch to using the new qml.transform function. Follow the instructions here for " - "further details: https://docs.pennylane.ai/en/stable/code/qml_transforms.html#custom-transforms.", - qml.PennyLaneDeprecationWarning, - ) - 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 - - 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 - - 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) - - 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.X(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 diff --git a/pennylane/transforms/qfunc_transforms.py b/pennylane/transforms/qfunc_transforms.py deleted file mode 100644 index 504b86a6ac5..00000000000 --- a/pennylane/transforms/qfunc_transforms.py +++ /dev/null @@ -1,436 +0,0 @@ -# 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. -"""Contains tools and decorators for registering qfunc transforms.""" -# pylint: disable=too-few-public-methods -from copy import deepcopy -import functools -import inspect -import os -import warnings - -import pennylane as qml -from pennylane.tape import make_qscript - - -def make_tape(fn): - """Returns a function that generates the tape from a quantum function without any - operation queuing taking place. - - This is useful when you would like to manipulate or transform - the tape created by a quantum function without evaluating it. - - Args: - fn (function): the quantum function to generate the tape from - - Returns: - function: The returned function takes the same arguments as the quantum - function. When called, it returns the generated quantum tape - without any queueing occuring. - - **Example** - - Consider the following quantum function: - - .. code-block:: python - - def qfunc(x): - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - qml.RX(x, wires=0) - - We can use ``make_tape`` to extract the tape generated by this - quantum function, without any of the operations being queued by - any existing queuing contexts: - - >>> with qml.tape.QuantumTape() as active_tape: - ... qml.RY(1.0, wires=0) - ... tape = make_tape(qfunc)(0.5) - >>> tape.operations - [Hadamard(wires=[0]), CNOT(wires=[0, 1]), RX(0.5, wires=[0])] - - Note that the currently recording tape did not queue any of these quantum operations: - - >>> active_tape.operations - [RY(1.0, wires=[0])] - """ - - def wrapper(*args, **kwargs): - with qml.QueuingManager.stop_recording(), qml.tape.QuantumTape() as new_tape: - fn(*args, **kwargs) - - return new_tape - - return wrapper - - -class single_tape_transform: - """For registering a tape transform that takes a tape and outputs a single new tape. - - .. warning:: - - Use of ``single_tape_transform`` to create a custom transform is deprecated. Instead - switch to using the new :func:`transform` function. Follow the instructions - `here `_ - for further details - - Examples of such transforms include circuit compilation. - - Args: - transform_fn (function): The function to register as the single tape transform. - It can have an arbitrary number of arguments, but the first argument - **must** be the input tape. - - **Example** - - A valid single tape transform is a quantum function that satisfies the following: - - - The first argument must be an input tape - - - Depending on the structure of this input tape, various quantum operations, functions, - and templates may be called. - - - Any internal classical processing should use the ``qml.math`` module to ensure - the transform is differentiable. - - - There is no return statement. - - For example: - - .. code-block:: python - - @qml.single_tape_transform - def my_transform(tape, x, y): - # loop through all operations on the input tape - for op in tape: - if op.name == "CRX": - wires = op.wires - param = op.parameters[0] - - qml.RX(x * qml.math.abs(param), wires=wires[1]) - qml.RY(y * qml.math.abs(param), wires=wires[1]) - qml.CZ(wires=[wires[1], wires[0]]) - else: - op.queue() - - This transform iterates through the input tape, and replaces any :class:`~.CRX` operation with - two single qubit rotations and a :class:`~.CZ` operation. These newly queued operations will - form the output transformed tape. - - We can apply this transform to a quantum tape: - - >>> with qml.tape.QuantumTape() as tape: - ... qml.Hadamard(wires=0) - ... qml.CRX(-0.5, wires=[0, 1]) - >>> new_tape = my_transform(tape, 1., 2.) - >>> print(qml.drawer.tape_text(new_tape, decimals=1)) - 0: ──H────────────────╭Z─┤ - 1: ──RX(0.5)──RY(1.0)─╰●─┤ - - """ - - def __init__(self, transform_fn): - if not callable(transform_fn): - raise ValueError( - f"The tape transform function to register, {transform_fn}, " - "does not appear to be a valid Python function or callable." - ) - - warnings.warn( - "Use of `single_tape_transform` to create a custom transform is deprecated. Instead " - "switch to using the new qml.transform function. Follow the instructions here for " - "further details: https://docs.pennylane.ai/en/stable/code/qml_transforms.html#custom-transforms.", - qml.PennyLaneDeprecationWarning, - ) - self.transform_fn = transform_fn - functools.update_wrapper(self, transform_fn) - - def __call__(self, tape, *args, **kwargs): - with qml.queuing.AnnotatedQueue() as q: - self.transform_fn(tape, *args, **kwargs) - qs = qml.tape.QuantumScript.from_queue(q, shots=tape.shots) - for obj, info in q.items(): - qml.queuing.QueuingManager.append(obj, **info) - return qs - - -def _create_qfunc_internal_wrapper( - fn, tape_transform, transform_args, transform_kwargs -): # pragma: no cover - """Convenience function to create the internal wrapper function - generated by the qfunc_transform decorator""" - if isinstance(fn, qml.Device): - new_dev = deepcopy(fn) - - @new_dev.custom_expand - def new_expand_fn(self, tape, *args, **kwargs): # pylint: disable=unused-variable - tape = tape_transform(tape, *transform_args, **transform_kwargs) - return self.default_expand_fn(tape, *args, **kwargs) - - return new_dev - - if isinstance(fn, qml.tape.QuantumScript): - return tape_transform(fn, *transform_args, **transform_kwargs) - - if not callable(fn): - raise ValueError( - f"The qfunc to transform, {fn}, does not appear " - "to be a valid Python function or callable." - ) - if isinstance(fn, qml.QNode): - raise ValueError("QNodes cannot be declared as qfunc transforms.") - - @functools.wraps(fn) - def internal_wrapper(*args, **kwargs): - tape = make_qscript(fn)(*args, **kwargs) - tape = tape_transform(tape, *transform_args, **transform_kwargs) - - num_measurements = len(tape.measurements) - if num_measurements == 0: - return None - return tape.measurements[0] if num_measurements == 1 else tape.measurements - - return internal_wrapper - - -def qfunc_transform(tape_transform): - """Given a function which defines a tape transform, convert the function into - one that applies the tape transform to quantum functions (qfuncs). - - .. warning:: - - Use of ``qfunc_transform`` to create a custom transform is deprecated. Instead - switch to using the new :func:`transform` function. Follow the instructions - `here `_ - for further details - - Args: - tape_transform (function or single_tape_transform): the single tape transform - to convert into the qfunc transform. - - Returns: - function: A qfunc transform, that acts on any qfunc, and returns a *new* - qfunc as per the tape transform. Note that if ``tape_transform`` takes - additional parameters beyond a single tape, then the created qfunc transform - will take the *same* parameters, prior to being applied to the qfunc. - - **Example** - - Given a single tape transform ``my_transform(tape, x, y)``, you can use - this function to convert it into a qfunc transform: - - >>> my_qfunc_transform = qfunc_transform(my_transform) - - It can then be used to transform an existing qfunc: - - >>> new_qfunc = my_qfunc_transform(0.6, 0.7)(old_qfunc) - >>> new_qfunc(params) - - It can also be used as a decorator: - - .. code-block:: python - - @qml.qfunc_transform - def my_transform(tape, x, y): - for op in tape: - if op.name == "CRX": - wires = op.wires - param = op.parameters[0] - qml.RX(x * param, wires=wires[1]) - qml.RY(y * qml.math.sqrt(param), wires=wires[1]) - qml.CZ(wires=[wires[1], wires[0]]) - else: - op.queue() - - @my_transform(0.6, 0.1) - def qfunc(x): - qml.Hadamard(wires=0) - qml.CRX(x, wires=[0, 1]) - return qml.expval(qml.Z(1)) - - >>> dev = qml.device("default.qubit", wires=2) - >>> qnode = qml.QNode(qfunc, dev) - >>> print(qml.draw(qnode)(2.5)) - 0: ──H──────────────────╭Z─┤ - 1: ──RX(1.50)──RY(0.16)─╰●─┤ - - The transform weights provided to a qfunc transform are fully differentiable, - allowing the transform itself to be differentiated and trained. For more details, - see the Differentiability section under Usage Details. - - .. details:: - :title: Usage Details - - **Inline usage** - - qfunc transforms, when used inline (that is, not as a decorator), take the following form: - - >>> my_transform(transform_weights)(ansatz)(param) - - or - - >>> my_transform(ansatz)(param) - - if they do not permit any parameters. We can break this down into distinct steps, - to show what is happening with each new function call: - - 0. Create a transform defined by the transform weights: - - >>> specific_transform = my_transform(transform_weights) - - Note that this step is skipped if the transform does not provide any - weights/parameters that can be modified! - - 1. Apply the transform to the qfunc. A qfunc transform always acts on - a qfunc, returning a new qfunc: - - >>> new_qfunc = specific_transform(ansatz) - - 2. Finally, we evaluate the new, transformed, qfunc: - - >>> new_qfunc(params) - - So the syntax - - >>> my_transform(transform_weights)(ansatz)(param) - - simply 'chains' these three steps together, into a single call. - - **Differentiability** - - When applying a qfunc transform, not only is the newly transformed qfunc fully - differentiable, but the qfunc transform parameters *themselves* are differentiable. - This allows us to train both the quantum function, as well as the transform - that created it. - - Consider the following example, where a pre-defined ansatz is transformed - within a QNode: - - .. code-block:: python - - dev = qml.device("default.qubit", wires=2) - - def ansatz(x): - qml.Hadamard(wires=0) - qml.CRX(x, wires=[0, 1]) - - @qml.qnode(dev) - def circuit(param, transform_weights): - qml.RX(0.1, wires=0) - - # apply the transform to the ansatz - my_transform(*transform_weights)(ansatz)(param) - - return qml.expval(qml.Z(1)) - - We can print this QNode to show that the qfunc transform is taking place: - - >>> x = np.array(0.5, requires_grad=True) - >>> y = np.array([0.1, 0.2], requires_grad=True) - >>> print(qml.draw(circuit)(x, y)) - 0: ──RX(0.10)──H────────╭Z─┤ - 1: ──RX(0.05)──RY(0.14)─╰●─┤ - - Evaluating the QNode, as well as the derivative, with respect to the gate - parameter *and* the transform weights: - - >>> circuit(x, y) - tensor(0.98877939, requires_grad=True) - >>> qml.grad(circuit)(x, y) - (tensor(-0.02485651, requires_grad=True), array([-0.02474011, -0.09954244])) - - **Implementation details** - - Internally, the qfunc transform works as follows: - - .. code-block:: python - - def transform(old_qfunc, params): - def new_qfunc(*args, **kwargs): - # 1. extract the QuantumTape from the old qfunc, being - # careful *not* to have it queued. - tape = make_qscript(old_qfunc)(*args, **kwargs) - - # 2. transform the tape - new_tape = tape_transform(tape, params) - - # 3. queue the *new* tape to the active queuing context - new_tape.queue() - return new_qfunc - - *Note: this is pseudocode; the actual implementation is significantly more complicated!* - - Steps (1) and (3) are identical for all qfunc transforms; it is only step (2), - ``tape_transform`` and the corresponding tape transform parameters, that define the qfunc - transformation. - - That is, given a tape transform that **defines the qfunc transformation**, the - decorator **elevates** the tape transform to one that works on quantum functions - rather than tapes. This decorator therefore automates the process of adding in - the queueing logic required under steps (1) and (3), so that it does not need to be - repeated and tested for every new qfunc transform. - """ - 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( - "qfunc 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, - ) - - return tape_transform - - if not callable(tape_transform): - raise ValueError( - "The qfunc_transform decorator can only be applied " - "to single tape transform functions." - ) - - warnings.warn( - "Use of `qfunc_transform` to create a custom transform is deprecated. Instead " - "switch to using the new qml.transform function. Follow the instructions here for " - "further details: https://docs.pennylane.ai/en/stable/code/qml_transforms.html#custom-transforms.", - qml.PennyLaneDeprecationWarning, - ) - if not isinstance(tape_transform, single_tape_transform): - with warnings.catch_warnings(): - warnings.simplefilter("ignore", qml.PennyLaneDeprecationWarning) - tape_transform = single_tape_transform(tape_transform) - - sig = inspect.signature(tape_transform) - params = sig.parameters - - if len(params) > 1: - - @functools.wraps(tape_transform) - def make_qfunc_transform(*targs, **tkwargs): - def wrapper(fn): - return _create_qfunc_internal_wrapper(fn, tape_transform, targs, tkwargs) - - wrapper.tape_fn = functools.partial(tape_transform, *targs, **tkwargs) - - return wrapper - - elif len(params) == 1: - - @functools.wraps(tape_transform) - def make_qfunc_transform(fn): - return _create_qfunc_internal_wrapper(fn, tape_transform, [], {}) - - make_qfunc_transform.tape_fn = tape_transform - return make_qfunc_transform diff --git a/pennylane/transforms/zx/converter.py b/pennylane/transforms/zx/converter.py index 508bf4a2780..d492b53276f 100644 --- a/pennylane/transforms/zx/converter.py +++ b/pennylane/transforms/zx/converter.py @@ -21,7 +21,7 @@ import pennylane as qml from pennylane.operation import Operator from pennylane.tape import QuantumScript, QuantumTape -from pennylane.transforms.op_transforms import OperationTransformError +from pennylane.transforms import TransformError from pennylane.transforms import transform from pennylane.wires import Wires @@ -256,9 +256,7 @@ def mod_5_4(): # If it is a simple operation just transform it to a tape if not isinstance(tape, Operator): if not isinstance(tape, (qml.tape.QuantumScript, qml.QNode)) and not callable(tape): - raise OperationTransformError( - "Input is not an Operator, tape, QNode, or quantum function" - ) + raise TransformError("Input is not an Operator, tape, QNode, or quantum function") return _to_zx_transform(tape, expand_measurements=expand_measurements) return to_zx(QuantumScript([tape])) diff --git a/tests/ops/functions/test_eigvals.py b/tests/ops/functions/test_eigvals.py index e9cda9579b4..7397c5bc070 100644 --- a/tests/ops/functions/test_eigvals.py +++ b/tests/ops/functions/test_eigvals.py @@ -22,7 +22,7 @@ from gate_data import CNOT, H, I, S, X, Y, Z import pennylane as qml from pennylane import numpy as np -from pennylane.transforms.op_transforms import OperationTransformError +from pennylane.transforms import TransformError one_qubit_no_parameter = [ qml.PauliX, @@ -40,7 +40,7 @@ def test_invalid_argument(): """Assert error raised when input is neither a tape, QNode, nor quantum function""" with pytest.raises( - OperationTransformError, + TransformError, match="Input is not an Operator, tape, QNode, or quantum function", ): _ = qml.eigvals(None) diff --git a/tests/ops/functions/test_matrix.py b/tests/ops/functions/test_matrix.py index 4ef03d2db72..eaa220cefef 100644 --- a/tests/ops/functions/test_matrix.py +++ b/tests/ops/functions/test_matrix.py @@ -24,7 +24,7 @@ import pennylane as qml from pennylane import numpy as np -from pennylane.transforms.op_transforms import OperationTransformError +from pennylane.transforms import TransformError from pennylane.pauli import PauliWord, PauliSentence one_qubit_no_parameter = [ @@ -548,7 +548,7 @@ class TestValidation: def test_invalid_argument(self): """Assert error raised when input is neither a tape, QNode, nor quantum function""" with pytest.raises( - OperationTransformError, + TransformError, match="Input is not an Operator, tape, QNode, or quantum function", ): _ = qml.matrix(None) @@ -563,13 +563,13 @@ def circuit(): wires = [0, "b"] with pytest.raises( - OperationTransformError, + TransformError, match=r"Wires in circuit \[1, 0\] are inconsistent with those in wire_order \[0, 'b'\]", ): qml.matrix(circuit, wire_order=wires)() with pytest.raises( - OperationTransformError, + TransformError, match=r"Wires in circuit \[0\] are inconsistent with those in wire_order \[1\]", ): qml.matrix(qml.PauliX(0), wire_order=[1]) diff --git a/tests/transforms/test_batch_transform.py b/tests/transforms/test_batch_transform.py index a00e159a99c..3e43d091bd9 100644 --- a/tests/transforms/test_batch_transform.py +++ b/tests/transforms/test_batch_transform.py @@ -22,698 +22,6 @@ from pennylane import numpy as np -pytestmark = pytest.mark.filterwarnings( - "ignore:.*batch_transform.*:pennylane.PennyLaneDeprecationWarning" -) - - -class TestBatchTransform: - """Unit tests for the batch_transform class""" - - def test_batch_transform_is_deprecated(self): - """Test that the batch_transform class is deprecated.""" - - def func(op): - return op - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="Use of `batch_transform`"): - _ = qml.batch_transform(func) - - def my_transform(tape, a, b): - """Generates two tapes, one with all RX replaced with RY, - and the other with all RX replaced with RZ.""" - - q_tape1 = qml.queuing.AnnotatedQueue() - q_tape2 = qml.queuing.AnnotatedQueue() - - # loop through all operations on the input tape - for op in tape.operations: - if op.name == "RX": - wires = op.wires - param = op.parameters[0] - - with q_tape1: - qml.RY(a * qml.math.abs(param), wires=wires) - - with q_tape2: - qml.RZ(b * qml.math.sin(param), wires=wires) - else: - for t in [q_tape1, q_tape2]: - with t: - qml.apply(op) - for mp in tape.measurements: - for t in [q_tape1, q_tape2]: - with t: - qml.apply(mp) - - tape1 = qml.tape.QuantumScript.from_queue(q_tape1) - tape2 = qml.tape.QuantumScript.from_queue(q_tape2) - - def processing_fn(results): - return qml.math.sum(qml.math.stack(results)) - - return [tape1, tape2], processing_fn - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="Use of `batch_transform`"): - my_transform = staticmethod(qml.batch_transform(my_transform)) - - @staticmethod - def phaseshift_expand(tape): - return tape.expand(stop_at=lambda obj: obj.name != "PhaseShift") - - @staticmethod - def expand_logic_with_kwarg(tape, perform_expansion=None, **kwargs): - # pylint: disable=unused-argument - if perform_expansion: - return TestBatchTransform.phaseshift_expand(tape) - return tape - - def test_error_invalid_callable(self): - """Test that an error is raised if the transform - is applied to an invalid function""" - - with pytest.raises(ValueError, match="does not appear to be a valid Python function"): - qml.batch_transform(5) - - def test_sphinx_build(self, monkeypatch): - """Test that batch transforms are not created during Sphinx builds""" - - @qml.batch_transform - def my_transform0(tape): - tape1 = tape.copy() - tape2 = tape.copy() - return [tape1, tape2], None - - assert isinstance(my_transform0, qml.batch_transform) - - monkeypatch.setenv("SPHINX_BUILD", "1") - - with pytest.warns(UserWarning, match="Batch transformations have been disabled"): - - @qml.batch_transform - def my_transform1(tape): - tape1 = tape.copy() - tape2 = tape.copy() - return [tape1, tape2], None - - assert not isinstance(my_transform1, qml.batch_transform) - - def test_none_processing(self): - """Test that a transform that returns None for a processing function applies - the identity as the processing function""" - - @qml.batch_transform - def my_transform(tape): - tape1 = tape.copy() - tape2 = tape.copy() - return [tape1, tape2], None - - with qml.queuing.AnnotatedQueue() as q: - qml.Hadamard(wires=0) - qml.expval(qml.PauliX(0)) - - tape = qml.tape.QuantumScript.from_queue(q) - _, fn = my_transform(tape) - assert fn(5) == 5 - - qs = qml.tape.QuantumScript(tape.operations, tape.measurements) - _, fn = my_transform(qs) - assert fn(5) == 5 - - def test_not_differentiable(self): - """Test that a non-differentiable transform cannot be differentiated""" - - def my_transform(tape): - tape1 = tape.copy() - tape2 = tape.copy() - return [tape1, tape2], qml.math.sum - - my_transform = qml.batch_transform(my_transform, differentiable=False) - - dev = qml.device("default.qubit", wires=2) - - @my_transform - @qml.qnode(dev) - def circuit(x): - qml.Hadamard(wires=0) - qml.RY(x, wires=0) - return qml.expval(qml.PauliX(0)) - - res = circuit(0.5) - assert isinstance(res, float) - assert not np.allclose(res, 0) - - with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"): - qml.grad(circuit)(0.5) - - def test_use_qnode_execution_options(self, mocker): - """Test that a QNodes execution options are used by the - batch transform""" - dev = qml.device("default.qubit", wires=2) - cache = {} - - @qml.qnode(dev, max_diff=3, cache=cache) - def circuit(x): - qml.Hadamard(wires=0) - qml.RY(x, wires=0) - return qml.expval(qml.PauliX(0)) - - a = 0.1 - b = 0.4 - x = 0.543 - - fn = self.my_transform(circuit, a, b) - - spy = mocker.spy(qml, "execute") - fn(x) - assert spy.call_args[1]["max_diff"] == 3 - assert spy.call_args[1]["cache"] is cache - - # test that the QNode execution options remain unchanged - assert circuit.execute_kwargs["max_diff"] == 3 - - def test_expand_fn(self, mocker): - """Test that if an expansion function is provided, - that the input tape is expanded before being transformed.""" - - class MyTransform: - """Dummy class to allow spying to work""" - - def my_transform(self, tape): - tape1 = tape.copy() - tape2 = tape.copy() - return [tape1, tape2], None - - spy_transform = mocker.spy(MyTransform, "my_transform") - transform_fn = qml.batch_transform( - MyTransform().my_transform, expand_fn=self.phaseshift_expand - ) - - with qml.queuing.AnnotatedQueue() as q: - qml.PhaseShift(0.5, wires=0) - qml.expval(qml.PauliX(0)) - - tape = qml.tape.QuantumScript.from_queue(q) - spy_expand = mocker.spy(transform_fn, "expand_fn") - - transform_fn(tape) - - spy_transform.assert_called() - spy_expand.assert_called() - - input_tape = spy_transform.call_args[0][1] - assert len(input_tape.operations) == 2 - assert input_tape.operations[0].name == "RZ" - assert input_tape.operations[1].name == "GlobalPhase" - assert input_tape.operations[0].parameters == [0.5] - assert input_tape.operations[1].parameters == [-0.25] - - @pytest.mark.parametrize("perform_expansion", [True, False]) - def test_expand_fn_with_kwarg(self, mocker, perform_expansion): - """Test that kwargs are respected in the expansion.""" - - class MyTransform: - """Dummy class to allow spying to work""" - - # pylint: disable=unused-argument - - def my_transform(self, tape, **kwargs): - tape1 = tape.copy() - tape2 = tape.copy() - return [tape1, tape2], None - - spy_transform = mocker.spy(MyTransform, "my_transform") - transform_fn = qml.batch_transform( - MyTransform().my_transform, expand_fn=self.expand_logic_with_kwarg - ) - with qml.queuing.AnnotatedQueue() as q: - qml.PhaseShift(0.5, wires=0) - qml.expval(qml.PauliX(0)) - - tape = qml.tape.QuantumScript.from_queue(q) - spy_expand = mocker.spy(transform_fn, "expand_fn") - - transform_fn(tape, perform_expansion=perform_expansion) - - spy_transform.assert_called() - spy_expand.assert_called() # The expand_fn of transform_fn always is called - - input_tape = spy_transform.call_args[0][1] - - if perform_expansion: - assert len(input_tape.operations) == 2 - assert input_tape.operations[0].name == "RZ" - assert input_tape.operations[0].parameters == [0.5] - assert input_tape.operations[1].name == "GlobalPhase" - assert input_tape.operations[1].parameters == [-0.25] - else: - assert len(input_tape.operations) == 1 - assert input_tape.operations[0].name == "PhaseShift" - assert input_tape.operations[0].parameters == [0.5] - - @pytest.mark.parametrize("perform_expansion", [True, False]) - def test_expand_qnode_with_kwarg(self, mocker, perform_expansion): - """Test that kwargs are respected in the expansion.""" - - class MyTransform: - """Dummy class to allow spying to work""" - - # pylint: disable=unused-argument - - def my_transform(self, tape, **kwargs): - tape1 = tape.copy() - tape2 = tape.copy() - return [tape1, tape2], None - - spy_transform = mocker.spy(MyTransform, "my_transform") - transform_fn = qml.batch_transform( - MyTransform().my_transform, expand_fn=self.expand_logic_with_kwarg - ) - - spy_expand = mocker.spy(transform_fn, "expand_fn") - dev = qml.device("default.qubit", wires=2) - - @functools.partial(transform_fn, perform_expansion=perform_expansion) - @qml.qnode(dev) - def qnode(x): - # pylint: disable=unused-argument - qml.PhaseShift(0.5, wires=0) - return qml.expval(qml.PauliX(0)) - - qnode(0.2) - - spy_transform.assert_called() - spy_expand.assert_called() # The expand_fn of transform_fn always is called - input_tape = spy_transform.call_args[0][1] - - if perform_expansion: - assert len(input_tape.operations) == 2 - assert input_tape.operations[0].name == "RZ" - assert input_tape.operations[0].parameters == [0.5] - assert input_tape.operations[1].name == "GlobalPhase" - assert input_tape.operations[1].parameters == [-0.25] - else: - assert len(input_tape.operations) == 1 - assert input_tape.operations[0].name == "PhaseShift" - assert input_tape.operations[0].parameters == [0.5] - - def test_parametrized_transform_tape(self): - """Test that a parametrized transform can be applied - to a tape""" - - a = 0.1 - b = 0.4 - x = 0.543 - - with qml.queuing.AnnotatedQueue() as q: - qml.Hadamard(wires=0) - qml.RX(x, wires=0) - qml.expval(qml.PauliX(0)) - - tape = qml.tape.QuantumScript.from_queue(q) - tapes, _ = self.my_transform(tape, a, b) - - assert len(tapes[0].operations) == 2 - assert tapes[0].operations[0].name == "Hadamard" - assert tapes[0].operations[1].name == "RY" - assert tapes[0].operations[1].parameters == [a * np.abs(x)] - - assert len(tapes[1].operations) == 2 - assert tapes[1].operations[0].name == "Hadamard" - assert tapes[1].operations[1].name == "RZ" - assert tapes[1].operations[1].parameters == [b * np.sin(x)] - - def test_parametrized_transform_tape_decorator(self): - """Test that a parametrized transform can be applied - to a tape""" - - a = 0.1 - b = 0.4 - x = 0.543 - - with qml.queuing.AnnotatedQueue() as q: - qml.Hadamard(wires=0) - qml.RX(x, wires=0) - qml.expval(qml.PauliX(0)) - - tape = qml.tape.QuantumScript.from_queue(q) - tapes, _ = self.my_transform(a, b)(tape) # pylint: disable=no-value-for-parameter - - assert len(tapes[0].operations) == 2 - assert tapes[0].operations[0].name == "Hadamard" - assert tapes[0].operations[1].name == "RY" - assert tapes[0].operations[1].parameters == [a * np.abs(x)] - - assert len(tapes[1].operations) == 2 - assert tapes[1].operations[0].name == "Hadamard" - assert tapes[1].operations[1].name == "RZ" - assert tapes[1].operations[1].parameters == [b * np.sin(x)] - - def test_parametrized_transform_device(self, mocker): - """Test that a parametrized transform can be applied - to a device""" - - a = 0.1 - b = 0.4 - x = 0.543 - - dev = qml.device("default.qubit.legacy", wires=1) - dev = self.my_transform(dev, a, b) - - @qml.qnode(dev, interface="autograd") - def circuit(x): - qml.Hadamard(wires=0) - qml.RX(x, wires=0) - return qml.expval(qml.PauliX(0)) - - spy = mocker.spy(circuit.device, "batch_execute") - circuit(x) - tapes = spy.call_args[0][0] - - assert len(tapes[0].operations) == 2 - assert tapes[0].operations[0].name == "Hadamard" - assert tapes[0].operations[1].name == "RY" - assert tapes[0].operations[1].parameters == [a * np.abs(x)] - - assert len(tapes[1].operations) == 2 - assert tapes[1].operations[0].name == "Hadamard" - assert tapes[1].operations[1].name == "RZ" - assert tapes[1].operations[1].parameters == [b * np.sin(x)] - - def test_parametrized_transform_device_decorator(self, mocker): - """Test that a parametrized transform can be applied - to a device""" - - a = 0.1 - b = 0.4 - x = 0.543 - - dev = qml.device("default.qubit.legacy", wires=1) - dev = self.my_transform(a, b)(dev) # pylint: disable=no-value-for-parameter - - @qml.qnode(dev, interface="autograd") - def circuit(x): - qml.Hadamard(wires=0) - qml.RX(x, wires=0) - return qml.expval(qml.PauliX(0)) - - spy = mocker.spy(circuit.device, "batch_execute") - circuit(x) - tapes = spy.call_args[0][0] - - assert len(tapes[0].operations) == 2 - assert tapes[0].operations[0].name == "Hadamard" - assert tapes[0].operations[1].name == "RY" - assert tapes[0].operations[1].parameters == [a * np.abs(x)] - - assert len(tapes[1].operations) == 2 - assert tapes[1].operations[0].name == "Hadamard" - assert tapes[1].operations[1].name == "RZ" - assert tapes[1].operations[1].parameters == [b * np.sin(x)] - - def test_parametrized_transform_qnode(self, mocker): - """Test that a parametrized transform can be applied - to a QNode""" - - a = 0.1 - b = 0.4 - x = 0.543 - - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(x): - qml.Hadamard(wires=0) - qml.RX(x, wires=0) - return qml.expval(qml.PauliX(0)) - - transform_fn = self.my_transform(circuit, a, b) - - spy = mocker.spy(self.my_transform, "construct") - res = transform_fn(x) - - spy.assert_called() - tapes, fn = spy.spy_return - - assert len(tapes[0].operations) == 2 - assert tapes[0].operations[0].name == "Hadamard" - assert tapes[0].operations[1].name == "RY" - assert tapes[0].operations[1].parameters == [a * np.abs(x)] - - assert len(tapes[1].operations) == 2 - assert tapes[1].operations[0].name == "Hadamard" - assert tapes[1].operations[1].name == "RZ" - assert tapes[1].operations[1].parameters == [b * np.sin(x)] - - expected = fn(dev.execute(tapes)) - assert res == expected - assert circuit.interface == "auto" - - def test_parametrized_transform_qnode_decorator(self, mocker): - """Test that a parametrized transform can be applied - to a QNode as a decorator""" - a = 0.1 - b = 0.4 - x = 0.543 - - dev = qml.device("default.qubit", wires=2) - - @self.my_transform(a, b) # pylint: disable=no-value-for-parameter - @qml.qnode(dev) - def circuit(x): - qml.Hadamard(wires=0) - qml.RX(x, wires=0) - return qml.expval(qml.PauliX(0)) - - spy = mocker.spy(self.my_transform, "construct") - res = circuit(x) - - spy.assert_called() - tapes, fn = spy.spy_return - - assert len(tapes[0].operations) == 2 - assert tapes[0].operations[0].name == "Hadamard" - assert tapes[0].operations[1].name == "RY" - assert tapes[0].operations[1].parameters == [a * np.abs(x)] - - assert len(tapes[1].operations) == 2 - assert tapes[1].operations[0].name == "Hadamard" - assert tapes[1].operations[1].name == "RZ" - assert tapes[1].operations[1].parameters == [b * np.sin(x)] - - expected = fn(dev.execute(tapes)) - assert res == expected - - def test_custom_qnode_wrapper(self): - """Test that the QNode execution wrapper can be overridden - if required.""" - a = 0.654 - x = 0.543 - - dev = qml.device("default.qubit", wires=2) - - @qml.batch_transform - def my_transform(tape, a): - tape1 = tape.copy() - tape2 = tape.copy() - return [tape1, tape2], lambda res: a * qml.math.sum(res) - - custom_wrapper_called = [False] # use list so can edit by reference - - @my_transform.custom_qnode_wrapper - def qnode_wrapper(self, qnode, targs, tkwargs): # pylint: disable=unused-variable - wrapper = self.default_qnode_wrapper(qnode, targs, tkwargs) - assert targs == (a,) - assert tkwargs == {} - custom_wrapper_called[0] = True - return wrapper - - @my_transform(a) # pylint: disable=no-value-for-parameter - @qml.qnode(dev) - def circuit(x): - qml.Hadamard(wires=0) - qml.RX(x, wires=0) - return qml.expval(qml.PauliX(0)) - - circuit(x) - - assert custom_wrapper_called[0] is True - - -@pytest.mark.parametrize("diff_method", ["parameter-shift", "backprop", "finite-diff"]) -class TestBatchTransformGradients: - """Tests for the batch_transform decorator differentiability""" - - def my_transform(tape, weights): - """Generates two tapes, one with all RX replaced with RY, - and the other with all RX replaced with RZ.""" - - q_tape1 = qml.queuing.AnnotatedQueue() - q_tape2 = qml.queuing.AnnotatedQueue() - - # loop through all operations on the input tape - for op in tape.operations: - if op.name == "RX": - wires = op.wires - param = op.parameters[0] - - with q_tape1: - qml.RY(weights[0] * qml.math.sin(param), wires=wires) - - with q_tape2: - qml.RZ(weights[1] * qml.math.cos(param), wires=wires) - else: - for t in [q_tape1, q_tape2]: - with t: - qml.apply(op) - for mp in tape.measurements: - for t in [q_tape1, q_tape2]: - with t: - qml.apply(mp) - - tape1 = qml.tape.QuantumScript.from_queue(q_tape1) - tape2 = qml.tape.QuantumScript.from_queue(q_tape2) - - def processing_fn(results): - return qml.math.sum(qml.math.stack(results)) - - return [tape1, tape2], processing_fn - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="Use of `batch_transform`"): - my_transform = staticmethod(qml.batch_transform(my_transform)) - - @staticmethod - def circuit(x): - """Test ansatz""" - qml.Hadamard(wires=0) - qml.RX(x, wires=0) - return qml.expval(qml.PauliX(0)) - - @staticmethod - def expval(x, weights): - """Analytic expectation value of the above circuit qfunc""" - return np.cos(weights[1] * np.cos(x)) + np.cos(weights[0] * np.sin(x)) - - @pytest.mark.autograd - def test_differentiable_autograd(self, diff_method): - """Test that a batch transform is differentiable when using - autograd""" - dev = qml.device("default.qubit", wires=2) - qnode = qml.QNode(self.circuit, dev, diff_method=diff_method) - - def cost(x, weights): - return self.my_transform(qnode, weights)(x) - - weights = np.array([0.1, 0.2], requires_grad=True) - x = np.array(0.543, requires_grad=True) - - res = cost(x, weights) - assert np.allclose(res, self.expval(x, weights)) - - grad = qml.grad(cost)(x, weights) - expected = qml.grad(self.expval)(x, weights) - assert all(np.allclose(g, e) for g, e in zip(grad, expected)) - - @pytest.mark.tf - def test_differentiable_tf(self, diff_method): - """Test that a batch transform is differentiable when using - TensorFlow""" - import tensorflow as tf - - dev = qml.device("default.qubit", wires=2) - qnode = qml.QNode(self.circuit, dev, diff_method=diff_method) - - weights_np = np.array([0.1, 0.2], requires_grad=True) - x_np = np.array(0.543, requires_grad=True) - weights = tf.Variable(weights_np, dtype=tf.float64) - x = tf.Variable(x_np, dtype=tf.float64) - - with tf.GradientTape() as tape: - res = self.my_transform(qnode, weights)(x) - - assert np.allclose(res, self.expval(x, weights)) - - grad = tape.gradient(res, [x, weights]) - expected = qml.grad(self.expval)(x_np, weights_np) - assert len(grad) == len(expected) - assert all(np.allclose(g, e) for g, e in zip(grad, expected)) - - @pytest.mark.torch - def test_differentiable_torch(self, diff_method): - """Test that a batch transform is differentiable when using - PyTorch""" - import torch - - dev = qml.device("default.qubit", wires=2) - qnode = qml.QNode(self.circuit, dev, diff_method=diff_method) - - weights_np = np.array([0.1, 0.2], requires_grad=True) - weights = torch.tensor(weights_np, requires_grad=True, dtype=torch.float64) - x_np = np.array(0.543, requires_grad=True) - x = torch.tensor(x_np, requires_grad=True, dtype=torch.float64) - - res = self.my_transform(qnode, weights)(x) - expected = self.expval(x.detach().numpy(), weights.detach().numpy()) - assert np.allclose(res.detach().numpy(), expected) - - res.backward() - expected = qml.grad(self.expval)(x_np, weights_np) - assert np.allclose(x.grad, expected[0]) - assert np.allclose(weights.grad, expected[1]) - - @pytest.mark.jax - def test_differentiable_jax(self, diff_method): - """Test that a batch transform is differentiable when using - jax""" - import jax - - jax.config.update("jax_enable_x64", True) - - dev = qml.device("default.qubit", wires=2) - qnode = qml.QNode(self.circuit, dev, diff_method=diff_method) - - def cost(x, weights): - # pylint: disable=unexpected-keyword-arg - return self.my_transform(qnode, weights, max_diff=1)(x) - - weights_np = np.array([0.1, 0.2], requires_grad=True) - x_np = np.array(0.543, requires_grad=True) - weights = jax.numpy.array(weights_np) - x = jax.numpy.array(x_np) - - res = cost(x, weights) - assert np.allclose(res, self.expval(x, weights)) - - grad = jax.grad(cost, argnums=[0, 1])(x, weights) - expected = qml.grad(self.expval)(x_np, weights_np) - assert len(grad) == len(expected) - for g, e in zip(grad, expected): - assert qml.math.allclose(g, e) - - def test_batch_transforms_qnode(self, diff_method): - """Test that batch transforms can be applied to a QNode - without affecting device batch transforms""" - if diff_method == "backprop": - pytest.skip("Test only supports finite shots") - - dev = qml.device("default.qubit", wires=2, shots=100000) - - H = qml.PauliZ(0) @ qml.PauliZ(1) - qml.PauliX(0) - weights = np.array([0.5, 0.3], requires_grad=True) - - @qml.gradients.param_shift - @qml.qnode(dev, diff_method=diff_method) - def circuit(weights): - qml.RX(weights[0], wires=0) - qml.RY(weights[1], wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(H) - - res = circuit(weights) - - assert np.allclose(res, [0, -np.sin(weights[1])], atol=0.1) - - class TestMapBatchTransform: """Tests for the map_batch_transform function""" diff --git a/tests/transforms/test_op_transform.py b/tests/transforms/test_op_transform.py deleted file mode 100644 index 8eed1f2c000..00000000000 --- a/tests/transforms/test_op_transform.py +++ /dev/null @@ -1,585 +0,0 @@ -# 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. -"""Tests for the @op_transform framework""" -# pylint: disable=too-few-public-methods -import pytest - -import pennylane as qml -from pennylane import numpy as np -from pennylane.transforms.op_transforms import OperationTransformError - - -pytestmark = pytest.mark.filterwarnings( - "ignore:.*op_transform.*:pennylane.PennyLaneDeprecationWarning" -) - - -class TestValidation: - """Test for validation and exceptions""" - - def test_sphinx_build(self, monkeypatch): - """Test that op transforms are not created during Sphinx builds""" - - @qml.op_transform - def my_transform0(op): - return op.name - - assert isinstance(my_transform0, qml.op_transform) - - monkeypatch.setenv("SPHINX_BUILD", "1") - - with pytest.warns(UserWarning, match="Operator transformations have been disabled"): - - @qml.op_transform - def my_transform1(op): - return op.name - - assert not isinstance(my_transform1, qml.batch_transform) - - def test_error_invalid_callable(self): - """Test that an error is raised if the transform - is applied to an invalid function""" - - with pytest.raises( - OperationTransformError, match="does not appear to be a valid Python function" - ): - qml.op_transform(5) - - def test_unknown_object(self): - """Test that an error is raised if the transform - is applied to an unknown object""" - - @qml.op_transform - def my_transform(op): - return op.name - - with pytest.raises( - OperationTransformError, - match="Input is not an Operator, tape, QNode, or quantum function", - ): - my_transform(5)(5) - - def test_empty_qfunc(self): - """Test that an error is raised if the qfunc has no quantum operations - (e.g., it is not a qfunc)""" - - @qml.op_transform - def my_transform(op): - return op.name - - def qfunc(x): - return x**2 - - with pytest.raises( - OperationTransformError, - match="Quantum function contains no quantum operations", - ): - my_transform(qfunc)(0.5) - - -class TestUI: - """Test the user interface of the op_transform, and ensure it applies - and works well for all combinations of inputs and styles""" - - def test_instantiated_operator(self): - """Test that a transform can be applied to an instantiated operator""" - - @qml.op_transform - def my_transform(op): - return op.name - - op = qml.CRX(0.5, wires=[0, 2]) - res = my_transform(op) - assert res == "CRX" - - def test_single_operator_qfunc(self, mocker): - """Test that a transform can be applied to a quantum function - that contains a single operation""" - spy = mocker.spy(qml.op_transform, "_make_tape") - - @qml.op_transform - def my_transform(op): - return op.name - - # pylint:disable=assignment-from-no-return - res = my_transform(qml.CRX)(0.5, wires=[0, "a"]) - assert res == "CRX" - - # check default wire order - assert spy.spy_return[1].tolist() == [0, "a"] - - def test_multiple_operator_error(self): - """Test that an exception is raised if the transform - is applied to a multi-op quantum function, without - the corresponding behaviour being registered""" - - @qml.op_transform - def my_transform(op): - return op.name - - @my_transform - def multi_op_qfunc(x): - qml.RX(x, wires=0) - qml.RY(0.65, wires=1) - - with pytest.raises( - OperationTransformError, match="transform does not support .+ multiple operations" - ): - multi_op_qfunc(1.5) - - def test_multiple_operator_tape(self, mocker): - """Test that a transform can be applied to a quantum function - with multiple operations as long as it is registered _how_ - the transform applies to multiple operations.""" - spy = mocker.spy(qml.op_transform, "_make_tape") - - @qml.op_transform - def my_transform(op): - return op.name - - @my_transform.tape_transform - def my_transform(tape): - return [op.name for op in tape.operations] - - with qml.queuing.AnnotatedQueue() as q: - qml.RX(1.6, wires=0) - qml.RY(0.65, wires="a") - - tape = qml.tape.QuantumScript.from_queue(q) - res = my_transform(tape) - assert res == ["RX", "RY"] - - # check default wire order - assert spy.spy_return[1].tolist() == [0, "a"] - - qs = qml.tape.QuantumScript(tape.operations) - res_qs = my_transform(qs) - assert res_qs == ["RX", "RY"] - - def test_multiple_operator_qfunc(self, mocker): - """Test that a transform can be applied to a quantum function - with multiple operations as long as it is registered _how_ - the transform applies to multiple operations.""" - spy = mocker.spy(qml.op_transform, "_make_tape") - - @qml.op_transform - def my_transform(op): - return op.name - - @my_transform.tape_transform - def my_transform(tape): - return [op.name for op in tape.operations] - - @my_transform - def multi_op_qfunc(x): - if x > 1: - qml.RX(x, wires=0) - qml.RY(0.65, wires="a") - else: - qml.RZ(x, wires="b") - - res = multi_op_qfunc(1.5) - assert res == ["RX", "RY"] - # check default wire order - assert spy.spy_return[1].tolist() == [0, "a"] - - res = multi_op_qfunc(0.5) - assert res == ["RZ"] - # check default wire order - assert spy.spy_return[1].tolist() == ["b"] - - def test_qnode(self, mocker): - """Test that a transform can be applied to a QNode - with multiple operations as long as it is registered _how_ - the transform applies to multiple operations.""" - dev = qml.device("default.qubit", wires=["a", 0, 3]) - spy = mocker.spy(qml.op_transform, "_make_tape") - - @qml.op_transform - def my_transform(op): - return op.name - - @my_transform.tape_transform - def my_transform(tape): - return [op.name for op in tape.operations] - - @my_transform - @qml.qnode(dev) - def multi_op_qfunc(x): - if x > 1: - qml.RX(x, wires=0) - qml.RY(0.65, wires="a") - else: - qml.RZ(x, wires=0) - - return qml.probs(wires="a") - - res = multi_op_qfunc(1.5) - assert res == ["RX", "RY"] - # check default wire order - assert spy.spy_return[1] == dev.wires - - res = multi_op_qfunc(0.5) - assert res == ["RZ"] - # check default wire order - assert spy.spy_return[1] == dev.wires - - -class TestTransformParameters: - def test_instantiated_operator(self): - """Test that a transform can be applied to an instantiated operator""" - - @qml.op_transform - def my_transform(op, lower=False): - return op.name.lower() if lower else op.name - - op = qml.RX(0.5, wires=0) - res = my_transform(op, lower=True) - assert res == "rx" - - def test_single_operator_qfunc(self): - """Test that a transform can be applied to a quantum function""" - - @qml.op_transform - def my_transform(op, lower=False): - return op.name.lower() if lower else op.name - - res = my_transform(qml.RX, lower=True)(0.5, wires=0) - assert res == "rx" - - def test_transform_parameters_qfunc_decorator(self): - """Test that transform parameters correctly work - when used as a decorator""" - - @qml.op_transform - def my_transform(op, lower=False): - return op.name.lower() if lower else op.name - - @my_transform.tape_transform - def my_transform(tape, lower=False): - if lower: - return [op.name.lower() for op in tape.operations] - return [op.name for op in tape.operations] - - @my_transform(True) - def multi_op_qfunc(x): - if x > 1: - qml.RX(x, wires=0) - qml.RY(0.65, wires=1) - else: - qml.RZ(x, wires=0) - - res = multi_op_qfunc(1.5) - assert res == ["rx", "ry"] - - res = multi_op_qfunc(0.5) - assert res == ["rz"] - - -def simplify_rotation(op): - """Simplify Rot(x, 0, 0) to RZ(x) or Rot(0,0,0) to Identity""" - if op.name == "Rot": - params = op.parameters - wires = op.wires - - if qml.math.allclose(params, 0): - return None - - if qml.math.allclose(params[1:2], 0): - return qml.RZ(params[0], wires) - - return op - - -with pytest.warns(qml.PennyLaneDeprecationWarning): - simplify_rotation = qml.op_transform(simplify_rotation) - - @simplify_rotation.tape_transform - @qml.qfunc_transform - def simplify_rotation(tape): - """Define how simplify rotation works on a tape""" - for op in tape.operations: - if op.name == "Rot": - simplify_rotation(op) - else: - qml.apply(op) - for mp in tape.measurements: - qml.apply(mp) - - -class TestQFuncTransformIntegration: - """Test that @qfunc_transform seamlessly integrates - with an operator transform.""" - - def test_instantiated_operator(self): - """Test a qfunc and operator transform applied to - an op""" - dev = qml.device("default.qubit", wires=2) - assert simplify_rotation.is_qfunc_transform - - weights = np.array([0.5, 0, 0]) - op = qml.Rot(*weights, wires=0) - res = simplify_rotation(op) - assert res.name == "RZ" - - @qml.qnode(dev) - def circuit(): - simplify_rotation(op) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliX(1)) - - circuit() - ops = circuit.tape.operations - assert len(ops) == 2 - assert ops[0].name == "RZ" - assert ops[1].name == "CNOT" - - def test_qfunc_inside(self): - """Test a qfunc and operator transform - applied to a qfunc inside a qfunc""" - # pylint: disable=not-callable - dev = qml.device("default.qubit", wires=2) - - def ansatz(weights): - qml.Rot(*weights, wires=0) - qml.CRX(0.5, wires=[0, 1]) - - @qml.qnode(dev) - def circuit(weights): - simplify_rotation(ansatz)(weights) # <--- qfunc is applied within circuit (inside) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliX(1)) - - weights = np.array([0.1, 0.0, 0.0]) - circuit(weights) - - ops = circuit.tape.operations - assert len(ops) == 3 - assert ops[0].name == "RZ" - assert ops[1].name == "CRX" - assert ops[2].name == "CNOT" - - weights = np.array([0.0, 0.0, 0.0]) - circuit(weights) - - ops = circuit.tape.operations - assert len(ops) == 2 - assert ops[0].name == "CRX" - assert ops[1].name == "CNOT" - - def test_qfunc_outside(self): - """Test a qfunc and operator transform - applied to qfunc""" - dev = qml.device("default.qubit", wires=2) - - def ansatz(weights): - qml.Rot(*weights, wires=0) - qml.CRX(0.5, wires=[0, 1]) - - @qml.qnode(dev) - @simplify_rotation # <--- qfunc is applied to circuit (outside) - 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)) - - weights = np.array([0.1, 0.0, 0.0]) - circuit(weights) - - ops = circuit.tape.operations - assert len(ops) == 3 - assert ops[0].name == "RZ" - assert ops[1].name == "CRX" - assert ops[2].name == "CNOT" - - @pytest.mark.xfail(reason="op transform not done yet") - def test_compilation_pipeline(self): - """Test a qfunc and operator transform - applied to qfunc""" - dev = qml.device("default.qubit", wires=2) - - def ansatz(weights): - qml.Rot(*weights, wires=0) - qml.CRX(0.5, wires=[0, 1]) - - @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)) - - weights = np.array([0.1, 0.0, 0.0]) - circuit(weights) - - ops = circuit.tape.operations - assert len(ops) == 3 - assert ops[0].name == "RZ" - assert ops[1].name == "CRX" - assert ops[2].name == "CNOT" - - def test_qnode_error(self): - """Since a qfunc transform always returns a qfunc, - it cannot be applied to a QNode.""" - dev = qml.device("default.qubit", wires=2) - - def ansatz(weights): - qml.Rot(*weights, wires=0) - qml.CRX(0.5, wires=[0, 1]) - - @simplify_rotation - @qml.qnode(dev) - 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)) - - weights = np.array([0.1, 0.0, 0.0]) - - with pytest.raises(ValueError, match="QNodes cannot be declared as qfunc transforms"): - circuit(weights) - - -class TestExpansion: - """Test for operator and tape expansion""" - - def test_auto_expansion(self): - """Test that an operator is automatically expanded as needed""" - - @qml.op_transform - def get_matrix(op): - return op.matrix() - - weights = np.ones([2, 3, 3]) - op = qml.StronglyEntanglingLayers(weights, wires=[0, 2, "a"]) - - # strongly entangling layers does not define a matrix representation - - with pytest.raises(qml.operation.MatrixUndefinedError): - op.matrix() - - # attempting to call our operator transform will fail - - with pytest.raises(qml.operation.MatrixUndefinedError): - get_matrix(op) - - # if we define how the transform acts on a tape, - # then pennylane will automatically expand the object - # and apply the tape transform - - @get_matrix.tape_transform - def _(tape): - n_wires = len(tape.wires) - unitary_matrix = np.eye(2**n_wires) - - for op in tape.operations: - mat = qml.math.expand_matrix(get_matrix(op), op.wires, tape.wires) - unitary_matrix = mat @ unitary_matrix - - return unitary_matrix - - res = get_matrix(op) - assert isinstance(res, np.ndarray) - assert res.shape == (2**3, 2**3) - - -with pytest.warns(qml.PennyLaneDeprecationWarning, match="Use of `op_transform`"): - matrix = qml.op_transform(lambda op, wire_order=None: op.matrix(wire_order=wire_order)) - - -@matrix.tape_transform -def matrix_tape(tape, wire_order=None): - n_wires = len(wire_order) - unitary_matrix = np.eye(2**n_wires) - - for op in tape.operations: - unitary_matrix = matrix(op, wire_order=wire_order) @ unitary_matrix - - return unitary_matrix - - -class TestWireOrder: - """Test for wire re-ordering""" - - def test_instantiated_operator(self): - """Test that wire order can be passed to an instantiated operator""" - op = qml.PauliZ(wires=0) - res = matrix(op, wire_order=[1, 0]) - expected = np.kron(np.eye(2), np.diag([1, -1])) - assert np.allclose(res, expected) - - def test_single_operator_qfunc(self, mocker): - """Test that wire order can be passed to a quantum function""" - spy = mocker.spy(qml.op_transform, "_make_tape") - res = matrix(qml.PauliZ, wire_order=["a", 0])(0) - expected = np.kron(np.eye(2), np.diag([1, -1])) - assert np.allclose(res, expected) - assert spy.spy_return[1].tolist() == ["a", 0] - - def test_tape(self, mocker): - """Test that wire order can be passed to a tape""" - spy = mocker.spy(qml.op_transform, "_make_tape") - - with qml.queuing.AnnotatedQueue() as q: - qml.PauliZ(wires=0) - - tape = qml.tape.QuantumScript.from_queue(q) - res = matrix(tape, wire_order=["a", 0]) - expected = np.kron(np.eye(2), np.diag([1, -1])) - assert np.allclose(res, expected) - assert spy.spy_return[1].tolist() == ["a", 0] - - qs = qml.tape.QuantumScript(tape.operations) - res_qs = matrix(qs, wire_order=["a", 0]) - assert np.allclose(res_qs, expected) - - def test_inconsistent_wires_tape(self): - """Test that an exception is raised if the wire order and tape wires are inconsistent""" - with qml.queuing.AnnotatedQueue() as q: - qml.PauliZ(wires=0) - qml.PauliY(wires="b") - - tape = qml.tape.QuantumScript.from_queue(q) - with pytest.raises( - OperationTransformError, - match=r"Wires in circuit .+ inconsistent with those in wire\_order", - ): - matrix(tape, wire_order=["b", "a"]) - - def test_qfunc(self, mocker): - """Test that wire order can be passed to a qfunc""" - spy = mocker.spy(qml.op_transform, "_make_tape") - - def qfunc(): - qml.PauliZ(wires=0) - - res = matrix(qfunc, wire_order=["a", 0])() - expected = np.kron(np.eye(2), np.diag([1, -1])) - assert np.allclose(res, expected) - assert spy.spy_return[1].tolist() == ["a", 0] - - -def test_op_transform_is_deprecated(): - """Test that the op_transform class is deprecated.""" - - def func(op): - return op - - with pytest.warns( - UserWarning, match="Use of `op_transform` to create a custom transform is deprecated" - ): - _ = qml.op_transform(func) diff --git a/tests/transforms/test_qfunc_transform.py b/tests/transforms/test_qfunc_transform.py deleted file mode 100644 index dafcc8080e8..00000000000 --- a/tests/transforms/test_qfunc_transform.py +++ /dev/null @@ -1,519 +0,0 @@ -# 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. -""" -Unit tests for the qfunc transform decorators. -""" -# pylint:disable=no-value-for-parameter -import pytest - -import pennylane as qml -from pennylane import numpy as np - - -pytestmark = pytest.mark.filterwarnings( - "ignore:.*qfunc_transform.*:pennylane.PennyLaneDeprecationWarning" -) - - -class TestSingleTapeTransform: - """Tests for the single_tape_transform decorator""" - - def test_single_tape_transform_is_deprecated(self): - """Test that the single_tape_transform class is deprecated.""" - - def func(op): - return op - - with pytest.warns( - UserWarning, - match="Use of `single_tape_transform` to create a custom transform is deprecated", - ): - _ = qml.single_tape_transform(func) - - def test_error_invalid_callable(self): - """Test that an error is raised if the transform - is applied to an invalid function""" - - with pytest.raises(ValueError, match="does not appear to be a valid Python function"): - qml.single_tape_transform(5) - - @pytest.mark.parametrize("shots", [None, 100]) - def test_parametrized_transform(self, shots): - """Test that a parametrized transform can be applied - to a tape""" - - def my_transform(tape, a, b): - for op in tape: - if op.name == "CRX": - wires = op.wires - param = op.parameters[0] - qml.RX(a, wires=wires[1]) - qml.RY(qml.math.sum(b) * param / 2, wires=wires[1]) - qml.CZ(wires=[wires[1], wires[0]]) - else: - op.queue() - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="Use of `single_tape_transform`"): - my_transform = qml.single_tape_transform(my_transform) - - a = 0.1 - b = np.array([0.2, 0.3]) - x = 0.543 - - with qml.queuing.AnnotatedQueue() as q: - qml.Hadamard(wires=0) - qml.CRX(x, wires=[0, 1]) - - tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - new_tape = my_transform(tape, a, b) - ops = new_tape.operations - - assert len(ops) == 4 - assert ops[0].name == "Hadamard" - - assert ops[1].name == "RX" - assert ops[1].parameters == [a] - - assert ops[2].name == "RY" - assert ops[2].parameters == [np.sum(b) * x / 2] - - assert ops[3].name == "CZ" - - assert new_tape.shots == tape.shots - - -class TestQFuncTransforms: - """Tests for the qfunc_transform decorator""" - - def test_qfunc_transform_is_deprecated(self): - """Test that the qfunc_transform class is deprecated.""" - - def func(op): - return op - - with pytest.warns( - UserWarning, match="Use of `qfunc_transform` to create a custom transform is deprecated" - ): - _ = qml.qfunc_transform(func) - - def test_error_invalid_transform_callable(self): - """Test that an error is raised if the transform - is applied to an invalid function""" - - with pytest.raises( - ValueError, match="can only be applied to single tape transform functions" - ): - qml.qfunc_transform(5) - - def test_error_invalid_qfunc(self): - """Test that an error is raised if the transform - is applied to an invalid function""" - - def identity_transform(tape): - for op in tape: - op.queue() - - my_transform = qml.qfunc_transform(identity_transform) - - with pytest.raises(ValueError, match="does not appear to be a valid Python function"): - my_transform(5) - - def test_unparametrized_transform(self): - """Test that an unparametrized transform can be applied - to a quantum function""" - - def my_transform(tape): - for op in tape: - if op.name == "CRX": - wires = op.wires - param = op.parameters[0] - qml.RX(param, wires=wires[1]) - qml.RY(param / 2, wires=wires[1]) - qml.CZ(wires=[wires[1], wires[0]]) - else: - op.queue() - - my_transform = qml.qfunc_transform(my_transform) - - def qfunc(x): - qml.Hadamard(wires=0) - qml.CRX(x, wires=[0, 1]) - - new_qfunc = my_transform(qfunc) # pylint:disable=assignment-from-no-return - x = 0.543 - - ops = qml.tape.make_qscript(new_qfunc)(x).operations - assert len(ops) == 4 - assert ops[0].name == "Hadamard" - - assert ops[1].name == "RX" - assert ops[1].parameters == [x] - - assert ops[2].name == "RY" - assert ops[2].parameters == [x / 2] - - assert ops[3].name == "CZ" - - def test_unparametrized_transform_decorator(self): - """Test that an unparametrized transform can be applied - to a quantum function via a decorator""" - - @qml.qfunc_transform - def my_transform(tape): - for op in tape: - if op.name == "CRX": - wires = op.wires - param = op.parameters[0] - qml.RX(param, wires=wires[1]) - qml.RY(param / 2, wires=wires[1]) - qml.CZ(wires=[wires[1], wires[0]]) - else: - op.queue() - - @my_transform - def qfunc(x): - qml.Hadamard(wires=0) - qml.CRX(x, wires=[0, 1]) - - x = 0.543 - ops = qml.tape.make_qscript(qfunc)(x).operations - assert len(ops) == 4 - assert ops[0].name == "Hadamard" - - assert ops[1].name == "RX" - assert ops[1].parameters == [x] - - assert ops[2].name == "RY" - assert ops[2].parameters == [x / 2] - - assert ops[3].name == "CZ" - - def test_parametrized_transform(self): - """Test that a parametrized transform can be applied - to a quantum function""" - - def my_transform(tape, a, b): - for op in tape: - if op.name == "CRX": - wires = op.wires - param = op.parameters[0] - qml.RX(a, wires=wires[1]) - qml.RY(qml.math.sum(b) * param / 2, wires=wires[1]) - qml.CZ(wires=[wires[1], wires[0]]) - else: - op.queue() - - my_transform = qml.qfunc_transform(my_transform) - - def qfunc(x): - qml.Hadamard(wires=0) - qml.CRX(x, wires=[0, 1]) - - a = 0.1 - b = np.array([0.2, 0.3]) - x = 0.543 - new_qfunc = my_transform(a, b)(qfunc) - - ops = qml.tape.make_qscript(new_qfunc)(x).operations - assert len(ops) == 4 - assert ops[0].name == "Hadamard" - - assert ops[1].name == "RX" - assert ops[1].parameters == [a] - - assert ops[2].name == "RY" - assert ops[2].parameters == [np.sum(b) * x / 2] - - assert ops[3].name == "CZ" - - def test_parametrized_transform_decorator(self): - """Test that a parametrized transform can be applied - to a quantum function via a decorator""" - - @qml.qfunc_transform - def my_transform(tape, a, b): - for op in tape: - if op.name == "CRX": - wires = op.wires - param = op.parameters[0] - qml.RX(a, wires=wires[1]) - qml.RY(qml.math.sum(b) * param / 2, wires=wires[1]) - qml.CZ(wires=[wires[1], wires[0]]) - else: - op.queue() - - a = 0.1 - b = np.array([0.2, 0.3]) - x = 0.543 - - @my_transform(a, b) - def qfunc(x): - qml.Hadamard(wires=0) - qml.CRX(x, wires=[0, 1]) - - ops = qml.tape.make_qscript(qfunc)(x).operations - assert len(ops) == 4 - assert ops[0].name == "Hadamard" - - assert ops[1].name == "RX" - assert ops[1].parameters == [a] - - assert ops[2].name == "RY" - assert ops[2].parameters == [np.sum(b) * x / 2] - - assert ops[3].name == "CZ" - - def test_nested_transforms(self): - """Test that nesting multiple transforms works as expected""" - - @qml.qfunc_transform - def convert_cnots(tape): - for op in tape: - if op.name == "CNOT": - wires = op.wires - qml.Hadamard(wires=wires[0]) - qml.CZ(wires=[wires[0], wires[1]]) - else: - op.queue() - - @qml.qfunc_transform - def expand_hadamards(tape, x): - for op in tape: - if op.name == "Hadamard": - qml.RZ(x, wires=op.wires) - else: - op.queue() - - x = 0.5 - - @expand_hadamards(x) - @convert_cnots - def ansatz(): - qml.CNOT(wires=[0, 1]) - - ops = qml.tape.make_qscript(ansatz)().operations - assert len(ops) == 2 - assert ops[0].name == "RZ" - assert ops[0].parameters == [x] - assert ops[1].name == "CZ" - - def test_transform_single_measurement(self): - """Test that transformed functions return a scalar value when there is only - a single measurement.""" - - @qml.qfunc_transform - def expand_hadamards(tape): - for op in tape.operations: - if op.name == "Hadamard": - qml.RZ(np.pi, wires=op.wires) - qml.RY(np.pi / 2, wires=op.wires) - else: - op.queue() - for mp in tape.measurements: - qml.apply(mp) - - def ansatz(): - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliX(wires=1)) - - dev = qml.device("default.qubit", wires=2) - - normal_qnode = qml.QNode(ansatz, dev) - - transformed_ansatz = expand_hadamards(ansatz) - transformed_qnode = qml.QNode(transformed_ansatz, dev) - - normal_result = normal_qnode() - transformed_result = transformed_qnode() - - assert np.allclose(normal_result, transformed_result) - assert normal_result.shape == transformed_result.shape - - def test_transform_does_not_add_return_value_to_qnode(self): - """Tests that qfunc_transform doesn't add an empty list of measurements to a QNode.""" - - @qml.qfunc_transform - def expand_hadamards(tape): - for op in tape: - if op.name == "Hadamard": - qml.RZ(np.pi, wires=op.wires) - qml.RY(np.pi / 2, wires=op.wires) - else: - op.queue() - - @expand_hadamards - def ansatz(): - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - - assert ansatz() is None - - qnode = qml.QNode(ansatz, qml.device("default.qubit", wires=2)) - with pytest.raises(qml.QuantumFunctionError, match="return either a single measurement"): - qnode() - - def test_sphinx_build(self, monkeypatch): - """Test that qfunc transforms are not created during Sphinx builds""" - - def original_fn(tape): - for op in tape: - if op.name == "Hadamard": - qml.RZ(np.pi, wires=op.wires) - qml.RY(np.pi / 2, wires=op.wires) - else: - op.queue() - - decorated_transform = qml.qfunc_transform(original_fn) - assert original_fn is not decorated_transform - - monkeypatch.setenv("SPHINX_BUILD", "1") - - with pytest.warns(UserWarning, match="qfunc transformations have been disabled"): - decorated_transform = qml.qfunc_transform(original_fn) - - assert original_fn is decorated_transform - - -############################################ -# Test transform, ansatz, and qfunc function - - -@pytest.mark.parametrize("diff_method", ["parameter-shift", "backprop"]) -class TestQFuncTransformGradients: - """Tests for the qfunc_transform decorator differentiability""" - - def my_transform(tape, a, b): - """Test transform""" - for op in tape: - if op.name == "CRX": - wires = op.wires - param = op.parameters[0] - qml.RX(a * param, wires=wires[1]) - qml.RY(qml.math.sum(b) * qml.math.sqrt(param), wires=wires[1]) - qml.CZ(wires=[wires[1], wires[0]]) - else: - op.queue() - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="Use of `qfunc_transform`"): - my_transform = staticmethod(qml.qfunc_transform(my_transform)) - - @staticmethod - def ansatz(x): - """Test ansatz""" - qml.Hadamard(wires=0) - qml.CRX(x, wires=[0, 1]) - - @staticmethod - def circuit(param, *transform_weights): - """Test QFunc""" - qml.RX(0.1, wires=0) - TestQFuncTransformGradients.my_transform(*transform_weights)( # pylint:disable=not-callable - TestQFuncTransformGradients.ansatz - )(param) - return qml.expval(qml.PauliZ(1)) - - @staticmethod - def expval(x, a, b): - """Analytic expectation value of the above circuit qfunc""" - return np.cos(np.sum(b) * np.sqrt(x)) * np.cos(a * x) - - @pytest.mark.autograd - def test_differentiable_qfunc_autograd(self, diff_method): - """Test that a qfunc transform is differentiable when using - autograd""" - dev = qml.device("default.qubit", wires=2) - qnode = qml.QNode(self.circuit, dev, diff_method=diff_method) - - a = np.array(0.5, requires_grad=True) - b = np.array([0.1, 0.2], requires_grad=True) - x = np.array(0.543, requires_grad=True) - - res = qnode(x, a, b) - assert np.allclose(res, self.expval(x, a, b)) - - grad = qml.grad(qnode)(x, a, b) - expected = qml.grad(self.expval)(x, a, b) - assert all(np.allclose(g, e) for g, e in zip(grad, expected)) - - @pytest.mark.tf - def test_differentiable_qfunc_tf(self, diff_method): - """Test that a qfunc transform is differentiable when using - TensorFlow""" - import tensorflow as tf - - dev = qml.device("default.qubit", wires=2) - qnode = qml.QNode(self.circuit, dev, diff_method=diff_method) - - a_np = np.array(0.5, requires_grad=True) - b_np = np.array([0.1, 0.2], requires_grad=True) - x_np = np.array(0.543, requires_grad=True) - a = tf.Variable(a_np, dtype=tf.float64) - b = tf.Variable(b_np, dtype=tf.float64) - x = tf.Variable(x_np, dtype=tf.float64) - - with tf.GradientTape() as tape: - res = qnode(x, a, b) - - assert np.allclose(res, self.expval(x, a, b)) - - grad = tape.gradient(res, [x, a, b]) - expected = qml.grad(self.expval)(x_np, a_np, b_np) - assert all(np.allclose(g, e) for g, e in zip(grad, expected)) - - @pytest.mark.torch - def test_differentiable_qfunc_torch(self, diff_method): - """Test that a qfunc transform is differentiable when using - PyTorch""" - import torch - - dev = qml.device("default.qubit", wires=2) - qnode = qml.QNode(self.circuit, dev, diff_method=diff_method) - - a_np = np.array(0.5, requires_grad=True) - b_np = np.array([0.1, 0.2], requires_grad=True) - x_np = np.array(0.543, requires_grad=True) - a = torch.tensor(a_np, requires_grad=True) - b = torch.tensor(b_np, requires_grad=True) - x = torch.tensor(x_np, requires_grad=True) - - res = qnode(x, a, b) - expected = self.expval(x_np, a_np, b_np) - assert np.allclose(res.detach().numpy(), expected) - - res.backward() - expected = qml.grad(self.expval)(x_np, a_np, b_np) - assert np.allclose(x.grad, expected[0]) - assert np.allclose(a.grad, expected[1]) - assert np.allclose(b.grad, expected[2]) - - @pytest.mark.jax - def test_differentiable_qfunc_jax(self, diff_method): - """Test that a qfunc transform is differentiable when using - jax""" - import jax - - dev = qml.device("default.qubit", wires=2) - qnode = qml.QNode(self.circuit, dev, diff_method=diff_method) - - a = jax.numpy.array(0.5) - b = jax.numpy.array([0.1, 0.2]) - x = jax.numpy.array(0.543) - - res = qnode(x, a, b) - assert np.allclose(res, self.expval(x, a, b)) - - grad = jax.grad(qnode, argnums=[0, 1, 2])(x, a, b) - expected = qml.grad(self.expval)(np.array(x), np.array(a), np.array(b)) - assert all(np.allclose(g, e) for g, e in zip(grad, expected)) diff --git a/tests/transforms/test_zx.py b/tests/transforms/test_zx.py index a0e25a75fab..cdcf1696922 100644 --- a/tests/transforms/test_zx.py +++ b/tests/transforms/test_zx.py @@ -21,7 +21,7 @@ import pytest import pennylane as qml from pennylane.tape import QuantumScript -from pennylane.transforms.op_transforms import OperationTransformError +from pennylane.transforms import TransformError pyzx = pytest.importorskip("pyzx") @@ -72,7 +72,7 @@ class TestConvertersZX: def test_invalid_argument(self): """Assert error raised when input is neither a tape, QNode, nor quantum function""" with pytest.raises( - OperationTransformError, + TransformError, match="Input is not an Operator, tape, QNode, or quantum function", ): _ = qml.transforms.to_zx(None) From 9187aea93f2c6487a8796dcce0c05c93b4876936 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 7 Mar 2024 15:13:44 -0500 Subject: [PATCH 2/8] Fix imports --- pennylane/gradients/gradient_transform.py | 1 - pennylane/gradients/hessian_transform.py | 2 -- pennylane/transforms/__init__.py | 6 ++++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pennylane/gradients/gradient_transform.py b/pennylane/gradients/gradient_transform.py index 6b3d9a9ef58..3fc77d4ebf7 100644 --- a/pennylane/gradients/gradient_transform.py +++ b/pennylane/gradients/gradient_transform.py @@ -18,7 +18,6 @@ import warnings import pennylane as qml -from pennylane.transforms.tape_expand import expand_invalid_trainable from pennylane.measurements import ( MutualInfoMP, StateMP, diff --git a/pennylane/gradients/hessian_transform.py b/pennylane/gradients/hessian_transform.py index 1273debf570..d2ad1982f5d 100644 --- a/pennylane/gradients/hessian_transform.py +++ b/pennylane/gradients/hessian_transform.py @@ -13,11 +13,9 @@ # limitations under the License. """This module contains utilities for defining custom Hessian transforms, including a decorator for specifying Hessian expansions.""" -import warnings from string import ascii_letters as ABC import pennylane as qml -from pennylane.transforms.tape_expand import expand_invalid_trainable def _process_jacs(jac, qhess): diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index ca16ac1b180..c8975114acd 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -271,6 +271,10 @@ def circuit(x, y): Discover quantum information transformations in the :doc:`quantum information documentation <../code/qml_qinfo>`. Finally, for a comprehensive overview of transforms and core functionalities, consult the :doc:`transforms documentation <../code/qml_transforms>`. """ + +# Leave as alias for backwards-compatibility +from pennylane.tape import make_qscript as make_tape + # Import the decorators first to prevent circular imports when used in other transforms from .core import transform, TransformError from .batch_transform import map_batch_transform @@ -322,5 +326,3 @@ def circuit(x, y): from .transpile import transpile from .zx import to_zx, from_zx from .broadcast_expand import broadcast_expand - -from pennylane.tape import make_qscript as make_tape From b59342084dbcc34cf6e70731a2802fac006902f6 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 7 Mar 2024 15:15:10 -0500 Subject: [PATCH 3/8] Remove unused import --- pennylane/transforms/batch_transform.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pennylane/transforms/batch_transform.py b/pennylane/transforms/batch_transform.py index 6f8fc968e4a..43aeede21bb 100644 --- a/pennylane/transforms/batch_transform.py +++ b/pennylane/transforms/batch_transform.py @@ -13,12 +13,6 @@ # limitations under the License. """Contains tools and decorators for registering batch transforms.""" # pylint: disable=too-few-public-methods -import copy -import functools -import inspect -import os -import types -import warnings from typing import Callable, Tuple From e2d3060780897def1ca03d55d54e1e819b4d8f12 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 7 Mar 2024 15:23:32 -0500 Subject: [PATCH 4/8] remove unused import from test --- tests/transforms/test_batch_transform.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/transforms/test_batch_transform.py b/tests/transforms/test_batch_transform.py index 3e43d091bd9..6c5cc32f893 100644 --- a/tests/transforms/test_batch_transform.py +++ b/tests/transforms/test_batch_transform.py @@ -15,8 +15,6 @@ Unit tests for the batch transform. """ # pylint: disable=too-few-public-methods,not-callable -import functools -import pytest import pennylane as qml from pennylane import numpy as np From d17b4875529153d94e35923ca41eddcbdc229e2e Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 7 Mar 2024 16:21:32 -0500 Subject: [PATCH 5/8] Remove hessian_transform.py --- doc/releases/changelog-dev.md | 1 + pennylane/gradients/hessian_transform.py | 64 ------------------- .../gradients/parameter_shift_hessian.py | 48 +++++++++++++- 3 files changed, 48 insertions(+), 65 deletions(-) delete mode 100644 pennylane/gradients/hessian_transform.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 37f889c0f46..f4aec072f74 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -66,6 +66,7 @@ and ``hessian_transform`` are removed. Instead, switch to using the new ``qml.transform`` function. Please refer to `the transform docs `_ to see how this can be done. + [(#5339)](https://github.com/PennyLaneAI/pennylane/pull/5339)

Deprecations 👋

diff --git a/pennylane/gradients/hessian_transform.py b/pennylane/gradients/hessian_transform.py deleted file mode 100644 index d2ad1982f5d..00000000000 --- a/pennylane/gradients/hessian_transform.py +++ /dev/null @@ -1,64 +0,0 @@ -# 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 utilities for defining custom Hessian transforms, -including a decorator for specifying Hessian expansions.""" -from string import ascii_letters as ABC - -import pennylane as qml - - -def _process_jacs(jac, qhess): - """ - Combine the classical and quantum jacobians - """ - # Check for a Jacobian equal to the identity matrix. - if not qml.math.is_abstract(jac): - shape = qml.math.shape(jac) - is_square = len(shape) == 2 and shape[0] == shape[1] - if is_square and qml.math.allclose(jac, qml.numpy.eye(shape[0])): - return qhess if len(qhess) > 1 else qhess[0] - - hess = [] - for qh in qhess: - if not isinstance(qh, tuple) or not isinstance(qh[0], tuple): - # single parameter case - qh = qml.math.expand_dims(qh, [0, 1]) - else: - # multi parameter case - qh = qml.math.stack([qml.math.stack(row) for row in qh]) - - jac_ndim = len(qml.math.shape(jac)) - - # The classical jacobian has shape (num_params, num_qnode_args) - # The quantum Hessian has shape (num_params, num_params, output_shape) - # contracting the quantum Hessian with the classical jacobian twice gives - # a result with shape (num_qnode_args, num_qnode_args, output_shape) - - qh_indices = "ab..." - - # contract the first axis of the jacobian with the first and second axes of the Hessian - first_jac_indices = f"a{ABC[2:2 + jac_ndim - 1]}" - second_jac_indices = f"b{ABC[2 + jac_ndim - 1:2 + 2 * jac_ndim - 2]}" - - result_indices = f"{ABC[2:2 + 2 * jac_ndim - 2]}..." - qh = qml.math.einsum( - f"{qh_indices},{first_jac_indices},{second_jac_indices}->{result_indices}", - qh, - jac, - jac, - ) - - hess.append(qh) - - return tuple(hess) if len(hess) > 1 else hess[0] diff --git a/pennylane/gradients/parameter_shift_hessian.py b/pennylane/gradients/parameter_shift_hessian.py index d80117b8a45..a0c6a57b116 100644 --- a/pennylane/gradients/parameter_shift_hessian.py +++ b/pennylane/gradients/parameter_shift_hessian.py @@ -17,6 +17,7 @@ """ import itertools as it import warnings +from abc import ABC from functools import partial from typing import Sequence, Callable @@ -32,7 +33,52 @@ ) from .gradient_transform import find_and_validate_gradient_methods from .parameter_shift import _get_operation_recipe -from .hessian_transform import _process_jacs + + +def _process_jacs(jac, qhess): + """ + Combine the classical and quantum jacobians + """ + # Check for a Jacobian equal to the identity matrix. + if not qml.math.is_abstract(jac): + shape = qml.math.shape(jac) + is_square = len(shape) == 2 and shape[0] == shape[1] + if is_square and qml.math.allclose(jac, qml.numpy.eye(shape[0])): + return qhess if len(qhess) > 1 else qhess[0] + + hess = [] + for qh in qhess: + if not isinstance(qh, tuple) or not isinstance(qh[0], tuple): + # single parameter case + qh = qml.math.expand_dims(qh, [0, 1]) + else: + # multi parameter case + qh = qml.math.stack([qml.math.stack(row) for row in qh]) + + jac_ndim = len(qml.math.shape(jac)) + + # The classical jacobian has shape (num_params, num_qnode_args) + # The quantum Hessian has shape (num_params, num_params, output_shape) + # contracting the quantum Hessian with the classical jacobian twice gives + # a result with shape (num_qnode_args, num_qnode_args, output_shape) + + qh_indices = "ab..." + + # contract the first axis of the jacobian with the first and second axes of the Hessian + first_jac_indices = f"a{ABC[2:2 + jac_ndim - 1]}" + second_jac_indices = f"b{ABC[2 + jac_ndim - 1:2 + 2 * jac_ndim - 2]}" + + result_indices = f"{ABC[2:2 + 2 * jac_ndim - 2]}..." + qh = qml.math.einsum( + f"{qh_indices},{first_jac_indices},{second_jac_indices}->{result_indices}", + qh, + jac, + jac, + ) + + hess.append(qh) + + return tuple(hess) if len(hess) > 1 else hess[0] def _process_argnum(argnum, tape): From 0c658166ef7661ceefce388f8a18e95ca4c71273 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 7 Mar 2024 16:54:25 -0500 Subject: [PATCH 6/8] fix wrong import --- pennylane/gradients/parameter_shift_hessian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/gradients/parameter_shift_hessian.py b/pennylane/gradients/parameter_shift_hessian.py index a0c6a57b116..8d600053d66 100644 --- a/pennylane/gradients/parameter_shift_hessian.py +++ b/pennylane/gradients/parameter_shift_hessian.py @@ -17,7 +17,7 @@ """ import itertools as it import warnings -from abc import ABC +from string import ascii_letters as ABC from functools import partial from typing import Sequence, Callable From c67e4adadc5e684062abd9e80ebdfdc65e5b4469 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Fri, 8 Mar 2024 11:44:14 -0500 Subject: [PATCH 7/8] Update doc/development/deprecations.rst Co-authored-by: Thomas R. Bromley <49409390+trbromley@users.noreply.github.com> --- doc/development/deprecations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst index ced98be0505..56ab6c5d404 100644 --- a/doc/development/deprecations.rst +++ b/doc/development/deprecations.rst @@ -57,7 +57,7 @@ Completed deprecation cycles ---------------------------- * ``single_tape_transform``, ``batch_transform``, ``qfunc_transform``, ``op_transform``, - `` gradient_transform`` and ``hessian_transform`` are deprecated. Instead switch to using the new + ``gradient_transform`` and ``hessian_transform`` are deprecated. Instead switch to using the new ``qml.transform`` function. Please refer to `the transform docs `_ to see how this can be done. From b36f75a8ef8afbd9a50d1d4557065db6284d782b Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Mon, 11 Mar 2024 10:31:35 -0400 Subject: [PATCH 8/8] Update __init__.py --- pennylane/transforms/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index c8975114acd..e96abb880d2 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -19,8 +19,6 @@ Custom transforms ----------------- -.. _transforms_custom_transforms: - :func:`qml.transform ` can be used to define custom transformations that work with PennyLane QNodes; such transformations can map a circuit to one or many new circuits alongside associated classical post-processing.