From c3b65187be062060cf2293056a84e44945f90e9c Mon Sep 17 00:00:00 2001 From: Tom Bromley <49409390+trbromley@users.noreply.github.com> Date: Fri, 27 Oct 2023 16:07:46 -0400 Subject: [PATCH] Update transforms documentation (#4682) **Description of the Change:** - `qml.transforms.core.transform` is now top level `qml.transform` - Update `qml.transforms` page - Update the documentation for `qml.transforms.core.transform` - Update thar args and returns of all transform using `qml.transforms.core.transform` --------- Co-authored-by: rmoyard Co-authored-by: Mudit Pandey Co-authored-by: Matthew Silverman Co-authored-by: BM7878 <117289949+BM7878@users.noreply.github.com> Co-authored-by: Josh Izaac --- doc/development/deprecations.rst | 58 +++-- doc/introduction/compiling_circuits.rst | 8 +- pennylane/__init__.py | 1 + pennylane/devices/device_api.py | 2 +- pennylane/devices/preprocess.py | 63 +++-- pennylane/fourier/circuit_spectrum.py | 11 +- pennylane/gradients/__init__.py | 22 +- pennylane/gradients/finite_difference.py | 24 +- pennylane/gradients/hadamard_gradient.py | 153 ++++++------ pennylane/gradients/parameter_shift.py | 24 +- pennylane/gradients/parameter_shift_cv.py | 21 +- .../gradients/parameter_shift_hessian.py | 23 +- pennylane/gradients/pulse_gradient.py | 18 +- pennylane/gradients/pulse_gradient_odegen.py | 20 +- pennylane/gradients/spsa_gradient.py | 20 +- pennylane/interfaces/execution.py | 2 +- pennylane/interfaces/jacobian_products.py | 6 +- pennylane/ops/functions/eigvals.py | 13 +- pennylane/ops/functions/map_wires.py | 9 +- pennylane/ops/functions/matrix.py | 13 +- pennylane/optimize/adaptive.py | 11 +- pennylane/optimize/riemannian_gradient.py | 11 +- pennylane/qinfo/transforms.py | 49 ++-- pennylane/qnode.py | 22 +- pennylane/shadows/transforms.py | 22 +- pennylane/transforms/__init__.py | 132 ++++++---- pennylane/transforms/adjoint_metric_tensor.py | 10 +- pennylane/transforms/batch_input.py | 10 +- pennylane/transforms/batch_params.py | 9 +- pennylane/transforms/broadcast_expand.py | 8 +- pennylane/transforms/commutation_dag.py | 13 +- pennylane/transforms/compile.py | 41 +++- pennylane/transforms/core/transform.py | 142 +++++++---- .../transforms/core/transform_dispatcher.py | 85 ++++--- .../transforms/core/transform_program.py | 23 +- pennylane/transforms/defer_measurements.py | 10 +- pennylane/transforms/hamiltonian_expand.py | 10 +- pennylane/transforms/insert_ops.py | 16 +- pennylane/transforms/metric_tensor.py | 16 +- pennylane/transforms/mitigate.py | 24 +- .../optimization/cancel_inverses.py | 81 ++++--- .../optimization/commute_controlled.py | 85 ++++--- .../optimization/merge_amplitude_embedding.py | 58 +++-- .../optimization/merge_rotations.py | 95 +++++--- .../optimization/pattern_matching.py | 229 ++++++++++-------- .../transforms/optimization/remove_barrier.py | 56 +++-- .../optimization/single_qubit_fusion.py | 54 +++-- .../transforms/optimization/undo_swaps.py | 63 +++-- pennylane/transforms/qcut/cutcircuit.py | 12 +- pennylane/transforms/qcut/montecarlo.py | 11 +- .../transforms/sign_expand/sign_expand.py | 8 +- pennylane/transforms/split_non_commuting.py | 10 +- pennylane/transforms/transpile.py | 12 +- pennylane/transforms/unitary_to_rot.py | 10 +- pennylane/transforms/zx/converter.py | 13 +- .../test_transform_dispatcher.py | 15 ++ tests/transforms/test_specs.py | 12 +- 57 files changed, 1140 insertions(+), 859 deletions(-) diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst index 67518d937c1..7780925d278 100644 --- a/doc/development/deprecations.rst +++ b/doc/development/deprecations.rst @@ -6,6 +6,41 @@ Deprecations Pending deprecations -------------------- +* Passing additional arguments to a transform that decorates a QNode should now be done through use + of ``functools.partial``. For example, the :func:`~pennylane.metric_tensor` transform has an + optional ``approx`` argument which should now be set using: + + .. code-block:: python + + from functools import partial + + @partial(qml.metric_tensor, approx="block-diag") + @qml.qnode(dev) + def circuit(weights): + ... + + The previously-recommended approach is now deprecated: + + .. code-block:: python + + @qml.metric_tensor(approx="block-diag") + @qml.qnode(dev) + def circuit(weights): + ... + + Alternatively, consider calling the transform directly: + + .. code-block:: python + + @qml.qnode(dev) + def circuit(weights): + ... + + transformed_circuit = qml.metric_tensor(circuit, approx="block-diag") + + - Deprecated in v0.33 + - Will be removed in v0.35 + * ``qml.ExpvalCost`` has been deprecated, and usage will now raise a warning. - Deprecated in v0.24 @@ -155,29 +190,6 @@ Completed deprecation cycles - Deprecated in v0.32 - Removed in v0.33 -* The following decorator syntax for transforms has been deprecated: - - .. code-block:: python - - @transform_fn(**transform_kwargs) - @qml.qnode(dev) - def circuit(): - ... - - If you are using a transform that has supporting ``transform_kwargs``, please call the - transform directly using ``circuit = transform_fn(circuit, **transform_kwargs)``, - or use ``functools.partial``: - - .. code-block:: python - - @functools.partial(transform_fn, **transform_kwargs) - @qml.qnode(dev) - def circuit(): - ... - - - Deprecated in v0.33 - - Will be removed in v0.34 - * The ``mode`` keyword argument in ``QNode`` has been removed, as it was only used in the old return system (which has also been removed). Please use ``grad_on_execution`` instead. diff --git a/doc/introduction/compiling_circuits.rst b/doc/introduction/compiling_circuits.rst index 843717907e4..ef82985471d 100644 --- a/doc/introduction/compiling_circuits.rst +++ b/doc/introduction/compiling_circuits.rst @@ -135,9 +135,9 @@ For example, take the following decorated quantum function: dev = qml.device('default.qubit', wires=[0, 1, 2]) + @qml.compile @qml.qnode(dev) - @qml.compile() - def qfunc(x, y, z): + def circuit(x, y, z): qml.Hadamard(wires=0) qml.Hadamard(wires=1) qml.Hadamard(wires=2) @@ -157,7 +157,7 @@ The default behaviour of :func:`~.pennylane.compile` applies a sequence of three transforms: :func:`~.pennylane.transforms.commute_controlled`, :func:`~.pennylane.transforms.cancel_inverses`, and then :func:`~.pennylane.transforms.merge_rotations`. ->>> print(qml.draw(qfunc)(0.2, 0.3, 0.4)) +>>> print(qml.draw(circuit)(0.2, 0.3, 0.4)) 0: ──H──RX(0.60)─────────────────┤ 1: ──H─╭X─────────────────────╭●─┤ 2: ──H─╰●─────────RX(0.30)──Y─╰Z─┤ @@ -173,8 +173,8 @@ controlled gates and cancel adjacent inverses, we could do: from pennylane.transforms import commute_controlled, cancel_inverses pipeline = [commute_controlled, cancel_inverses] + @partial(qml.compile, pipeline=pipeline) @qml.qnode(dev) - @qml.compile(pipeline=pipeline) def qfunc(x, y, z): qml.Hadamard(wires=0) qml.Hadamard(wires=1) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 26688efd266..dc068bea6ea 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -82,6 +82,7 @@ from pennylane import qaoa from pennylane.qnode import QNode, qnode from pennylane.transforms import ( + transform, adjoint_metric_tensor, batch_params, batch_input, diff --git a/pennylane/devices/device_api.py b/pennylane/devices/device_api.py index 9c07e6c4d2c..f9b6da37a54 100644 --- a/pennylane/devices/device_api.py +++ b/pennylane/devices/device_api.py @@ -236,7 +236,7 @@ def preprocess( **Example** All the transforms that are part of the preprocessing need to respect the transform contract defined in - :func:`pennylane.transforms.core.transform`. + :func:`pennylane.transform`. .. code-block:: python diff --git a/pennylane/devices/preprocess.py b/pennylane/devices/preprocess.py index 7444e96bf39..8b988048d9f 100644 --- a/pennylane/devices/preprocess.py +++ b/pennylane/devices/preprocess.py @@ -30,7 +30,7 @@ ) from pennylane.typing import ResultBatch, Result from pennylane import DeviceError -from pennylane.transforms.core import transform +from pennylane import transform from pennylane.wires import WireError PostprocessingFn = Callable[[ResultBatch], Union[Result, ResultBatch]] @@ -87,8 +87,14 @@ def no_sampling( """Raises an error if the tape has finite shots. Args: - tape (QuantumTape): a quantum circuit - name="device" (str): name to use in error message. + tape (QuantumTape or .QNode or Callable): a quantum circuit + name (str): name to use in error message. + + Returns: + qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function]: + + The unaltered input circuit. The output type is explained in :func:`qml.transform `. + This transform can be added to forbid finite shots. For example, ``default.qubit`` uses it for adjoint and backprop validation. @@ -107,17 +113,15 @@ def validate_device_wires( across all available wires. Args: - tape (QuantumTape): a quantum circuit. + tape (QuantumTape or QNode or Callable): a quantum circuit. wires=None (Optional[Wires]): the allowed wires. Wires of ``None`` allows any wires to be present in the tape. name="device" (str): the name of the device to use in error messages. Returns: - pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The unaltered input circuit. The output type is explained in :func:`qml.transform `. Raises: WireError: if the tape has a wire not present in the provided wires. @@ -153,16 +157,15 @@ def validate_multiprocessing_workers( threads per worker. Args: - tape (QuantumTape): a quantum circuit. + tape (QuantumTape or .QNode or Callable): a quantum circuit. max_workers (int): Maximal number of multiprocessing workers device (pennylane.devices.Device): The device to be checked. Returns: - pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (pennylane.QNode) or quantum function (callable) or tuple[List[.QuantumTape], function]: + + The unaltered input circuit. The output type is explained in :func:`qml.transform `. + """ if max_workers is not None: threads_per_proc = os.cpu_count() # all threads by default @@ -238,7 +241,7 @@ def decompose( """Decompose operations until the stopping condition is met. Args: - tape (QuantumTape): a quantum circuit. + tape (QuantumTape or QNode or Callable): a quantum circuit. stopping_condition (Callable): a function from an operator to a boolean. If ``False``, the operator should be decomposed. If an operator cannot be decomposed and is not accepted by ``stopping_condition``, a ``DecompositionUndefinedError`` will be raised. @@ -249,11 +252,9 @@ def decompose( Returns: - pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The decomposed circuit. The output type is explained in :func:`qml.transform `. Raises: DecompositionUndefinedError: if an operator is not accepted and does not define a decomposition @@ -339,16 +340,14 @@ def validate_observables( """Validates the observables and measurements for a circuit. Args: - tape (QuantumTape): a quantum circuit. + tape (QuantumTape or QNode or Callable): a quantum circuit. stopping_condition (callable): a function that specifies whether or not an observable is accepted. name (str): the name of the device to use in error messages. Returns: - pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function]: + + The unaltered input circuit. The output type is explained in :func:`qml.transform `. Raises: DeviceError: if an observable is not supported @@ -383,7 +382,7 @@ def validate_measurements( """Validates the supported state and sample based measurement processes. Args: - tape (QuantumTape): a quantum circuit. + tape (QuantumTape, .QNode, Callable): a quantum circuit. analytic_measurements (Callable[[MeasurementProcess], bool]): a function from a measurement process to whether or not it is accepted in analytic simulations. sample_measurements (Callable[[MeasurementProcess], bool]): a function from a measurement process @@ -391,11 +390,9 @@ def validate_measurements( name (str): the name to use in error messages. Returns: - pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (pennylane.QNode) or quantum function (callable) or tuple[List[.QuantumTape], function]: + + The unaltered input circuit. The output type is explained in :func:`qml.transform `. Raises: DeviceError: if a measurement process is not supported. diff --git a/pennylane/fourier/circuit_spectrum.py b/pennylane/fourier/circuit_spectrum.py index fd861f1c604..5c95f32b5dd 100644 --- a/pennylane/fourier/circuit_spectrum.py +++ b/pennylane/fourier/circuit_spectrum.py @@ -16,7 +16,7 @@ preprocessing in the QNode.""" from typing import Sequence, Callable from functools import partial -from pennylane.transforms.core import transform +from pennylane import transform from pennylane.tape import QuantumTape from .utils import get_spectrum, join_spectra @@ -48,15 +48,17 @@ def circuit_spectrum( If no input-encoding gates are found, an empty dictionary is returned. Args: - tape (QuantumTape): a quantum node representing a circuit in which + tape (QNode or QuantumTape or Callable): a quantum circuit in which input-encoding gates are marked by their ``id`` attribute encoding_gates (list[str]): list of input-encoding gate ``id`` strings for which to compute the frequency spectra decimals (int): number of decimals to which to round frequencies. Returns: - (dict[str, list[float]]): Dictionary with the input-encoding gate ``id`` as keys and - their frequency spectra as values. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will return a dictionary with the input-encoding gate ``id`` as keys and their frequency spectra as values. **Details** @@ -185,6 +187,7 @@ def circuit(x): """ def processing_fn(tapes): + """Process the tapes extract the spectrum of the circuit.""" tape = tapes[0] freqs = {} for op in tape.operations: diff --git a/pennylane/gradients/__init__.py b/pennylane/gradients/__init__.py index af928852a82..29137187c13 100644 --- a/pennylane/gradients/__init__.py +++ b/pennylane/gradients/__init__.py @@ -51,15 +51,6 @@ stoch_pulse_grad pulse_odegen -Custom gradients -^^^^^^^^^^^^^^^^ - -.. autosummary:: - :toctree: api - - gradient_transform - hessian_transform - Utility functions ^^^^^^^^^^^^^^^^^ @@ -131,7 +122,10 @@ def circuit(weights): Transforming QNodes ------------------- -Alternatively, quantum gradient transforms can be applied manually to QNodes. +Alternatively, quantum gradient transforms can be applied manually to QNodes. This is not +recommended because PennyLane must compute the classical Jacobian of the parameters and multiply it with +the quantum Jacobian, we recommend using the ``diff_method`` kwargs with your favorite machine learning +framework. .. code-block:: python @@ -301,13 +295,13 @@ def circuit(weights): Custom gradient transforms -------------------------- -Using the :class:`~.gradient_transform` decorator, custom gradient transforms +Using the :func:`qml.transform ` decorator, custom gradient transforms can be created: .. code-block:: python - @gradient_transform - def my_custom_gradient(tape, **kwargs): + @transform + def my_custom_gradient(tape: qml.tape.QuantumTape, **kwargs) -> (Sequence[qml.tape.QuantumTape], Callable): ... return gradient_tapes, processing_fn @@ -315,7 +309,7 @@ def my_custom_gradient(tape, **kwargs): to QNodes, or registered as the quantum gradient transform to use during autodifferentiation. -For more details, please see the :class:`~.gradient_transform` +For more details, please see the :func:`qml.transform ` documentation. """ import pennylane as qml diff --git a/pennylane/gradients/finite_difference.py b/pennylane/gradients/finite_difference.py index d925c57853a..e69893679d7 100644 --- a/pennylane/gradients/finite_difference.py +++ b/pennylane/gradients/finite_difference.py @@ -26,7 +26,7 @@ import pennylane as qml from pennylane.measurements import ProbabilityMP -from pennylane.transforms.core import transform +from pennylane import transform from pennylane.transforms.tape_expand import expand_invalid_trainable from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac @@ -210,10 +210,10 @@ def finite_diff( f0=None, validate_params=True, ) -> (Sequence[qml.tape.QuantumTape], Callable): - r"""Transform a QNode to compute the finite-difference gradient of all gate parameters with respect to its inputs. + r"""Transform a circuit to compute the finite-difference gradient of all gate parameters with respect to its inputs. Args: - tape (pennylane.QNode or .QuantumTape): quantum tape or QNode to differentiate + tape (QNode or QuantumTape): quantum circuit to differentiate argnum (int or list[int] or None): Trainable parameter indices to differentiate with respect to. If not provided, the derivatives with respect to all trainable parameters are returned. @@ -237,17 +237,11 @@ def finite_diff( If ``False``, the finite-difference method will be applied to all parameters. Returns: - function or tuple[list[QuantumTape], function]: + qnode (QNode) or tuple[List[QuantumTape], function]: - - If the input is a QNode, an object representing the Jacobian (function) of the QNode - that can be executed to obtain the Jacobian. - The type of the Jacobian returned is either a tensor, a tuple or a - nested tuple depending on the nesting structure of the original QNode output. - - - If the input is a tape, a tuple containing a - list of generated tapes, together with a post-processing - function to be applied to the results of the evaluated tapes - in order to obtain the Jacobian. + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the Jacobian in the form of a tensor, a tuple, or a nested tuple depending upon the nesting + structure of measurements in the original circuit. **Example** @@ -287,7 +281,9 @@ def finite_diff( .. details:: :title: Usage Details - This gradient transform can also be applied directly to :class:`QNode ` objects: + This gradient transform can be applied directly to :class:`QNode ` objects. + However, for performance reasons, we recommend providing the gradient transform as the ``diff_method`` argument + of the QNode decorator, and differentiating with your preferred machine learning framework. >>> @qml.qnode(dev) ... def circuit(params): diff --git a/pennylane/gradients/hadamard_gradient.py b/pennylane/gradients/hadamard_gradient.py index edac693b90e..91bd0ab09b4 100644 --- a/pennylane/gradients/hadamard_gradient.py +++ b/pennylane/gradients/hadamard_gradient.py @@ -21,7 +21,7 @@ import pennylane as qml import pennylane.numpy as np from pennylane.transforms.metric_tensor import _get_aux_wire -from pennylane.transforms.core import transform +from pennylane import transform from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac from pennylane.transforms.tape_expand import expand_invalid_trainable_hadamard_gradient @@ -66,10 +66,10 @@ def hadamard_grad( aux_wire=None, device_wires=None, ) -> (Sequence[qml.tape.QuantumTape], Callable): - r"""Transform a QNode to compute the Hadamard test gradient of all gates with respect to their inputs. + r"""Transform a circuit to compute the Hadamard test gradient of all gates with respect to their inputs. Args: - tape (pennylane.QNode or .QuantumTape): quantum tape or QNode to differentiate + tape (QNode or QuantumTape): quantum circuit to differentiate argnum (int or list[int] or None): Trainable tape parameter indices to differentiate with respect to. If not provided, the derivatives with respect to all trainable parameters are returned. @@ -80,17 +80,11 @@ def hadamard_grad( is ``None``. Returns: - function or tuple[list[QuantumTape], function]: + qnode (QNode) or tuple[List[QuantumTape], function]: - - If the input is a QNode, an object representing the Jacobian (function) of the QNode - that can be executed to obtain the Jacobian. - The type of the Jacobian returned is either a tensor, a tuple or a - nested tuple depending on the nesting structure of the original QNode output. - - - If the input is a tape, a tuple containing a - list of generated tapes, together with a post-processing - function to be applied to the results of the evaluated tapes - in order to obtain the Jacobian. + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the Jacobian in the form of a tensor, a tuple, or a nested tuple depending upon the nesting + structure of measurements in the original circuit. For a variational evolution :math:`U(\mathbf{p}) \vert 0\rangle` with :math:`N` parameters :math:`\mathbf{p}`, @@ -114,72 +108,93 @@ def hadamard_grad( **Example** - This gradient transform can be applied directly to :class:`QNode ` objects: - - >>> dev = qml.device("default.qubit", wires=2) - >>> @qml.qnode(dev) - ... def circuit(params): - ... qml.RX(params[0], wires=0) - ... qml.RY(params[1], wires=0) - ... qml.RX(params[2], wires=0) - ... return qml.expval(qml.PauliZ(0)) - >>> params = np.array([0.1, 0.2, 0.3], requires_grad=True) - >>> qml.gradients.hadamard_grad(circuit)(params) - (tensor([-0.3875172], requires_grad=True), - tensor([-0.18884787], requires_grad=True), - tensor([-0.38355704], requires_grad=True)) - - This quantum gradient transform can also be applied to low-level - :class:`~.QuantumTape` objects. This will result in no implicit quantum - device evaluation. Instead, the processed tapes, and post-processing - function, which together define the gradient are directly returned: - - >>> ops = [qml.RX(p, wires=0) for p in params] - >>> measurements = [qml.expval(qml.PauliZ(0))] - >>> tape = qml.tape.QuantumTape(ops, measurements) - >>> gradient_tapes, fn = qml.gradients.hadamard_grad(tape) - >>> gradient_tapes - [, - , - ] - - This can be useful if the underlying circuits representing the gradient - computation need to be analyzed. - - The output tapes can then be evaluated and post-processed to retrieve - the gradient: - - >>> dev = qml.device("default.qubit", wires=2) - >>> fn(qml.execute(gradient_tapes, dev, None)) - (array(-0.3875172), array(-0.18884787), array(-0.38355704)) - This transform can be registered directly as the quantum gradient transform to use during autodifferentiation: - >>> dev = qml.device("default.qubit", wires=3) + >>> import jax + >>> dev = qml.device("default.qubit", wires=2) >>> @qml.qnode(dev, interface="jax", diff_method="hadamard") ... def circuit(params): ... qml.RX(params[0], wires=0) ... qml.RY(params[1], wires=0) ... qml.RX(params[2], wires=0) - ... return qml.expval(qml.PauliZ(0)) - >>> params = jax.numpy.array([0.1, 0.2, 0.3]) - >>> jax.jacobian(circuit)(params) - [-0.3875172 -0.18884787 -0.38355704] - - If you use custom wires on your device, you need to pass an auxiliary wire. - - >>> dev_wires = ("a", "c") - >>> dev = qml.device("default.qubit", wires=dev_wires) - >>> @qml.qnode(dev, interface="jax", diff_method="hadamard", aux_wire="c", device_wires=dev_wires) - >>> def circuit(params): - ... qml.RX(params[0], wires="a") - ... qml.RY(params[1], wires="a") - ... qml.RX(params[2], wires="a") - ... return qml.expval(qml.PauliZ("a")) + ... return qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(0)) >>> params = jax.numpy.array([0.1, 0.2, 0.3]) >>> jax.jacobian(circuit)(params) - [-0.3875172 -0.18884787 -0.38355704] + (Array([-0.38751727, -0.18884793, -0.3835571 ], dtype=float32), + Array([0.6991687 , 0.34072432, 0.6920237 ], dtype=float32)) + + .. details:: + :title: Usage Details + + This gradient transform can be applied directly to :class:`QNode ` objects. + However, for performance reasons, we recommend providing the gradient transform as the ``diff_method`` argument + of the QNode decorator, and differentiating with your preferred machine learning framework. + + >>> dev = qml.device("default.qubit", wires=2) + >>> @qml.qnode(dev) + ... def circuit(params): + ... qml.RX(params[0], wires=0) + ... qml.RY(params[1], wires=0) + ... qml.RX(params[2], wires=0) + ... return qml.expval(qml.PauliZ(0)) + >>> params = np.array([0.1, 0.2, 0.3], requires_grad=True) + >>> qml.gradients.hadamard_grad(circuit)(params) + (tensor([-0.3875172], requires_grad=True), + tensor([-0.18884787], requires_grad=True), + tensor([-0.38355704], requires_grad=True)) + + This quantum gradient transform can also be applied to low-level + :class:`~.QuantumTape` objects. This will result in no implicit quantum + device evaluation. Instead, the processed tapes, and post-processing + function, which together define the gradient are directly returned: + + >>> ops = [qml.RX(p, wires=0) for p in params] + >>> measurements = [qml.expval(qml.PauliZ(0))] + >>> tape = qml.tape.QuantumTape(ops, measurements) + >>> gradient_tapes, fn = qml.gradients.hadamard_grad(tape) + >>> gradient_tapes + [, + , + ] + + This can be useful if the underlying circuits representing the gradient + computation need to be analyzed. + + The output tapes can then be evaluated and post-processed to retrieve + the gradient: + + >>> dev = qml.device("default.qubit", wires=2) + >>> fn(qml.execute(gradient_tapes, dev, None)) + (array(-0.3875172), array(-0.18884787), array(-0.38355704)) + + This transform can be registered directly as the quantum gradient transform + to use during autodifferentiation: + + >>> dev = qml.device("default.qubit", wires=3) + >>> @qml.qnode(dev, interface="jax", diff_method="hadamard") + ... def circuit(params): + ... qml.RX(params[0], wires=0) + ... qml.RY(params[1], wires=0) + ... qml.RX(params[2], wires=0) + ... return qml.expval(qml.PauliZ(0)) + >>> params = jax.numpy.array([0.1, 0.2, 0.3]) + >>> jax.jacobian(circuit)(params) + [-0.3875172 -0.18884787 -0.38355704] + + If you use custom wires on your device, you need to pass an auxiliary wire. + + >>> dev_wires = ("a", "c") + >>> dev = qml.device("default.qubit", wires=dev_wires) + >>> @qml.qnode(dev, interface="jax", diff_method="hadamard", aux_wire="c", device_wires=dev_wires) + >>> def circuit(params): + ... qml.RX(params[0], wires="a") + ... qml.RY(params[1], wires="a") + ... qml.RX(params[2], wires="a") + ... return qml.expval(qml.PauliZ("a")) + >>> params = jax.numpy.array([0.1, 0.2, 0.3]) + >>> jax.jacobian(circuit)(params) + [-0.3875172 -0.18884787 -0.38355704] .. note:: diff --git a/pennylane/gradients/parameter_shift.py b/pennylane/gradients/parameter_shift.py index d7877bb890a..085c1acbfac 100644 --- a/pennylane/gradients/parameter_shift.py +++ b/pennylane/gradients/parameter_shift.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane.measurements import VarianceMP -from pennylane.transforms.core import transform +from pennylane import transform from pennylane.transforms.tape_expand import expand_invalid_trainable from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac @@ -759,11 +759,11 @@ def param_shift( f0=None, broadcast=False, ) -> (Sequence[qml.tape.QuantumTape], Callable): - r"""Transform a QNode to compute the parameter-shift gradient of all gate + r"""Transform a circuit to compute the parameter-shift gradient of all gate parameters with respect to its inputs. Args: - qnode (pennylane.QNode or .QuantumTape): quantum tape or QNode to differentiate + tape (QNode or QuantumTape): quantum circuit to differentiate argnum (int or list[int] or None): Trainable parameter indices to differentiate with respect to. If not provided, the derivative with respect to all trainable indices are returned. @@ -794,17 +794,11 @@ def param_shift( a single broadcasted tape per operation instead of one tape per shift angle. Returns: - function or tuple[list[QuantumTape], function]: + qnode (QNode) or tuple[List[QuantumTape], function]: - - If the input is a QNode, an object representing the Jacobian (function) of the QNode - that can be executed to obtain the Jacobian. - The type of the Jacobian returned is either a tensor, a tuple or a - nested tuple depending on the nesting structure of the original QNode output. - - - If the input is a tape, a tuple containing a - list of generated tapes, together with a post-processing - function to be applied to the results of the evaluated tapes - in order to obtain the Jacobian. + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the Jacobian in the form of a tensor, a tuple, or a nested tuple depending upon the nesting + structure of measurements in the original circuit. For a variational evolution :math:`U(\mathbf{p}) \vert 0\rangle` with :math:`N` parameters :math:`\mathbf{p}`, @@ -928,7 +922,9 @@ def param_shift( .. details:: :title: Usage Details - This gradient transform can be applied directly to :class:`QNode ` objects: + This gradient transform can be applied directly to :class:`QNode ` objects. + However, for performance reasons, we recommend providing the gradient transform as the ``diff_method`` argument + of the QNode decorator, and differentiating with your preferred machine learning framework. >>> @qml.qnode(dev) ... def circuit(params): diff --git a/pennylane/gradients/parameter_shift_cv.py b/pennylane/gradients/parameter_shift_cv.py index 06b3282094d..369fbf2fcc2 100644 --- a/pennylane/gradients/parameter_shift_cv.py +++ b/pennylane/gradients/parameter_shift_cv.py @@ -25,7 +25,7 @@ import pennylane as qml from pennylane.measurements import ExpectationMP, ProbabilityMP, StateMP, VarianceMP -from pennylane.transforms.core import transform +from pennylane import transform from pennylane.transforms.tape_expand import expand_invalid_trainable from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac @@ -524,7 +524,7 @@ def param_shift_cv( parameters with respect to its inputs. Args: - tape (.QuantumTape): quantum tape to differentiate + tape (QNode or QuantumTape): quantum circuit to differentiate dev (pennylane.Device): device the parameter-shift method is to be computed on argnum (int or list[int] or None): Trainable parameter indices to differentiate with respect to. If not provided, the derivative with respect to all @@ -556,16 +556,11 @@ def param_shift_cv( force_order2 (bool): if True, use the order-2 method even if not necessary Returns: - function or tuple[list[QuantumTape], function]: - - - If the input is a QNode, an object representing the Jacobian (function) of the QNode - that can be executed to obtain the Jacobian matrix. - The returned matrix is a tensor of size ``(number_outputs, number_gate_parameters)`` + qnode (QNode) or tuple[List[QuantumTape], function]: - - If the input is a tape, a tuple containing a - list of generated tapes, together with a post-processing - function to be applied to the results of the evaluated tapes - in order to obtain the Jacobian matrix. + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the Jacobian in the form of a tensor, a tuple, or a nested tuple depending upon the nesting + structure of measurements in the original circuit. This transform supports analytic gradients of Gaussian CV operations using the parameter-shift rule. This gradient method returns *exact* gradients, @@ -646,7 +641,9 @@ def circuit(weights): .. details:: :title: Usage Details - This gradient transform can be applied directly to :class:`QNode ` objects: + This gradient transform can be applied directly to :class:`QNode ` objects. + However, for performance reasons, we recommend providing the gradient transform as the ``diff_method`` argument + of the QNode decorator, and differentiating with your preferred machine learning framework. >>> @qml.qnode(dev) ... def circuit(params): diff --git a/pennylane/gradients/parameter_shift_hessian.py b/pennylane/gradients/parameter_shift_hessian.py index 6747529aa4b..89eeaa96b56 100644 --- a/pennylane/gradients/parameter_shift_hessian.py +++ b/pennylane/gradients/parameter_shift_hessian.py @@ -396,7 +396,7 @@ def _contract_qjac_with_cjac(qhess, cjac, tape): def param_shift_hessian( tape: qml.tape.QuantumTape, argnum=None, diagonal_shifts=None, off_diagonal_shifts=None, f0=None ) -> (Sequence[qml.tape.QuantumTape], Callable): - r"""Transform a QNode to compute the parameter-shift Hessian with respect to its trainable + r"""Transform a circuit to compute the parameter-shift Hessian with respect to its trainable parameters. This is the Hessian transform to replace the old one in the new return types system Use this transform to explicitly generate and explore parameter-shift circuits for computing @@ -408,7 +408,7 @@ def param_shift_hessian( >>> qml.jacobian(qml.grad(cost))(weights) Args: - tape (pennylane.QNode or .QuantumTape): quantum tape or QNode to differentiate + tape (QNode or QuantumTape): quantum circuit to differentiate argnum (int or list[int] or array_like[bool] or None): Parameter indices to differentiate with respect to. If not provided, the Hessian with respect to all trainable indices is returned. Note that the indices refer to tape @@ -432,18 +432,13 @@ def param_shift_hessian( instead of evaluating the input tape, reducing the number of device invocations. Returns: - function or tuple[list[QuantumTape], function]: + qnode (QNode) or tuple[List[QuantumTape], function]: - - If the input is a QNode, an object representing the Hessian (function) of - the QNode that can be executed to obtain the Hessian matrix. - The returned Hessian matrix is given as a tensor or nested tuples of tensors. - The level of nesting depends on the number of trainable QNode arguments, the output - shape(s) of the input QNode itself, and the usage of shot vectors in the QNode execution. + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the Hessian in the form of a tensor, a tuple, or a nested tuple depending upon the number + of trainable QNode arguments, the output shape(s) of the input QNode itself, and the usage of shot vectors + in the QNode execution. - - If the input is a tape, a tuple containing a - list of generated tapes, together with a post-processing - function to be applied to the results of the evaluated tapes - in order to obtain the Hessian matrix. Note: By default a QNode with the keyword ``hybrid=True`` computes derivates with respect to QNode arguments, which can include classical computations on those arguments before they are @@ -453,7 +448,9 @@ def param_shift_hessian( **Example** - Applying the Hessian transform to a QNode computes its Hessian tensor: + Applying the Hessian transform to a QNode computes its Hessian tensor. + This works best if no classical processing is applied within the + QNode to operation parameters. >>> dev = qml.device("default.qubit", wires=2) >>> @qml.qnode(dev) diff --git a/pennylane/gradients/pulse_gradient.py b/pennylane/gradients/pulse_gradient.py index 2693d10b675..f6a20ecc81c 100644 --- a/pennylane/gradients/pulse_gradient.py +++ b/pennylane/gradients/pulse_gradient.py @@ -22,7 +22,7 @@ import pennylane as qml from pennylane.pulse import ParametrizedEvolution, HardwareHamiltonian -from pennylane.transforms.core import transform +from pennylane import transform from .parameter_shift import _make_zero_rep from .general_shift_rules import eigvals_to_frequencies, generate_shift_rule @@ -334,7 +334,7 @@ def stoch_pulse_grad( of the envelopes :math:`\partial f_j / \partial v_k` at the sampled times suitably. Args: - tape (pennylane.QNode or .QuantumTape): quantum tape or QNode to differentiate + tape (QuantumTape): quantum circuit to differentiate argnum (int or list[int] or None): Trainable tape parameter indices to differentiate with respect to. If not provided, the derivatives with respect to all trainable parameters are returned. @@ -348,17 +348,11 @@ def stoch_pulse_grad( tapes is created, increasing performance on simulators. Returns: - function or tuple[list[QuantumTape], function]: + tuple[List[QuantumTape], function]: - - If the input is a QNode, an object representing the Jacobian (function) of the QNode - that can be executed to obtain the Jacobian. - The type of the Jacobian returned is either a tensor, a tuple or a - nested tuple depending on the nesting structure of the original QNode output. - - - If the input is a tape, a tuple containing a - list of generated tapes, together with a post-processing - function to be applied to the results of the evaluated tapes - in order to obtain the Jacobian. + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the Jacobian in the form of a tensor, a tuple, or a nested tuple depending upon the nesting + structure of measurements in the original circuit. This transform realizes the stochastic parameter-shift rule for pulse sequences, as introduced in `Banchi and Crooks (2018) `_ and diff --git a/pennylane/gradients/pulse_gradient_odegen.py b/pennylane/gradients/pulse_gradient_odegen.py index 39dc815f566..9ead08b3edf 100644 --- a/pennylane/gradients/pulse_gradient_odegen.py +++ b/pennylane/gradients/pulse_gradient_odegen.py @@ -24,7 +24,7 @@ from pennylane.pulse import ParametrizedEvolution from pennylane.ops.qubit.special_unitary import pauli_basis_strings, _pauli_decompose -from pennylane.transforms.core import transform +from pennylane import transform from .parameter_shift import _make_zero_rep from .pulse_gradient import _assert_has_jax, raise_pulse_diff_on_qnode @@ -403,7 +403,7 @@ def processing_fn(results): def pulse_odegen( tape: qml.tape.QuantumTape, argnum=None, atol=1e-7 ) -> (Sequence[qml.tape.QuantumTape], Callable): - r"""Transform a QNode to compute the pulse generator parameter-shift gradient of pulses + r"""Transform a circuit to compute the pulse generator parameter-shift gradient of pulses in a pulse program with respect to their inputs. This method combines automatic differentiation of few-qubit operations with hardware-compatible shift rules. @@ -428,7 +428,7 @@ def pulse_odegen( See the theoretical background section below for more details. Args: - tape (pennylane.QNode or .QuantumTape): quantum tape or QNode to differentiate + tape (QuantumTape): quantum circuit to differentiate argnum (int or list[int] or None): Trainable tape parameter indices to differentiate with respect to. If not provided, the derivatives with respect to all trainable parameters are returned. @@ -437,17 +437,11 @@ def pulse_odegen( ``qml.math.isclose(x, 0., atol=atol, rtol=0) == True`` are neglected. Returns: - function or tuple[list[QuantumTape], function]: + tuple[List[QuantumTape], function]: - - If the input is a QNode, an object representing the Jacobian (function) of the QNode - that can be executed to obtain the Jacobian. - The type of the Jacobian returned is either a tensor, a tuple or a - nested tuple depending on the nesting structure of the original QNode output. - - - If the input is a tape, a tuple containing a - list of generated tapes, together with a post-processing - function to be applied to the results of the evaluated tapes - in order to obtain the Jacobian. + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the Jacobian in the form of a tensor, a tuple, or a nested tuple depending upon the nesting + structure of measurements in the original circuit. .. note:: diff --git a/pennylane/gradients/spsa_gradient.py b/pennylane/gradients/spsa_gradient.py index f49454fe4b7..b43617631b1 100644 --- a/pennylane/gradients/spsa_gradient.py +++ b/pennylane/gradients/spsa_gradient.py @@ -22,7 +22,7 @@ import numpy as np import pennylane as qml -from pennylane.transforms.core import transform +from pennylane import transform from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac from pennylane.transforms.tape_expand import expand_invalid_trainable @@ -103,13 +103,13 @@ def spsa_grad( sampler=_rademacher_sampler, sampler_rng=None, ) -> (Sequence[qml.tape.QuantumTape], Callable): - r"""Transform a QNode to compute the SPSA gradient of all gate + r"""Transform a circuit to compute the SPSA gradient of all gate parameters with respect to its inputs. This estimator shifts all parameters simultaneously and approximates the gradient based on these shifts and a finite-difference method. Args: - tape (pennylane.QNode or .QuantumTape): quantum tape or QNode to differentiate + tape (QNode or QuantumTape): quantum circuit to differentiate argnum (int or list[int] or None): Trainable parameter indices to differentiate with respect to. If not provided, the derivatives with respect to all trainable parameters are returned. @@ -166,17 +166,11 @@ def spsa_grad( ``spsa_grad`` in each call. Returns: - function or tuple[list[QuantumTape], function]: + qnode (QNode) or tuple[List[QuantumTape], function]: - - If the input is a QNode, an object representing the Jacobian (function) of the QNode - that can be executed to obtain the Jacobian. - The type of the Jacobian returned is either a tensor, a tuple or a - nested tuple depending on the nesting structure of the original QNode output. - - - If the input is a tape, a tuple containing a - list of generated tapes, together with a post-processing - function to be applied to the results of the evaluated tapes - in order to obtain the Jacobian. + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the Jacobian in the form of a tensor, a tuple, or a nested tuple depending upon the nesting + structure of measurements in the original circuit. **Example** diff --git a/pennylane/interfaces/execution.py b/pennylane/interfaces/execution.py index 6a6a0b437fe..f5ec7165a4d 100644 --- a/pennylane/interfaces/execution.py +++ b/pennylane/interfaces/execution.py @@ -424,7 +424,7 @@ def execute( interface (str): The interface that will be used for classical autodifferentiation. This affects the types of parameters that can exist on the input tapes. Available options include ``autograd``, ``torch``, ``tf``, ``jax`` and ``auto``. - transform_program(qml.transforms.core.TransformProgram): A transform program to be applied to the initial tape. + transform_program(.TransformProgram): A transform program to be applied to the initial tape. config (qml.devices.ExecutionConfig): A datastructure describing the parameters needed to fully describe the execution. grad_on_execution (bool, str): Whether the gradients should be computed on the execution or not. Only applies if the device is queried for the gradient; gradient transform diff --git a/pennylane/interfaces/jacobian_products.py b/pennylane/interfaces/jacobian_products.py index 241f6a68586..e0a259c0e75 100644 --- a/pennylane/interfaces/jacobian_products.py +++ b/pennylane/interfaces/jacobian_products.py @@ -173,12 +173,12 @@ def compute_jacobian(self, tapes: Batch) -> Tuple: class TransformJacobianProducts(JacobianProductCalculator): - """Compute VJPs, JVPs and Jacobians via a :class:`~.gradient_transform`. + """Compute VJPs, JVPs and Jacobians via a gradient transform :class:`~.TransformDispatcher`. Args: inner_execute (Callable[[Tuple[QuantumTape]], ResultBatch]): a function that executes the batch of circuits and returns their results. - gradient_transform (pennylane.gradients.gradient_transform): the gradient transform to use. + gradient_transform (.TransformDispatcher): the gradient transform to use. gradient_kwargs (dict): Any keyword arguments for the gradient transform. >>> inner_execute = qml.device('default.qubit').execute @@ -194,7 +194,7 @@ def __repr__(self): def __init__( self, inner_execute: Callable, - gradient_transform: "qml.gradients.gradient_transform", + gradient_transform: "pennylane.transforms.core.TransformDispatcher", gradient_kwargs: Optional[dict] = None, ): if logger.isEnabledFor(logging.DEBUG): # pragma: no cover diff --git a/pennylane/ops/functions/eigvals.py b/pennylane/ops/functions/eigvals.py index 4201e38d29e..48f8938f0d0 100644 --- a/pennylane/ops/functions/eigvals.py +++ b/pennylane/ops/functions/eigvals.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane.transforms.op_transforms import OperationTransformError -from pennylane.transforms.core import transform +from pennylane import transform from pennylane.typing import TensorLike @@ -41,7 +41,7 @@ def eigvals(op: qml.operation.Operator, k=1, which="SA") -> TensorLike: `documentation `_. Args: - op (.Operator or .QuantumTape): A quantum operator or tape. + op (Operator or QNode or QuantumTape or Callable): A quantum operator or quantum circuit. k (int): The number of eigenvalues to be returned for a :class:`~.SparseHamiltonian`. which (str): Method for computing the eigenvalues of a :class:`~.SparseHamiltonian`. The possible methods are ``'LM'`` (largest in magnitude), ``'SM'`` (smallest in magnitude), @@ -49,10 +49,11 @@ def eigvals(op: qml.operation.Operator, k=1, which="SA") -> TensorLike: from each end of the spectrum). Returns: - TensorLike or (Sequence[.QuantumTape], Callable): If an operator is provided as input, the eigenvalues - are returned directly. If a quantum tape is provided as input, a list of transformed tapes and a post-processing - function are returned. When called, this function will return the unitary matrix in the appropriate autodiff - framework (Autograd, TensorFlow, PyTorch, JAX) given its parameters. + TensorLike or qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + If an operator is provided as input, the eigenvalues are returned directly in the form of a tensor. + Otherwise, the transformed circuit is returned as described in :func:`qml.transform `. + Executing this circuit will provide the eigenvalues as a tensor. **Example** diff --git a/pennylane/ops/functions/map_wires.py b/pennylane/ops/functions/map_wires.py index 60848d209a6..f13eb40a137 100644 --- a/pennylane/ops/functions/map_wires.py +++ b/pennylane/ops/functions/map_wires.py @@ -23,7 +23,7 @@ from pennylane.qnode import QNode from pennylane.queuing import QueuingManager from pennylane.tape import QuantumScript, QuantumTape -from pennylane.transforms.core import transform +from pennylane import transform def map_wires( @@ -36,15 +36,16 @@ def map_wires( wire map. Args: - input (.Operator, pennylane.QNode, .QuantumTape, or Callable): an operator, quantum node, - quantum tape, or function that applies quantum operations + input (Operator or QNode or QuantumTape or Callable): an operator or a quantum circuit. wire_map (dict): dictionary containing the old wires as keys and the new wires as values queue (bool): Whether or not to queue the object when recording. Defaults to False. replace (bool): When ``queue=True``, if ``replace=True`` the input operators will be replaced by its mapped version. Defaults to False. Returns: - (.Operator, pennylane.QNode, .QuantumTape, or Callable): input with changed wires + operator (Operator) or qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function]: + + The transformed circuit or operator with updated wires in :func:`qml.transform `. .. note:: diff --git a/pennylane/ops/functions/matrix.py b/pennylane/ops/functions/matrix.py index 28c5f3e2a39..fa9ce14228a 100644 --- a/pennylane/ops/functions/matrix.py +++ b/pennylane/ops/functions/matrix.py @@ -20,7 +20,7 @@ import pennylane as qml from pennylane.transforms.op_transforms import OperationTransformError -from pennylane.transforms.core import transform +from pennylane import transform from pennylane.typing import TensorLike @@ -28,15 +28,16 @@ def matrix(op: qml.operation.Operator, wire_order=None) -> TensorLike: r"""The matrix representation of an operation or quantum circuit. Args: - op (.Operator or .QuantumTape): A quantum operator or tape. + op (Operator or QNode or QuantumTape or Callable): A quantum operator or quantum circuit. wire_order (Sequence[Any], optional): Order of the wires in the quantum circuit. Defaults to the order in which the wires appear in the quantum function. Returns: - TensorLike or (Sequence[.QuantumTape], Callable): If an operator is provided as input, the matrix - is returned directly. If a quantum tape is provided, a list of transformed tapes and a post-processing - function are returned. When called, this function will return unitary matrix in the appropriate - autodiff framework (Autograd, TensorFlow, PyTorch, JAX) given its parameters. + TensorLike or qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + If an operator is provided as input, the matrix is returned directly in the form of a tensor. + Otherwise, the transformed circuit is returned as described in :func:`qml.transform `. + Executing this circuit will provide its matrix representation. **Example** diff --git a/pennylane/optimize/adaptive.py b/pennylane/optimize/adaptive.py index cbc986d82e2..7492bf8efed 100644 --- a/pennylane/optimize/adaptive.py +++ b/pennylane/optimize/adaptive.py @@ -19,7 +19,7 @@ import pennylane as qml from pennylane import numpy as np from pennylane.tape import QuantumTape -from pennylane.transforms.core import transform +from pennylane import transform @transform @@ -27,16 +27,13 @@ def append_gate(tape: QuantumTape, params, gates) -> (Sequence[QuantumTape], Cal """Append parameterized gates to an existing tape. Args: - tape (QuantumTape): quantum tape to transform by adding gates + tape (QuantumTape or QNode or Callable): quantum circuit to transform by adding gates params (array[float]): parameters of the gates to be added gates (list[Operator]): list of the gates to be added Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. + """ new_operations = [] diff --git a/pennylane/optimize/riemannian_gradient.py b/pennylane/optimize/riemannian_gradient.py index 4f69c8fdca7..2ae06b7f79b 100644 --- a/pennylane/optimize/riemannian_gradient.py +++ b/pennylane/optimize/riemannian_gradient.py @@ -19,7 +19,7 @@ from scipy.sparse.linalg import expm import pennylane as qml -from pennylane.transforms.core import transform +from pennylane import transform from pennylane.tape import QuantumTape from pennylane.queuing import QueuingManager @@ -56,17 +56,14 @@ def append_time_evolution( and append this unitary. Args: - tape (QuantumTape or .QNode): circuit to transform. + tape (QuantumTape or QNode or Callable): circuit to transform. riemannian_gradient (.Hamiltonian): Hamiltonian object representing the Riemannian gradient. t (float): time evolution parameter. n (int): number of Trotter steps. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. + """ new_operations = tape.operations diff --git a/pennylane/qinfo/transforms.py b/pennylane/qinfo/transforms.py index 3e6f83c0bbd..a72b658397b 100644 --- a/pennylane/qinfo/transforms.py +++ b/pennylane/qinfo/transforms.py @@ -21,7 +21,7 @@ from pennylane.devices import DefaultQubit, DefaultQubitLegacy, DefaultMixed from pennylane.measurements import StateMP, DensityMatrixMP from pennylane.transforms import adjoint_metric_tensor, metric_tensor -from pennylane.transforms.core import transform +from pennylane import transform @partial(transform, final_transform=True) @@ -30,14 +30,14 @@ def reduced_dm(tape: QuantumTape, wires, **kwargs) -> (Sequence[QuantumTape], Ca :func:`~pennylane.state`. Args: - tape (QuantumTape): A :class:`~.QuantumTape` returning :func:`~pennylane.state`. + tape (QuantumTape or QNode or Callable)): A quantum circuit returning :func:`~pennylane.state`. wires (Sequence(int)): List of wires in the considered subsystem. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], Callable]: If a QNode - is passed, it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of quantum tapes to be - evaluated, and a function to be applied to these tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the reduced density matrix in the form of a tensor. **Example** @@ -119,14 +119,14 @@ def purity(tape: QuantumTape, wires, **kwargs) -> (Sequence[QuantumTape], Callab the overall state, include all wires in the ``wires`` argument. Args: - tape (pennylane.tape.QuantumTape): A :class:`.QuantumTape` objeect returning a :func:`~pennylane.state`. + tape (QNode or QuantumTape or Callable): A quantum circuit object returning a :func:`~pennylane.state`. wires (Sequence(int)): List of wires in the considered subsystem. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], Callable]: If a QNode - is passed, it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of quantum tapes to be - evaluated, and a function to be applied to these tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the purity in the form of a tensor. **Example** @@ -211,15 +211,15 @@ def vn_entropy( S( \rho ) = -\text{Tr}( \rho \log ( \rho )) Args: - tape (.QuantumTape): A :class:`.QuantumTape` returning a :func:`~pennylane.state`. + tape (QNode or QuantumTape or Callable): A quantum circuit returning a :func:`~pennylane.state`. wires (Sequence(int)): List of wires in the considered subsystem. base (float): Base for the logarithm, default is None the natural logarithm is used in this case. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], Callable]: If a QNode - is passed, it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of quantum tapes to be - evaluated, and a function to be applied to these tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the Von Neumann entropy in the form of a tensor. **Example** @@ -312,16 +312,16 @@ def mutual_info( one system by measuring the other system. Args: - qnode (QNode): A :class:`.QNode` returning a :func:`~pennylane.state`. + qnode (QNode or QuantumTape or Callable): A quantum circuit returning a :func:`~pennylane.state`. wires0 (Sequence(int)): List of wires in the first subsystem. wires1 (Sequence(int)): List of wires in the second subsystem. base (float): Base for the logarithm. If None, the natural logarithm is used. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], Callable]: If a QNode - is passed, it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of quantum tapes to be - evaluated, and a function to be applied to these tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the mutual information in the form of a tensor. **Example** @@ -667,11 +667,14 @@ def quantum_fisher( :func:`~.pennylane.metric_tensor`, :func:`~.pennylane.adjoint_metric_tensor`, :func:`~.pennylane.qinfo.transforms.classical_fisher` Args: - qnode (:class:`.QNode`): A :class:`.QNode` that may have arbitrary return types. + tape (QNode or QuantumTape or Callable): A quantum circuit that may have arbitrary return types. *args: In case finite shots are used, further arguments according to :func:`~.pennylane.metric_tensor` may be passed. Returns: - func: A function that computes the quantum fisher information matrix. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the quantum Fisher information in the form of a tensor. .. note:: diff --git a/pennylane/qnode.py b/pennylane/qnode.py index 1a9362f14c4..68b410b510f 100644 --- a/pennylane/qnode.py +++ b/pennylane/qnode.py @@ -100,9 +100,9 @@ class QNode: * ``"auto"``: The QNode automatically detects the interface from the input values of the quantum function. - diff_method (str or .gradient_transform): The method of differentiation to use in the created QNode. - Can either be a :class:`~.gradient_transform`, which includes all quantum gradient - transforms in the :mod:`qml.gradients <.gradients>` module, or a string. The following + diff_method (str or .TransformDispatcher): The method of differentiation to use in + the created QNode. Can either be a :class:`~.TransformDispatcher`, which includes all + quantum gradient transforms in the :mod:`qml.gradients <.gradients>` module, or a string. The following strings are allowed: * ``"best"``: Best available method. Uses classical backpropagation or the @@ -131,7 +131,6 @@ class QNode: rule for all supported quantum operation arguments. More info is in the documentation :func:`qml.gradients.hadamard_grad <.gradients.hadamard_grad>`. - * ``"finite-diff"``: Uses numerical finite-differences for all quantum operation arguments. @@ -512,16 +511,13 @@ def interface(self, value): @property def transform_program(self): - """The transform program used by the QNode. - - .. warning:: This is an experimental feature. - """ + """The transform program used by the QNode.""" return self._transform_program def add_transform(self, transform_container): - """Add a transform container to the transform program. + """Add a transform (container) to the transform program. - .. warning:: This is an experimental feature. + .. warning:: This is a developer facing feature and is called when a transform is applied on a QNode. """ self._transform_program.push_back(transform_container=transform_container) @@ -575,13 +571,13 @@ def get_gradient_fn(device, interface, diff_method="best", shots=None): Args: device (.Device): PennyLane device interface (str): name of the requested interface - diff_method (str or .gradient_transform): The requested method of differentiation. + diff_method (str or .TransformDispatcher): The requested method of differentiation. If a string, allowed options are ``"best"``, ``"backprop"``, ``"adjoint"``, ``"device"``, ``"parameter-shift"``, ``"hadamard"``, ``"finite-diff"``, or ``"spsa"``. A gradient transform may also be passed here. Returns: - tuple[str or .gradient_transform, dict, .Device: Tuple containing the ``gradient_fn``, + tuple[str or .TransformDispatcher, dict, .Device: Tuple containing the ``gradient_fn``, ``gradient_kwargs``, and the device to use when calling the execute function. """ if diff_method == "best": @@ -644,7 +640,7 @@ def get_best_method(device, interface, shots=None): interface (str): name of the requested interface Returns: - tuple[str or .gradient_transform, dict, .Device: Tuple containing the ``gradient_fn``, + tuple[str or .TransformDispatcher, dict, .Device: Tuple containing the ``gradient_fn``, ``gradient_kwargs``, and the device to use when calling the execute function. """ try: diff --git a/pennylane/shadows/transforms.py b/pennylane/shadows/transforms.py index 92ea0d5b423..4beaca69c0e 100644 --- a/pennylane/shadows/transforms.py +++ b/pennylane/shadows/transforms.py @@ -22,7 +22,7 @@ import pennylane.numpy as np from pennylane.measurements import ClassicalShadowMP from pennylane.tape import QuantumScript, QuantumTape -from pennylane.transforms.core import transform +from pennylane import transform @transform @@ -54,19 +54,23 @@ def processing_fn(res): @partial(transform, final_transform=True) def shadow_expval(tape: QuantumTape, H, k=1) -> (Sequence[QuantumTape], Callable): - """Transform a QNode returning a classical shadow into one that returns + """Transform a circuit returning a classical shadow into one that returns the approximate expectation values in a differentiable manner. See :func:`~.pennylane.shadow_expval` for more usage details. Args: + tape (QNode or QuantumTape or Callable): A quantum circuit. H (:class:`~.pennylane.Observable` or list[:class:`~.pennylane.Observable`]): Observables for which to compute the expectation values k (int): k (int): Number of equal parts to split the shadow's measurements to compute the median of means. ``k=1`` corresponds to simply taking the mean over all measurements. Returns: - tensor-like[float]: 1-D tensor containing the expectation value estimates for each observable + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the expectation value estimates for each observable in the form of a tensor. **Example** @@ -75,7 +79,7 @@ def shadow_expval(tape: QuantumTape, H, k=1) -> (Sequence[QuantumTape], Callable H = qml.PauliZ(0) @ qml.PauliZ(1) dev = qml.device("default.qubit", wires=2, shots=10000) - @qml.shadows.shadow_expval(H, k=1) + @partial(qml.shadows.shadow_expval, H, k=1) @qml.qnode(dev) def circuit(x): qml.Hadamard(wires=0) @@ -168,10 +172,11 @@ def post_processing(results): @partial(transform, final_transform=True) def shadow_state(tape: QuantumTape, wires, diffable=False) -> (Sequence[QuantumTape], Callable): - """Transform a QNode returning a classical shadow into one that returns + """Transform a circuit returning a classical shadow into one that returns the reconstructed state in a differentiable manner. Args: + tape (QNode or QuantumTape or Callable): A quantum circuit. wires (list[int] or list[list[int]]): If a list of ints, this represents the wires over which to reconstruct the state. If a list of list of ints, a state is reconstructed for every element of the outer list, saving @@ -182,7 +187,10 @@ def shadow_state(tape: QuantumTape, wires, diffable=False) -> (Sequence[QuantumT cost. Returns: - list[tensor-like[complex]]: The reconstructed states + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the reconstructed state in the form of a tensor. **Example** @@ -190,7 +198,7 @@ def shadow_state(tape: QuantumTape, wires, diffable=False) -> (Sequence[QuantumT dev = qml.device("default.qubit", wires=2, shots=10000) - @qml.shadows.shadow_state(wires=[0, 1], diffable=True) + @partial(qml.shadows.shadow_state, wires=[0, 1], diffable=True) @qml.qnode(dev) def circuit(x): qml.Hadamard(wires=0) diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index f753322ce6e..4405fa58fe3 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -12,51 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This subpackage contains QNode, quantum function, device, and tape transforms. - +This subpackage contains PennyLane transforms and their building blocks. .. currentmodule:: pennylane -Transforms ----------- - -Transforms that act on QNodes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Custom transforms +----------------- -These transforms accept QNodes, and return new transformed functions -that compute the desired quantity. +: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. .. autosummary:: :toctree: api - ~transforms.classical_jacobian - ~batch_params - ~batch_input - ~batch_partial - ~metric_tensor - ~adjoint_metric_tensor - ~specs - ~transforms.split_non_commuting - -Transforms that act on quantum functions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -These transforms accept quantum functions (Python functions -containing quantum operations) that are used to construct QNodes. + ~transforms.core.transform -.. autosummary:: - :toctree: api - - ~transforms.cond - ~defer_measurements - ~apply_controlled_Q - ~quantum_monte_carlo - ~transforms.insert +Transforms library +------------------ +A range of ready-to-use transforms are available in PennyLane. Transforms for circuit compilation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This set of transforms accept quantum functions, and perform basic circuit compilation tasks. +A set of transforms to perform basic circuit compilation tasks. .. autosummary:: :toctree: api @@ -137,26 +116,68 @@ ~transforms.qcut.place_wire_cuts ~transforms.qcut.find_and_place_cuts -Transforms that act on tapes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Transforms for error mitigation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -These transforms accept quantum tapes, and return one or -more tapes as well as a classical processing function. +.. autosummary:: + :toctree: api + + ~transforms.mitigate_with_zne + ~transforms.fold_global + ~transforms.poly_extrapolate + ~transforms.richardson_extrapolate + +Other transforms +~~~~~~~~~~~~~~~~ + +These transforms use the :func:`pennylane.transform` function / decorator and can be used on +:class:`pennylane.tape.QuantumTape`, :class:`pennylane.QNode`. They fulfill multiple purposes like circuit +preprocessing, get information from a circuit and more. .. autosummary:: :toctree: api + ~metric_tensor + ~adjoint_metric_tensor + ~batch_params + ~batch_input + ~transforms.insert + ~defer_measurements + ~transforms.split_non_commuting ~transforms.broadcast_expand ~transforms.hamiltonian_expand ~transforms.sign_expand ~transforms.sum_expand + ~transforms.convert_to_numpy_parameters + ~apply_controlled_Q + ~quantum_monte_carlo -This transform accepts a single tape and returns a single tape: +Transforms that act only on QNodes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These transforms only accept QNodes, and return new transformed functions +that compute the desired quantity. .. autosummary:: :toctree: api - ~transforms.convert_to_numpy_parameters + ~transforms.classical_jacobian + ~batch_partial + ~specs + ~draw + ~draw_mpl + + +Transforms that act on quantum functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These transforms accept quantum functions (Python functions +containing quantum operations) that are used to construct QNodes. + +.. autosummary:: + :toctree: api + + ~transforms.cond Decorators and utility functions -------------------------------- @@ -167,10 +188,6 @@ .. autosummary:: :toctree: api - ~single_tape_transform - ~batch_transform - ~qfunc_transform - ~op_transform ~transforms.make_tape ~transforms.map_batch_transform ~transforms.create_expand_fn @@ -181,28 +198,38 @@ ~transforms.expand_trainable_multipar ~transforms.expand_nonunitary_gen -Transforms for error mitigation -------------------------------- +Transforms developer functions +------------------------------ + +:class:`~.TransformContainer`, :class:`~.TransformDispatcher` and :class:`~.TransformProgram` are +developer-facing objects that allow the +creation, dispatching and composability of transforms. If you would like to make a custom transform, refer +instead to the documentation of :func:`qml.transform `. .. autosummary:: :toctree: api - ~transforms.mitigate_with_zne - ~transforms.fold_global - ~transforms.poly_extrapolate - ~transforms.richardson_extrapolate + ~transforms.core.transform_dispatcher + ~transforms.core.transform_program + +Old transforms framework +------------------------ -Transforms core ---------------- +These utility functions were previously used to create transforms in PennyLane and will be +deprecated soon. It is now recommended to use :class:`qml.transform ` +for creation of custom transforms. .. autosummary:: :toctree: api - ~transforms.core.transform - ~transforms.core.transform_dispatcher + ~single_tape_transform + ~batch_transform + ~qfunc_transform + ~op_transform """ # 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 @@ -260,4 +287,3 @@ from .qcut import cut_circuit, cut_circuit_mc from .zx import to_zx, from_zx from .broadcast_expand import broadcast_expand -from .core import transform, TransformError diff --git a/pennylane/transforms/adjoint_metric_tensor.py b/pennylane/transforms/adjoint_metric_tensor.py index 2897139eab9..4047ec0eb2a 100644 --- a/pennylane/transforms/adjoint_metric_tensor.py +++ b/pennylane/transforms/adjoint_metric_tensor.py @@ -23,7 +23,7 @@ # pylint: disable=too-many-statements,unused-argument from pennylane.transforms.metric_tensor import _contract_metric_tensor_with_cjac -from pennylane.transforms.core import transform +from pennylane.transforms import transform def _reshape_real_imag(state, dim): @@ -80,11 +80,13 @@ def adjoint_metric_tensor( Note also that this makes the metric tensor strictly real-valued. Args: - tape (.QuantumTape): Circuit to compute the metric tensor of + tape (QNode or QuantumTape): Circuit to compute the metric tensor of Returns: - array: the metric tensor of the tape with respect to its trainable parameters. - Dimensions are ``(tape.num_params, tape.num_params)``. + qnode (QNode) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the metric tensor in the form of a tensor. Dimensions are ``(tape.num_params, tape.num_params)``. .. seealso:: :func:`~.metric_tensor` for hardware-compatible metric tensor computations. diff --git a/pennylane/transforms/batch_input.py b/pennylane/transforms/batch_input.py index 7effe8f5678..ca21e1c6743 100644 --- a/pennylane/transforms/batch_input.py +++ b/pennylane/transforms/batch_input.py @@ -28,7 +28,7 @@ def batch_input( argnum: Union[Sequence[int], int], ) -> (Sequence[QuantumTape], Callable): """ - Transform a QNode to support an initial batch dimension for gate inputs. + Transform a circuit to support an initial batch dimension for gate inputs. In a classical ML application one needs to batch the non-trainable inputs of the network. This function executes the same analogue for a quantum circuit: @@ -42,13 +42,15 @@ def batch_input( Based on `arXiv:2202.10471 `__. Args: - tape (.QuantumTape or .QNode): Input quantum circuit to batch + tape (QNode or QuantumTape or Callable): Input quantum circuit to batch argnum (Sequence[int] or int): One or several index values indicating the position of the non-trainable batched parameters in the quantum tape. Returns: - Sequence[Sequence[.QuantumTape], Callable]: list of tapes arranged - according to unbatched inputs and a callable function to batch the results. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the batched results. .. seealso:: :func:`~.batch_params` diff --git a/pennylane/transforms/batch_params.py b/pennylane/transforms/batch_params.py index 05dfe70fb3e..0c4f2a99b4a 100644 --- a/pennylane/transforms/batch_params.py +++ b/pennylane/transforms/batch_params.py @@ -102,14 +102,15 @@ def batch_params( the error. Args: - qnode (pennylane.QNode or .QuantumTape): quantum tape or QNode to add a batch dimension to + tape (QNode or QuantumTape or Callable): a quantum circuit to add a batch dimension to all_operations (bool): If ``True``, a batch dimension will be added to *all* operations in the QNode, rather than just trainable QNode parameters. Returns: - func: Function which accepts the same arguments as the QNode, however the - first dimension of each argument will be treated as a batch dimension. - The function output will also contain an initial batch dimension. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the batched results, with the first dimension treated as the batch dimension. **Example** diff --git a/pennylane/transforms/broadcast_expand.py b/pennylane/transforms/broadcast_expand.py index c44254c7ade..7391c13cce4 100644 --- a/pennylane/transforms/broadcast_expand.py +++ b/pennylane/transforms/broadcast_expand.py @@ -55,12 +55,14 @@ def broadcast_expand(tape: qml.tape.QuantumTape) -> (Sequence[qml.tape.QuantumTa Currently, not all templates have been updated to support broadcasting. Args: - tape (.QuantumTape): Broadcasted tape to be expanded + tape (QNode or QuantumTape or Callable): Broadcasted tape to be expanded Returns: - function or tuple[list[.QuantumTape], function]: + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: - - If the input is a QNode, an object resembling the (broadcasted) input QNode + The transformed circuit as described in :func:`qml.transform `. + + - If the input is a QNode, the broadcasted input QNode that computes the QNode output serially with multiple circuit evaluations and stacks (and squeezes) the results into one batch of results. diff --git a/pennylane/transforms/commutation_dag.py b/pennylane/transforms/commutation_dag.py index 383df0497b8..e15a3e813de 100644 --- a/pennylane/transforms/commutation_dag.py +++ b/pennylane/transforms/commutation_dag.py @@ -25,7 +25,7 @@ import pennylane as qml from pennylane.tape import QuantumTape from pennylane.wires import Wires -from pennylane.transforms.core import transform +from pennylane.transforms import transform @partial(transform, is_informative=True) @@ -39,16 +39,21 @@ def commutation_dag(tape: QuantumTape) -> (Sequence[QuantumTape], Callable): operations can be moved next to each other by pairwise commutation. Args: - tape ( .QuantumTape): The quantum circuit. + tape (QNode or QuantumTape or Callable): The quantum circuit. Returns: - function: Function which accepts the same arguments as the :class:`qml.QNode`, :class:`qml.tape.QuantumTape` - or quantum function. When called, this function will return the commutation DAG representation of the circuit. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the commutation DAG. **Example** + >>> dev = qml.device("default.qubit") + .. code-block:: python + @qml.qnode(device=dev) def circuit(x, y, z): qml.RX(x, wires=0) qml.RX(y, wires=0) diff --git a/pennylane/transforms/compile.py b/pennylane/transforms/compile.py index 20f6e70f9ae..0b11d11f5e5 100644 --- a/pennylane/transforms/compile.py +++ b/pennylane/transforms/compile.py @@ -47,7 +47,7 @@ def compile( (:func:`~pennylane.transforms.merge_rotations`) Args: - qfunc (function): A quantum function. + tape (QNode or QuantumTape or Callable): A quantum circuit. pipeline (list[single_tape_transform, qfunc_transform]): A list of tape and/or quantum function transforms to apply. basis_set (list[str]): A list of basis gates. When expanding the tape, @@ -62,14 +62,36 @@ def compile( for tape expansion into the basis gates. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The compiled circuit. The output type is explained in :func:`qml.transform `. **Example** + >>> dev = qml.device('default.qubit', wires=[0, 1, 2]) + + You can apply the transform directly on a :class:`QNode`: + + .. code-block:: python + + @compile + @qml.qnode(device=dev) + def circuit(x, y, z): + qml.Hadamard(wires=0) + qml.Hadamard(wires=1) + qml.Hadamard(wires=2) + qml.RZ(z, wires=2) + qml.CNOT(wires=[2, 1]) + qml.RX(z, wires=0) + qml.CNOT(wires=[1, 0]) + qml.RX(x, wires=0) + qml.CNOT(wires=[1, 0]) + qml.RZ(-z, wires=2) + qml.RX(y, wires=2) + qml.PauliY(wires=2) + qml.CY(wires=[1, 2]) + return qml.expval(qml.PauliZ(wires=0)) + + The default compilation pipeline is applied before execution. + Consider the following quantum function: .. code-block:: python @@ -92,7 +114,6 @@ def qfunc(x, y, z): Visually, the original function looks like this: - >>> dev = qml.device('default.qubit', wires=[0, 1, 2]) >>> qnode = qml.QNode(qfunc, dev) >>> print(qml.draw(qnode)(0.2, 0.3, 0.4)) 0: ──H──RX(0.40)────╭X──────────RX(0.20)─╭X────┤ @@ -102,7 +123,7 @@ def qfunc(x, y, z): We can compile it down to a smaller set of gates using the ``qml.compile`` transform. - >>> compiled_qfunc = qml.compile()(qfunc) + >>> compiled_qfunc = qml.compile(qfunc) >>> compiled_qnode = qml.QNode(compiled_qfunc, dev) >>> print(qml.draw(compiled_qnode)(0.2, 0.3, 0.4)) 0: ──H──RX(0.60)─────────────────┤ @@ -119,8 +140,8 @@ def qfunc(x, y, z): compiled_qfunc = qml.compile( pipeline=[ - qml.transforms.commute_controlled(direction="left"), - qml.transforms.merge_rotations(atol=1e-6), + partial(qml.transforms.commute_controlled, direction="left"), + partial(qml.transforms.merge_rotations, atol=1e-6), qml.transforms.cancel_inverses ], basis_set=["CNOT", "RX", "RY", "RZ"], diff --git a/pennylane/transforms/core/transform.py b/pennylane/transforms/core/transform.py index 0feeb1ca9e3..3e327ed03b8 100644 --- a/pennylane/transforms/core/transform.py +++ b/pennylane/transforms/core/transform.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains the transform function to make your custom transforms compatible with qfunc and QNodes. +This module contains the transform function/decorator to make your custom transforms compatible with tapes, quantum +functions and QNodes. """ from typing import get_type_hints, Sequence, List, Tuple, Callable import pennylane as qml @@ -26,40 +27,50 @@ def transform( is_informative=None, final_transform=False, ): - """The transform function is to be used to validate and dispatch a quantum transform on PennyLane objects (tape, - qfunc and Qnode). It can be used directly as a decorator on qfunc and qnodes. + """Generalizes a function that transforms tapes to work with additional circuit-like objects such as a + :class:`~.QNode`. - Args: - quantum_transform (callable): A quantum transform is defined as a function that has the following requirements: - - * A quantum transform is a function that takes a quantum tape as first input and returns a sequence of tapes - and a processing function. - - * The transform must have type hinting of the following form: my_quantum_transform(tape: - qml.tape.QuantumTape, ...) -> ( Sequence[qml.tape.QuantumTape], callable) + ``transform`` should be applied to a function that transforms tapes. Once validated, the result will + be an object that is able to transform PennyLane's range of circuit-like objects: + :class:`~.QuantumTape`, quantum function and :class:`~.QNode`. + A circuit-like object can be transformed either via decoration or by passing it functionally through + the created transform. - expand_transform (callable): An expand transform is defined as a function that has the following requirements: + Args: + quantum_transform (Callable): The input quantum transform must be a function that satisfies the + following requirements: - * An expand transform is a function that is applied before applying the defined quantum transform. It - takes the same arguments as the transform and returns a single tape in a sequence with a dummy processing - function. + * Accepts a :class:`~.QuantumTape` as its first input and + returns a sequence of :class:`~.QuantumTape` and a processing function. - * The expand transform must have the same type hinting as a quantum transform. + * The transform must have type hinting of the following form: ``my_quantum_transform(tape: + qml.tape.QuantumTape, ...) -> ( Sequence[qml.tape.QuantumTape], Callable)`` - classical_cotransform (callable): A classical co-transform. NOT YET SUPPORTED. + expand_transform (Callable): An optional expand transform is applied directly before the input + quantum transform. It must be a function that satisfies the same requirements as + ``quantum_transform``. + classical_cotransform (Callable): A classical co-transform is a function to post-process the classical + jacobian and the quantum jacobian and has the signature: ``my_cotransform(qjac, cjac, tape) -> tensor_like`` is_informative (bool): Whether or not a transform is informative. If true the transform is queued at the end of the transform program and the tapes or qnode aren't executed. final_transform (bool): Whether or not the transform is terminal. If true the transform is queued at the end of the transform program. ``is_informative`` supersedes ``final_transform``. + Returns: + + .TransformDispatcher: Returns a transform dispatcher object that that can transform any + circuit-like object in PennyLane. + **Example** - First define your quantum_transform, with the necessary type hinting defined above. In this example we copy the + First define an input quantum transform with the necessary type hinting defined above. In this example we copy the tape and sum the results of the execution of the two tapes. .. code-block:: python - def my_quantum_transform(tape: qml.tape.QuantumTape) -> (Sequence[qml.tape.QuantumTape], callable): + from typing import Sequence, Callable + + def my_quantum_transform(tape: qml.tape.QuantumTape) -> (Sequence[qml.tape.QuantumTape], Callable): tape1 = tape tape2 = tape.copy() @@ -68,41 +79,83 @@ def post_processing_fn(results): return [tape1, tape2], post_processing_fn - Of course, we want to be able to apply this transform on ``qfunc`` and ``qnodes``. That's where the ``transform`` function - comes into play. This function validates the signature of your quantum transform and dispatches it on the different - object. Let's define a circuit as a qfunc and as a qnode. + We want to be able to apply this transform on both a ``qfunc`` and a :class:`pennylane.QNode` and will + use ``transform`` to achieve this. ``transform`` validates the signature of your input quantum transform + and makes it capable of transforming ``qfunc`` and :class:`pennylane.QNode` in addition to quantum tapes. + Let's define a circuit as a :class:`pennylane.QNode`: - .. code-block:: python + .. code-block:: python - def qfunc_circuit(a): - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - qml.PauliX(wires=0) - qml.RZ(a, wires=1) - return qml.expval(qml.PauliZ(wires=0)) + dev = qml.device("default.qubit") - dev = qml.device("default.qubit", wires=2) + @qml.qnode(device=dev) + def qnode_circuit(a): + qml.Hadamard(wires=0) + qml.CNOT(wires=[0, 1]) + qml.PauliX(wires=0) + qml.RZ(a, wires=1) + return qml.expval(qml.PauliZ(wires=0)) - @qml.qnode(device=dev) - def qnode_circuit(a): - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - qml.PauliX(wires=0) - qml.RZ(a, wires=1) - return qml.expval(qml.PauliZ(wires=0)) + We first apply ``transform`` to ``my_quantum_transform``: >>> dispatched_transform = transform(my_quantum_transform) - Now you can use the dispatched transform directly on qfunc and qnodes. + Now you can use the dispatched transform directly on a :class:`pennylane.QNode`. - For QNodes, the dispatched transform populates the ``TransformProgram`` of your QNode. The transform and its - processing function are applied in the execution. + For :class:`pennylane.QNode`, the dispatched transform populates the ``TransformProgram`` of your QNode. The + transform and its processing function are applied in the execution. - >>> transformed_qnode = dispatched_transform(qfunc_circuit) + >>> transformed_qnode = dispatched_transform(qnode_circuit) - One subtlety here, this transform would not work for a qfunc because our transform return more than one case. If - it was not the case you would be able to dispatch on quantum functions. + >>> transformed_qnode.transform_program + TransformProgram(my_quantum_transform) + + If we apply ``dispatched_transform`` a second time to the :class:`pennylane.QNode`, we would add + it to the transform program again and therefore the transform would be applied twice before execution. + + >>> transformed_qnode = dispatched_transform(transformed_qnode) + >>> transformed_qnode.transform_program + TransformProgram(my_quantum_transform, my_quantum_transform) + + When a transformed QNode is executed, the QNode's transform program is applied to the generated tape + and creates a sequence of tapes to be executed. The execution results are then post-processed in the + reverse order of the transform program to obtain the final results. + + .. details:: + :title: Signature of a transform + + A dispatched transform is able to handle several PennyLane circuit-like objects: + + - :class:`pennylane.QNode` + - a quantum function (callable) + - :class:`pennylane.tape.QuantumTape` + - :class:`pennylane.devices.Device`. + + For each object, the transform will be applied in a different way, but it always preserves the underlying + tape-based quantum transform behaviour. + + The return of a dispatched transform depends upon which of the above objects is passed as an input: + + - For a :class:`~.QNode` input, the underlying transform is added to the QNode's + :class:`~.TransformProgram` and the return is the transformed :class:`~.QNode`. + For each execution of the :class:`pennylane.QNode`, it first applies the transform program on the original captured + circuit. Then the transformed circuits are executed by a device and finally the post-processing function is + applied on the results. + + - For a quantum function (callable) input, the transform builds the tape when the quantum function is + executed and then applies itself to the tape. The resulting tape is then converted back + to a quantum function (callable). It therefore returns a transformed quantum function (Callable). The limitation + is that the underlying transform can only return a sequence containing a single tape, because quantum + functions only support a single circuit. + + - For a :class:`~.QuantumTape`, the underlying quantum transform is directly applied on the + :class:`~.QuantumTape`. It returns a sequence of :class:`~.QuantumTape` and a processing + function to be applied after execution. + + - For a :class:`~.devices.Device`, the transform is added to the device's transform program + and a transformed :class:`pennylane.devices.Device` is returned. The transform is added + to the end of the device program and will be last in the overall transform program. """ # 1: Checks for the transform if not callable(quantum_transform): @@ -133,13 +186,14 @@ def qnode_circuit(a): if not callable(classical_cotransform): raise TransformError("The classical co-transform must be a valid Python function.") - return TransformDispatcher( + dispatcher = TransformDispatcher( quantum_transform, expand_transform=expand_transform, classical_cotransform=classical_cotransform, is_informative=is_informative, final_transform=final_transform, ) + return dispatcher def _transform_signature_check(signature): diff --git a/pennylane/transforms/core/transform_dispatcher.py b/pennylane/transforms/core/transform_dispatcher.py index 0c1004b1f55..a3a6b1e9f40 100644 --- a/pennylane/transforms/core/transform_dispatcher.py +++ b/pennylane/transforms/core/transform_dispatcher.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains the transform function, the transform dispatcher and the transform container. +This module contains the transform dispatcher and the transform container. """ +import functools +import os import copy import warnings import types @@ -22,23 +24,42 @@ class TransformError(Exception): - """Raised when there is an error with the transform logic""" + """Raised when there is an error with the transform logic.""" class TransformDispatcher: - r"""This object is developer facing and should not be used directly to create transforms. Use - :func:`~.pennylane.transforms.core.transform`. - - Convert a transform that has the signature (tape -> Sequence(tape), fn) to a transform dispatcher that can act - on tape, qfunc and qnode. + r"""Converts a transform that has the signature ``(tape -> Sequence(tape), fn)`` to a transform dispatcher + that can act on :class:`pennylane.tape.QuantumTape`, quantum function, :class:`pennylane.QNode`, + :class:`pennylane.devices.Device`. .. warning:: - This class is developer-facing and should not be used directly. + This class is developer-facing and should not be used directly. Instead, use + :func:`qml.transform ` if you would like to make a custom + transform. - .. seealso:: :func:`~.pennylane.transforms.core.transform` + .. seealso:: :func:`~.pennylane.transform` """ + 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( + "Transforms 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_transform = lambda x: x + return args[0] + + return super().__new__(cls) + # pylint: disable=too-many-arguments def __init__( self, @@ -48,15 +69,14 @@ def __init__( is_informative=False, final_transform=False, ): # pylint:disable=redefined-outer-name - self.__doc__ = transform.__doc__ self._transform = transform self._expand_transform = expand_transform self._classical_cotransform = classical_cotransform self._is_informative = is_informative # is_informative supersedes final_transform self._final_transform = is_informative or final_transform - self._qnode_transform = self.default_qnode_transform + functools.update_wrapper(self, transform) def __call__(self, *targs, **tkwargs): # pylint: disable=too-many-return-statements obj = None @@ -111,7 +131,9 @@ def processing_fn(results): "Decorating a QNode with @transform_fn(**transform_kwargs) has been " "deprecated and will be removed in a future version. Please decorate " "with @functools.partial(transform_fn, **transform_kwargs) instead, " - "or call the transform directly using qnode = transform_fn(qnode, **transform_kwargs)", + "or call the transform directly using qnode = transform_fn(qnode, " + "**transform_kwargs). Visit the deprecations page for more details: " + "https://docs.pennylane.ai/en/stable/development/deprecations.html", UserWarning, ) @@ -128,36 +150,31 @@ def wrapper(obj): return wrapper def __repr__(self): - return f"" - - @property - def __name__(self): - """Return the quantum transform name.""" - return self._transform.__name__ + return f"" @property def transform(self): - """Return the quantum transform.""" + """The quantum transform.""" return self._transform @property def expand_transform(self): - """Return the expand transform.""" + """The expand transform.""" return self._expand_transform @property def classical_cotransform(self): - """Return the classical co-transform.""" + """The classical co-transform.""" return self._classical_cotransform @property def is_informative(self): - """Return True is the transform is informative.""" + """``True`` if the transform is informative.""" return self._is_informative @property def final_transform(self): - """Return True if the transformed tapes must be executed.""" + """``True`` if the transformed tapes must be executed.""" return self._final_transform def custom_qnode_transform(self, fn): @@ -318,14 +335,16 @@ def original_device(self): class TransformContainer: - """Class to store a quantum transform with its args, kwargs and classical co-transforms. Use - :func:`~.pennylane.transforms.core.transform`. + """Class to store a quantum transform with its ``args``, ``kwargs`` and classical co-transforms. Use + :func:`~.pennylane.transform`. .. warning:: - This class is developer-facing and should not be used directly. + This class is developer-facing and should not be used directly. Instead, use + :func:`qml.transform ` if you would like to make a custom + transform. - .. seealso:: :func:`~.pennylane.transforms.core.transform` + .. seealso:: :func:`~.pennylane.transform` """ def __init__( @@ -358,30 +377,30 @@ def __iter__(self): @property def transform(self): - """Return the stored quantum transform.""" + """The stored quantum transform.""" return self._transform @property def args(self): - """Return the stored quantum transform's args.""" + """The stored quantum transform's ``args``.""" return self._args @property def kwargs(self): - """Return the stored quantum transform's arkwgs.""" + """The stored quantum transform's ``kwargs``.""" return self._kwargs @property def classical_cotransform(self): - """Return the stored quantum transform's classical co-transform.""" + """The stored quantum transform's classical co-transform.""" return self._classical_cotransform @property def is_informative(self): - """Return True is the transform is informative.""" + """``True`` if the transform is informative.""" return self._is_informative @property def final_transform(self): - """Return True if the transform needs to be executed""" + """``True`` if the transform needs to be executed""" return self._final_transform diff --git a/pennylane/transforms/core/transform_program.py b/pennylane/transforms/core/transform_program.py index d6ee51fc9c1..c21037d2886 100644 --- a/pennylane/transforms/core/transform_program.py +++ b/pennylane/transforms/core/transform_program.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains the transform program class. +This module contains the ``TransformProgram`` class. """ from functools import partial from typing import Callable, List, Tuple, Optional, Sequence @@ -101,14 +101,21 @@ def null_postprocessing(results: ResultBatch) -> ResultBatch: class TransformProgram: - """Class that contains a transform program and the methods to interact with it. The order of execution is the order - in the list containing the containers. + """Class that contains a transform program and the methods to interact with it. + + The order of execution is the order in the list containing the containers. + + The main case where one would have to interact directly with a transform program is when developing a + :class:`Device `. In this case, the pre-processing method of a device + returns a transform program. You should directly refer to the device API documentation for more details. .. warning:: - This class is developer-facing and should not be used directly. + This class is developer-facing and should not be used directly. Instead, use + :func:`qml.transform ` if you would like to make a custom + transform. - .. seealso:: :func:`~.pennylane.transforms.core.transform` + .. seealso:: :func:`~.pennylane.transform` """ @@ -178,7 +185,7 @@ def add_transform(self, transform: TransformDispatcher, *targs, **tkwargs): """Add a transform (dispatcher) to the end of the program. Note that this should be a function decorated with/called by - `qml.transforms.transform`, and not a `TransformContainer`. + ``qml.transforms.transform``, and not a ``TransformContainer``. Args: transform (TransformDispatcher): The transform to add to the transform program. @@ -267,7 +274,7 @@ def is_empty(self): @property def is_informative(self) -> bool: - """Check if the transform program is informative or not. + """``True`` if the transform program is informative. Returns: bool: Boolean @@ -276,7 +283,7 @@ def is_informative(self) -> bool: @property def has_final_transform(self) -> bool: - """Check if the transform program has a terminal transform or not.""" + """``True`` if the transform program has a terminal transform.""" return self[-1].final_transform if self else False def has_classical_cotransform(self) -> bool: diff --git a/pennylane/transforms/defer_measurements.py b/pennylane/transforms/defer_measurements.py index c39bb081957..3626a83d728 100644 --- a/pennylane/transforms/defer_measurements.py +++ b/pennylane/transforms/defer_measurements.py @@ -18,7 +18,7 @@ from pennylane.ops.op_math import ctrl from pennylane.tape import QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.wires import Wires from pennylane.queuing import QueuingManager @@ -140,14 +140,10 @@ def defer_measurements(tape: QuantumTape, **kwargs) -> (Sequence[QuantumTape], C or an observable are explicitly specified. Args: - tape (.QuantumTape): a quantum tape + tape (QNode or QuantumTape or Callable): a quantum circuit. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. **Example** diff --git a/pennylane/transforms/hamiltonian_expand.py b/pennylane/transforms/hamiltonian_expand.py index 8c3dd49e1cd..ea81a32cd37 100644 --- a/pennylane/transforms/hamiltonian_expand.py +++ b/pennylane/transforms/hamiltonian_expand.py @@ -21,7 +21,7 @@ from pennylane.measurements import ExpectationMP, MeasurementProcess from pennylane.ops import SProd, Sum from pennylane.tape import QuantumScript, QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform @transform @@ -31,15 +31,13 @@ def hamiltonian_expand(tape: QuantumTape, group: bool = True) -> (Sequence[Quant and provides a function to recombine the results. Args: - tape (.QuantumTape): the tape used when calculating the expectation value - of the Hamiltonian + tape (QNode or QuantumTape or Callable): the quantum circuit used when calculating the + expectation value of the Hamiltonian group (bool): Whether to compute disjoint groups of commuting Pauli observables, leading to fewer tapes. If grouping information can be found in the Hamiltonian, it will be used even if group=False. Returns: - tuple[Sequence[.QuantumTape], Callable]: Returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions to compute the expectation value. + qnode (QNode) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. **Example** diff --git a/pennylane/transforms/insert_ops.py b/pennylane/transforms/insert_ops.py index 0e8ea6a03eb..1686e1c981b 100644 --- a/pennylane/transforms/insert_ops.py +++ b/pennylane/transforms/insert_ops.py @@ -20,7 +20,7 @@ import pennylane as qml from pennylane.operation import Operation from pennylane.tape import QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.ops.op_math import Adjoint # pylint: disable=too-many-branches @@ -69,7 +69,7 @@ def insert( for more information). Args: - circuit (QuantumTape): the input circuit to be transformed. + tape (QNode or QuantumTape or Callable or pennylane.devices.Device): the input circuit to be transformed. op (callable or Type[Operation]): the single-qubit operation, or sequence of operations acting on a single qubit, to be inserted into the circuit op_args (tuple or float): the arguments fed to the operation, either as a tuple or a single @@ -83,8 +83,10 @@ def insert( Default is ``False`` and the operation is inserted after. Returns: - callable or QuantumTape or pennylane.Device: the updated version of the input circuit or an updated - device which will transform circuits before execution + qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function] or device (pennylane.devices.Device): + + The transformed circuit as described in :func:`qml.transform `. + Raises: QuantumFunctionError: if some observables in the tape are not qubit-wise commuting @@ -100,8 +102,8 @@ def insert( dev = qml.device("default.mixed", wires=2) + @partial(qml.transforms.insert, qml.AmplitudeDamping, 0.2, position="end") @qml.qnode(dev) - @qml.transforms.insert(qml.AmplitudeDamping, 0.2, position="end") def f(w, x, y, z): qml.RX(w, wires=0) qml.RY(x, wires=1) @@ -173,7 +175,7 @@ def f(w, x, y, z): We can add the :class:`~.AmplitudeDamping` channel to the end of the circuit using: >>> from pennylane.transforms import insert - >>> noisy_tape = insert(qml.AmplitudeDamping, 0.05, position="end")(tape) + >>> noisy_tape = insert(tape, qml.AmplitudeDamping, 0.05, position="end") >>> print(qml.drawer.tape_text(noisy_tape, decimals=2)) 0: ──RX(0.90)─╭●──RY(0.50)──AmplitudeDamping(0.05)─┤ ╭ 1: ──RY(0.40)─╰X──RX(0.60)──AmplitudeDamping(0.05)─┤ ╰ @@ -203,7 +205,7 @@ def f(w, x, y, z): However, noise can be easily added to the device: - >>> dev_noisy = qml.transforms.insert(qml.AmplitudeDamping, 0.2)(dev) + >>> dev_noisy = qml.transforms.insert(dev, qml.AmplitudeDamping, 0.2) >>> qnode_noisy = qml.QNode(f, dev_noisy) >>> qnode_noisy(0.9, 0.4, 0.5, 0.6) tensor(0.72945434, requires_grad=True) diff --git a/pennylane/transforms/metric_tensor.py b/pennylane/transforms/metric_tensor.py index 479e21d7e04..dd1a110d7bf 100644 --- a/pennylane/transforms/metric_tensor.py +++ b/pennylane/transforms/metric_tensor.py @@ -24,7 +24,7 @@ import pennylane as qml from pennylane.circuit_graph import LayerData from pennylane.queuing import WrappedObj -from pennylane.transforms.core import transform +from pennylane.transforms import transform def _contract_metric_tensor_with_cjac(mt, cjac, tape): # pylint: disable=unused-argument @@ -118,7 +118,7 @@ def metric_tensor( # pylint:disable=too-many-arguments This is the case for unitary single-parameter operations. Args: - tape (QuantumTape): quantum tape to find the metric tensor of + tape (QNode or QuantumTape): quantum circuit to find the metric tensor of argnum (int or Sequence[int] or None): Trainable tape-parameter indices with respect to which the metric tensor is computed. If ``argnum=None``, the metric tensor with respect to all trainable parameters is returned. Excluding tape-parameter indices from this list reduces @@ -159,16 +159,10 @@ def metric_tensor( # pylint:disable=too-many-arguments The output shape is a single two-dimensional tensor. Returns: - function or tuple[list[QuantumTape], function]: + qnode (QNode) or tuple[List[QuantumTape], function]: - - If the input is a QNode, an object representing the metric tensor (function) of the - QNode that takes the same arguments as the QNode and can be executed to obtain the - metric tensor (matrix). - - - If the input is a tape, a tuple containing a - list of generated tapes, together with a post-processing - function to be applied to the results of the evaluated tapes - in order to obtain the metric tensor. + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the metric tensor in the form of a tensor. The block-diagonal part of the metric tensor always is computed using the covariance-based approach. If no approximation is selected, diff --git a/pennylane/transforms/mitigate.py b/pennylane/transforms/mitigate.py index 4964b5d627a..d88f02e4fc8 100644 --- a/pennylane/transforms/mitigate.py +++ b/pennylane/transforms/mitigate.py @@ -20,7 +20,7 @@ from pennylane.math import mean, shape, round from pennylane.queuing import AnnotatedQueue from pennylane.tape import QuantumTape, QuantumScript -from pennylane.transforms.core import transform +from pennylane.transforms import transform import pennylane as qml @@ -37,18 +37,11 @@ def fold_global(tape: QuantumTape, scale_factor) -> (Sequence[QuantumTape], Call The purpose of folding is to artificially increase the noise for zero noise extrapolation, see :func:`~.pennylane.transforms.mitigate_with_zne`. Args: - tape (~.QuantumTape): the circuit to be folded + tape (QNode or QuantumTape): the quantum circuit to be folded scale_factor (float): Scale factor :math:`\lambda` determining :math:`n` and :math:`s` Returns: - function or tuple[list[QuantumTape], function]: - - - If the input is a QNode, an object representing the folded QNode that can be executed - with the same arguments as the QNode to obtain the result of the folded circuit. - - - If the input is a tape, a tuple containing a (single-entry) list of generated - circuits, together with a post-processing function that extracts the single tape result - from the evaluated tape list in order to obtain the result of the folded circuit. + qnode (QNode) or tuple[List[QuantumTape], function]: The folded circuit as described in :func:`qml.transform `. .. seealso:: :func:`~.pennylane.transforms.mitigate_with_zne`; This function is analogous to the implementation in ``mitiq`` `mitiq.zne.scaling.fold_global `_. @@ -354,7 +347,7 @@ def mitigate_with_zne( see the example and usage details for further information. Args: - tape (~.QuantumTape): the circuit to be error-mitigated + tape (QNode or QuantumTape): the quantum circuit to be error-mitigated scale_factors (Sequence[float]): the range of noise scale factors used folding (callable): a function that returns a folded circuit for a specified scale factor extrapolate (callable): a function that returns an extrapolated result when provided a @@ -365,7 +358,11 @@ def mitigate_with_zne( folding function is stochastic. Returns: - float: the result of evaluating the circuit when mitigated using ZNE + qnode (QNode) or tuple[List[.QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the mitigated results in the form of a tensor of a tensor, a tuple, or a nested tuple depending + upon the nesting structure of measurements in the original circuit. **Example:** @@ -388,6 +385,7 @@ def mitigate_with_zne( .. code-block:: python3 + from functools import partial from pennylane import numpy as np from pennylane import qnode @@ -400,7 +398,7 @@ def mitigate_with_zne( np.random.seed(0) w1, w2 = [np.random.random(s) for s in shapes] - @qml.transforms.mitigate_with_zne([1., 2., 3.], fold_global, poly_extrapolate, extrapolate_kwargs={'order': 2}) + @partial(qml.transforms.mitigate_with_zne, [1., 2., 3.], fold_global, poly_extrapolate, extrapolate_kwargs={'order': 2}) @qnode(dev) def circuit(w1, w2): qml.SimplifiedTwoDesign(w1, w2, wires=range(2)) diff --git a/pennylane/transforms/optimization/cancel_inverses.py b/pennylane/transforms/optimization/cancel_inverses.py index a8155669110..44b5c49857f 100644 --- a/pennylane/transforms/optimization/cancel_inverses.py +++ b/pennylane/transforms/optimization/cancel_inverses.py @@ -18,7 +18,7 @@ from pennylane.ops.op_math import Adjoint from pennylane.wires import Wires from pennylane.tape import QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.ops.qubit.attributes import ( self_inverses, @@ -69,22 +69,23 @@ def cancel_inverses(tape: QuantumTape) -> (Sequence[QuantumTape], Callable): (self-)inverses or adjoint. Args: - tape (~.QuantumTape): A quantum tape + tape (QNode or QuantumTape or Callable): A quantum circuit. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. + **Example** - Consider the following quantum function: + You can apply the cancel inverses transform directly on :class:`~.QNode`. + + >>> dev = qml.device('default.qubit', wires=3) .. code-block:: python - def qfunc(x, y, z): + @cancel_inverses + @qml.qnode(device=dev) + def circuit(x, y, z): qml.Hadamard(wires=0) qml.Hadamard(wires=1) qml.Hadamard(wires=0) @@ -97,26 +98,48 @@ def qfunc(x, y, z): qml.PauliX(wires=1) return qml.expval(qml.PauliZ(0)) - The circuit before optimization: - - >>> dev = qml.device('default.qubit', wires=3) - >>> qnode = qml.QNode(qfunc, dev) - >>> print(qml.draw(qnode)(1, 2, 3)) - 0: ──H─────────H─────────RZ(3.00)─╭●────┤ - 1: ──H─────────RY(2.00)──X────────│───X─┤ - 2: ──RX(1.00)──RX(2.00)───────────╰X────┤ - - We can see that there are two adjacent Hadamards on the first qubit that - should cancel each other out. Similarly, there are two Pauli-X gates on the - second qubit that should cancel. We can obtain a simplified circuit by running - the ``cancel_inverses`` transform: - - >>> optimized_qfunc = cancel_inverses(qfunc) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - >>> print(qml.draw(optimized_qnode)(1, 2, 3)) - 0: ──RZ(3.00)───────────╭●─┤ - 1: ──H─────────RY(2.00)─│──┤ - 2: ──RX(1.00)──RX(2.00)─╰X─┤ + >>> circuit(0.1, 0.2, 0.3) + 0.999999999999999 + + .. details:: + :title: Usage Details + + You can also apply it on quantum functions: + + .. code-block:: python + + def qfunc(x, y, z): + qml.Hadamard(wires=0) + qml.Hadamard(wires=1) + qml.Hadamard(wires=0) + qml.RX(x, wires=2) + qml.RY(y, wires=1) + qml.PauliX(wires=1) + qml.RZ(z, wires=0) + qml.RX(y, wires=2) + qml.CNOT(wires=[0, 2]) + qml.PauliX(wires=1) + return qml.expval(qml.PauliZ(0)) + + The circuit before optimization: + + >>> qnode = qml.QNode(qfunc, dev) + >>> print(qml.draw(qnode)(1, 2, 3)) + 0: ──H─────────H─────────RZ(3.00)─╭●────┤ + 1: ──H─────────RY(2.00)──X────────│───X─┤ + 2: ──RX(1.00)──RX(2.00)───────────╰X────┤ + + We can see that there are two adjacent Hadamards on the first qubit that + should cancel each other out. Similarly, there are two Pauli-X gates on the + second qubit that should cancel. We can obtain a simplified circuit by running + the ``cancel_inverses`` transform: + + >>> optimized_qfunc = cancel_inverses(qfunc) + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + >>> print(qml.draw(optimized_qnode)(1, 2, 3)) + 0: ──RZ(3.00)───────────╭●─┤ + 1: ──H─────────RY(2.00)─│──┤ + 2: ──RX(1.00)──RX(2.00)─╰X─┤ """ # Make a working copy of the list to traverse diff --git a/pennylane/transforms/optimization/commute_controlled.py b/pennylane/transforms/optimization/commute_controlled.py index 4260495b36b..3b71f76a6a5 100644 --- a/pennylane/transforms/optimization/commute_controlled.py +++ b/pennylane/transforms/optimization/commute_controlled.py @@ -15,7 +15,7 @@ from typing import Sequence, Callable from pennylane.tape import QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.wires import Wires from .optimization_utils import find_next_gate @@ -157,26 +157,27 @@ def commute_controlled(tape: QuantumTape, direction="right") -> (Sequence[Quantu """Quantum transform to move commuting gates past control and target qubits of controlled operations. Args: - tape (QuantumTape): A quantum tape. + tape (QNode or QuantumTape or Callable): A quantum circuit. direction (str): The direction in which to move single-qubit gates. Options are "right" (default), or "left". Single-qubit gates will be pushed through controlled operations as far as possible in the specified direction. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. If a qfunc is passed, - it returns a transformed quantum function. If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. + **Example** - Consider the following quantum function. + >>> dev = qml.device('default.qubit', wires=3) + + You can apply the transform directly on :class:`QNode`: .. code-block:: python - def qfunc(theta): + @partial(commute_controlled, direction="right") + @qml.qnode(device=dev) + def circuit(theta): qml.CZ(wires=[0, 2]) qml.PauliX(wires=2) qml.S(wires=0) @@ -193,26 +194,52 @@ def qfunc(theta): return qml.expval(qml.PauliZ(0)) - >>> dev = qml.device('default.qubit', wires=3) - >>> qnode = qml.QNode(qfunc, dev) - >>> print(qml.draw(qnode)(0.5)) - 0: ─╭●──S─╭●────╭●─────────Rϕ(0.25)─╭●──T────────┤ - 1: ─│─────╰X──Y─╰RY(0.50)───────────├●──RZ(0.25)─┤ - 2: ─╰Z──X───────────────────────────╰X───────────┤ - - Diagonal gates on either side of control qubits do not affect the outcome - of controlled gates; thus we can push all the single-qubit gates on the - first qubit together on the right (and fuse them if desired). Similarly, X - gates commute with the target of ``CNOT`` and ``Toffoli`` (and ``PauliY`` - with ``CRY``). We can use the transform to push single-qubit gates as - far as possible through the controlled operations: - - >>> optimized_qfunc = commute_controlled(direction="right")(qfunc) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - >>> print(qml.draw(optimized_qnode)(0.5)) - 0: ─╭●─╭●─╭●───────────╭●──S─────────Rϕ(0.25)──T─┤ - 1: ─│──╰X─╰RY(0.50)──Y─├●──RZ(0.25)──────────────┤ - 2: ─╰Z─────────────────╰X──X─────────────────────┤ + >>> circuit(0.5) + 0.9999999999999999 + + .. details:: + :title: Usage Details + + You can also apply it on quantum function. + + .. code-block:: python + + def qfunc(theta): + qml.CZ(wires=[0, 2]) + qml.PauliX(wires=2) + qml.S(wires=0) + + qml.CNOT(wires=[0, 1]) + + qml.PauliY(wires=1) + qml.CRY(theta, wires=[0, 1]) + qml.PhaseShift(theta/2, wires=0) + + qml.Toffoli(wires=[0, 1, 2]) + qml.T(wires=0) + qml.RZ(theta/2, wires=1) + + return qml.expval(qml.PauliZ(0)) + + >>> qnode = qml.QNode(qfunc, dev) + >>> print(qml.draw(qnode)(0.5)) + 0: ─╭●──S─╭●────╭●─────────Rϕ(0.25)─╭●──T────────┤ + 1: ─│─────╰X──Y─╰RY(0.50)───────────├●──RZ(0.25)─┤ + 2: ─╰Z──X───────────────────────────╰X───────────┤ + + Diagonal gates on either side of control qubits do not affect the outcome + of controlled gates; thus we can push all the single-qubit gates on the + first qubit together on the right (and fuse them if desired). Similarly, X + gates commute with the target of ``CNOT`` and ``Toffoli`` (and ``PauliY`` + with ``CRY``). We can use the transform to push single-qubit gates as + far as possible through the controlled operations: + + >>> optimized_qfunc = commute_controlled(qfunc, direction="right") + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + >>> print(qml.draw(optimized_qnode)(0.5)) + 0: ─╭●─╭●─╭●───────────╭●──S─────────Rϕ(0.25)──T─┤ + 1: ─│──╰X─╰RY(0.50)──Y─├●──RZ(0.25)──────────────┤ + 2: ─╰Z─────────────────╰X──X─────────────────────┤ """ if direction not in ("left", "right"): diff --git a/pennylane/transforms/optimization/merge_amplitude_embedding.py b/pennylane/transforms/optimization/merge_amplitude_embedding.py index 76f44a57fc9..44d688cc698 100644 --- a/pennylane/transforms/optimization/merge_amplitude_embedding.py +++ b/pennylane/transforms/optimization/merge_amplitude_embedding.py @@ -14,7 +14,7 @@ """Transform for merging AmplitudeEmbedding gates in a quantum circuit.""" from typing import Sequence, Callable -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.tape import QuantumTape from pennylane import AmplitudeEmbedding from pennylane._device import DeviceError @@ -26,41 +26,57 @@ def merge_amplitude_embedding(tape: QuantumTape) -> (Sequence[QuantumTape], Call r"""Quantum function transform to combine amplitude embedding templates that act on different qubits. Args: - tape (QuantumTape): A quantum tape. + tape (QNode or QuantumTape or Callable): A quantum circuit. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. + **Example** - Consider the following quantum function. + >>> dev = qml.device('default.qubit', wires=4) + + You can apply the transform directly on :class:`QNode`: .. code-block:: python - def qfunc(): + @qml.transforms.merge_amplitude_embedding + @qml.qnode(device=dev) + def circuit(): qml.CNOT(wires = [0,1]) qml.AmplitudeEmbedding([0,1], wires = 2) qml.AmplitudeEmbedding([0,1], wires = 3) return qml.state() - The circuit before compilation will not work because of using two amplitude embedding. + >>> circuit() + [1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j] - Using the transformation we can join the different amplitude embedding into a single one: + .. details:: + :title: Usage Details - >>> dev = qml.device('default.qubit', wires=4) - >>> optimized_qfunc = qml.transforms.merge_amplitude_embedding(qfunc) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - >>> print(qml.draw(optimized_qnode)()) - 0: ─╭●──────────────────────┤ State - 1: ─╰X──────────────────────┤ State - 2: ─╭AmplitudeEmbedding(M0)─┤ State - 3: ─╰AmplitudeEmbedding(M0)─┤ State - M0 = - [0.+0.j 0.+0.j 0.+0.j 1.+0.j] + You can also apply it on quantum function. + + .. code-block:: python + + def qfunc(): + qml.CNOT(wires = [0,1]) + qml.AmplitudeEmbedding([0,1], wires = 2) + qml.AmplitudeEmbedding([0,1], wires = 3) + return qml.state() + + The circuit before compilation will not work because of using two amplitude embedding. + + Using the transformation we can join the different amplitude embedding into a single one: + + >>> optimized_qfunc = qml.transforms.merge_amplitude_embedding(qfunc) + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + >>> print(qml.draw(optimized_qnode)()) + 0: ─╭●──────────────────────┤ State + 1: ─╰X──────────────────────┤ State + 2: ─╭AmplitudeEmbedding(M0)─┤ State + 3: ─╰AmplitudeEmbedding(M0)─┤ State + M0 = + [0.+0.j 0.+0.j 0.+0.j 1.+0.j] """ # Make a working copy of the list to traverse diff --git a/pennylane/transforms/optimization/merge_rotations.py b/pennylane/transforms/optimization/merge_rotations.py index 7281825dea1..91cb67efa15 100644 --- a/pennylane/transforms/optimization/merge_rotations.py +++ b/pennylane/transforms/optimization/merge_rotations.py @@ -16,7 +16,7 @@ from typing import Sequence, Callable from pennylane.tape import QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.math import allclose, stack, cast_like, zeros, is_abstract from pennylane.queuing import QueuingManager @@ -35,7 +35,7 @@ def merge_rotations( neither gate will be applied. Args: - tape (QuantumTape): A quantum tape. + tape (QNode or QuantumTape or Callable): A quantum circuit. atol (float): After fusion of gates, if the fused angle :math:`\theta` is such that :math:`|\theta|\leq \text{atol}`, no rotation gate will be applied. include_gates (None or list[str]): A list of specific operations to merge. If @@ -44,19 +44,19 @@ def merge_rotations( only the operations whose names match those in the list will undergo merging. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. **Example** - Consider the following quantum function. + >>> dev = qml.device('default.qubit', wires=3) + + You can apply the transform directly on :class:`QNode` .. code-block:: python - def qfunc(x, y, z): + @merge_rotations + @qml.qnode(device=dev) + def circuit(x, y, z): qml.RX(x, wires=0) qml.RX(y, wires=0) qml.CNOT(wires=[1, 2]) @@ -66,35 +66,54 @@ def qfunc(x, y, z): qml.RY(-y, wires=1) return qml.expval(qml.PauliZ(0)) - The circuit before optimization: - - >>> dev = qml.device('default.qubit', wires=3) - >>> qnode = qml.QNode(qfunc, dev) - >>> print(qml.draw(qnode)(1, 2, 3)) - 0: ──RX(1.00)──RX(2.00)─╭RZ(3.00)────────────┤ - 1: ─╭●─────────RY(2.00)─│──────────RY(-2.00)─┤ - 2: ─╰X─────────H────────╰●───────────────────┤ - - By inspection, we can combine the two ``RX`` rotations on the first qubit. - On the second qubit, we have a cumulative angle of 0, and the gates will cancel. - - >>> optimized_qfunc = merge_rotations()(qfunc) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - >>> print(qml.draw(optimized_qnode)(1, 2, 3)) - 0: ──RX(3.00)────╭RZ(3.00)─┤ - 1: ─╭●───────────│─────────┤ - 2: ─╰X─────────H─╰●────────┤ - - It is also possible to explicitly specify which rotations ``merge_rotations`` should - be merged using the ``include_gates`` argument. For example, if in the above - circuit we wanted only to merge the "RX" gates, we could do so as follows: - - >>> optimized_qfunc = merge_rotations(include_gates=["RX"])(qfunc) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - >>> print(qml.draw(optimized_qnode)(1, 2, 3)) - 0: ──RX(3.00)───────────╭RZ(3.00)────────────┤ - 1: ─╭●─────────RY(2.00)─│──────────RY(-2.00)─┤ - 2: ─╰X─────────H────────╰●───────────────────┤ + >>> circuit(0.1, 0.2, 0.3) + 0.9553364891256055 + + .. details:: + :title: Usage Details + + You can also apply it on quantum function. + + .. code-block:: python + + def qfunc(x, y, z): + qml.RX(x, wires=0) + qml.RX(y, wires=0) + qml.CNOT(wires=[1, 2]) + qml.RY(y, wires=1) + qml.Hadamard(wires=2) + qml.CRZ(z, wires=[2, 0]) + qml.RY(-y, wires=1) + return qml.expval(qml.PauliZ(0)) + + The circuit before optimization: + + >>> qnode = qml.QNode(qfunc, dev) + >>> print(qml.draw(qnode)(1, 2, 3)) + 0: ──RX(1.00)──RX(2.00)─╭RZ(3.00)────────────┤ + 1: ─╭●─────────RY(2.00)─│──────────RY(-2.00)─┤ + 2: ─╰X─────────H────────╰●───────────────────┤ + + By inspection, we can combine the two ``RX`` rotations on the first qubit. + On the second qubit, we have a cumulative angle of 0, and the gates will cancel. + + >>> optimized_qfunc = merge_rotations()(qfunc) + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + >>> print(qml.draw(optimized_qnode)(1, 2, 3)) + 0: ──RX(3.00)────╭RZ(3.00)─┤ + 1: ─╭●───────────│─────────┤ + 2: ─╰X─────────H─╰●────────┤ + + It is also possible to explicitly specify which rotations ``merge_rotations`` should + be merged using the ``include_gates`` argument. For example, if in the above + circuit we wanted only to merge the "RX" gates, we could do so as follows: + + >>> optimized_qfunc = merge_rotations(include_gates=["RX"])(qfunc) + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + >>> print(qml.draw(optimized_qnode)(1, 2, 3)) + 0: ──RX(3.00)───────────╭RZ(3.00)────────────┤ + 1: ─╭●─────────RY(2.00)─│──────────RY(-2.00)─┤ + 2: ─╰X─────────H────────╰●───────────────────┤ """ # Expand away adjoint ops diff --git a/pennylane/transforms/optimization/pattern_matching.py b/pennylane/transforms/optimization/pattern_matching.py index 9cb9d728e34..113e9efe15d 100644 --- a/pennylane/transforms/optimization/pattern_matching.py +++ b/pennylane/transforms/optimization/pattern_matching.py @@ -22,7 +22,7 @@ import numpy as np import pennylane as qml -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane import adjoint from pennylane.ops.qubit.attributes import symmetric_over_all_wires from pennylane.tape import QuantumTape, QuantumScript @@ -38,16 +38,12 @@ def pattern_matching_optimization( r"""Quantum function transform to optimize a circuit given a list of patterns (templates). Args: - tape (QuantumTape): A quantum tape to be optimized. + tape (QNode or QuantumTape or Callable): A quantum circuit to be optimized. pattern_tapes(list(.QuantumTape)): List of quantum tapes that implement the identity. custom_quantum_cost (dict): Optional, quantum cost that overrides the default cost dictionary. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. Raises: QuantumFunctionError: The pattern provided is not a valid QuantumTape or the pattern contains measurements or @@ -55,11 +51,24 @@ def pattern_matching_optimization( **Example** - First let's consider the following circuit where we want to replace sequence of two ``pennylane.S`` gates with a + >>> dev = qml.device('default.qubit', wires=5) + + You can apply the transform directly on a :class:`QNode`. For that, you need first to define a pattern that is to be + found in the circuit. We use the following pattern that implements the identity: + + .. code-block:: python + + ops = [qml.S(0), qml.S(0), qml.PauliZ(0)] + pattern = qml.tape.QuantumTape(ops) + + + Let's consider the following circuit where we want to replace a sequence of two ``pennylane.S`` gates with a ``pennylane.PauliZ`` gate. .. code-block:: python + @partial(pattern_matching_optimization, pattern_tapes = [pattern]) + @qml.qnode(device=dev) def circuit(): qml.S(wires=0) qml.PauliZ(wires=0) @@ -71,102 +80,114 @@ def circuit(): qml.S(wires=2) return qml.expval(qml.PauliX(wires=0)) - Therefore we use the following pattern that implements the identity: - - .. code-block:: python - - ops = [qml.S(0), qml.S(0), qml.PauliZ(0)] - pattern = qml.tape.QuantumTape(ops) - - For optimizing the circuit given the following template of CNOTs we apply the ``pattern_matching`` - transform. - - >>> dev = qml.device('default.qubit', wires=5) - >>> qnode = qml.QNode(circuit, dev) - >>> optimized_qfunc = pattern_matching_optimization(pattern_tapes=[pattern])(circuit) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - - >>> print(qml.draw(qnode)()) - 0: ──S──Z─╭●──────────┤ - 1: ──S────╰Z──S─╭●────┤ - 2: ──S──────────╰Z──S─┤ - - >>> print(qml.draw(optimized_qnode)()) - 0: ──S†─╭●────┤ - 1: ──Z──╰Z─╭●─┤ - 2: ──Z─────╰Z─┤ - - Note that with this pattern we also replace a ``pennylane.S``, ``pennylane.PauliZ`` sequence by - ``Adjoint(S)``. If one would like avoiding this, it possible to give a custom - quantum cost dictionary with a negative cost for ``pennylane.PauliZ``. - - >>> my_cost = {"PauliZ": -1 , "S": 1, "Adjoint(S)": 1} - >>> optimized_qfunc = pattern_matching_optimization(pattern_tapes=[pattern], custom_quantum_cost=my_cost)(circuit) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - - >>> print(qml.draw(optimized_qnode)()) - 0: ──S──Z─╭●────┤ - 1: ──Z────╰Z─╭●─┤ - 2: ──Z───────╰Z─┤ - - Now we can consider a more complicated example with the following quantum circuit to be optimized - - .. code-block:: python - - def circuit(): - qml.Toffoli(wires=[3, 4, 0]) - qml.CNOT(wires=[1, 4]) - qml.CNOT(wires=[2, 1]) - qml.Hadamard(wires=3) - qml.PauliZ(wires=1) - qml.CNOT(wires=[2, 3]) - qml.Toffoli(wires=[2, 3, 0]) - qml.CNOT(wires=[1, 4]) - return qml.expval(qml.PauliX(wires=0)) - - We define a pattern that implement the identity: - - .. code-block:: python - - ops = [ - qml.CNOT(wires=[1, 2]), - qml.CNOT(wires=[0, 1]), - qml.CNOT(wires=[1, 2]), - qml.CNOT(wires=[0, 1]), - qml.CNOT(wires=[0, 2]), - ] - tape = qml.tape.QuantumTape(ops) - - For optimizing the circuit given the given following pattern of CNOTs we apply the `pattern_matching` - transform. - - >>> dev = qml.device('default.qubit', wires=5) - >>> qnode = qml.QNode(circuit, dev) - >>> optimized_qfunc = pattern_matching_optimization(pattern_tapes=[pattern])(circuit) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - - In our case, it is possible to find three CNOTs and replace this pattern with only two CNOTs and therefore - optimizing the circuit. The number of CNOTs in the circuit is reduced by one. - - >>> qml.specs(qnode)()["resources"].gate_types["CNOT"] - 4 - - >>> qml.specs(optimized_qnode)()["resources"].gate_types["CNOT"] - 3 - - >>> print(qml.draw(qnode)()) - 0: ─╭X──────────╭X────┤ - 1: ─│──╭●─╭X──Z─│──╭●─┤ - 2: ─│──│──╰●─╭●─├●─│──┤ - 3: ─├●─│───H─╰X─╰●─│──┤ - 4: ─╰●─╰X──────────╰X─┤ - - >>> print(qml.draw(optimized_qnode)()) - 0: ─╭X──────────╭X─┤ - 1: ─│─────╭X──Z─│──┤ - 2: ─│──╭●─╰●─╭●─├●─┤ - 3: ─├●─│───H─╰X─╰●─┤ - 4: ─╰●─╰X──────────┤ + During the call of the circuit, it is first optimized (if possible) and then executed. + + >>> circuit() + + .. details:: + :title: Usage Details + + .. code-block:: python + + def circuit(): + qml.S(wires=0) + qml.PauliZ(wires=0) + qml.S(wires=1) + qml.CZ(wires=[0, 1]) + qml.S(wires=1) + qml.S(wires=2) + qml.CZ(wires=[1, 2]) + qml.S(wires=2) + return qml.expval(qml.PauliX(wires=0)) + + For optimizing the circuit given the following template of CNOTs we apply the ``pattern_matching`` + transform. + + >>> qnode = qml.QNode(circuit, dev) + >>> optimized_qfunc = pattern_matching_optimization(pattern_tapes=[pattern])(circuit) + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + + >>> print(qml.draw(qnode)()) + 0: ──S──Z─╭●──────────┤ + 1: ──S────╰Z──S─╭●────┤ + 2: ──S──────────╰Z──S─┤ + + >>> print(qml.draw(optimized_qnode)()) + 0: ──S†─╭●────┤ + 1: ──Z──╰Z─╭●─┤ + 2: ──Z─────╰Z─┤ + + Note that with this pattern we also replace a ``pennylane.S``, ``pennylane.PauliZ`` sequence by + ``Adjoint(S)``. If one would like avoiding this, it possible to give a custom + quantum cost dictionary with a negative cost for ``pennylane.PauliZ``. + + >>> my_cost = {"PauliZ": -1 , "S": 1, "Adjoint(S)": 1} + >>> optimized_qfunc = pattern_matching_optimization(circuit, pattern_tapes=[pattern], custom_quantum_cost=my_cost) + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + + >>> print(qml.draw(optimized_qnode)()) + 0: ──S──Z─╭●────┤ + 1: ──Z────╰Z─╭●─┤ + 2: ──Z───────╰Z─┤ + + Now we can consider a more complicated example with the following quantum circuit to be optimized + + .. code-block:: python + + def circuit(): + qml.Toffoli(wires=[3, 4, 0]) + qml.CNOT(wires=[1, 4]) + qml.CNOT(wires=[2, 1]) + qml.Hadamard(wires=3) + qml.PauliZ(wires=1) + qml.CNOT(wires=[2, 3]) + qml.Toffoli(wires=[2, 3, 0]) + qml.CNOT(wires=[1, 4]) + return qml.expval(qml.PauliX(wires=0)) + + We define a pattern that implement the identity: + + .. code-block:: python + + ops = [ + qml.CNOT(wires=[1, 2]), + qml.CNOT(wires=[0, 1]), + qml.CNOT(wires=[1, 2]), + qml.CNOT(wires=[0, 1]), + qml.CNOT(wires=[0, 2]), + ] + tape = qml.tape.QuantumTape(ops) + + For optimizing the circuit given the given following pattern of CNOTs we apply the `pattern_matching` + transform. + + >>> dev = qml.device('default.qubit', wires=5) + >>> qnode = qml.QNode(circuit, dev) + >>> optimized_qfunc = pattern_matching_optimization(circuit, pattern_tapes=[pattern]) + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + + In our case, it is possible to find three CNOTs and replace this pattern with only two CNOTs and therefore + optimizing the circuit. The number of CNOTs in the circuit is reduced by one. + + >>> qml.specs(qnode)()["resources"].gate_types["CNOT"] + 4 + + >>> qml.specs(optimized_qnode)()["resources"].gate_types["CNOT"] + 3 + + >>> print(qml.draw(qnode)()) + 0: ─╭X──────────╭X────┤ + 1: ─│──╭●─╭X──Z─│──╭●─┤ + 2: ─│──│──╰●─╭●─├●─│──┤ + 3: ─├●─│───H─╰X─╰●─│──┤ + 4: ─╰●─╰X──────────╰X─┤ + + >>> print(qml.draw(optimized_qnode)()) + 0: ─╭X──────────╭X─┤ + 1: ─│─────╭X──Z─│──┤ + 2: ─│──╭●─╰●─╭●─├●─┤ + 3: ─├●─│───H─╰X─╰●─┤ + 4: ─╰●─╰X──────────┤ .. seealso:: :func:`~.pattern_matching` diff --git a/pennylane/transforms/optimization/remove_barrier.py b/pennylane/transforms/optimization/remove_barrier.py index 150c5fab1d5..708d863f7dd 100644 --- a/pennylane/transforms/optimization/remove_barrier.py +++ b/pennylane/transforms/optimization/remove_barrier.py @@ -16,7 +16,7 @@ from typing import Sequence, Callable from pennylane.tape import QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform @transform @@ -24,44 +24,58 @@ def remove_barrier(tape: QuantumTape) -> (Sequence[QuantumTape], Callable): """Quantum transform to remove Barrier gates. Args: - qfunc (function): A quantum function. + tape (QNode or QuantumTape or Callable): A quantum circuit. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. **Example** - Consider the following quantum function: + The transform can be applied on :class:`QNode` directly. .. code-block:: python - def qfunc(x, y): + @remove_barrier + @qml.qnode(device=dev) + def circuit(x, y): qml.Hadamard(wires=0) qml.Hadamard(wires=1) qml.Barrier(wires=[0,1]) qml.PauliX(wires=0) return qml.expval(qml.PauliZ(0)) - The circuit before optimization: + The barrier is then removed before execution. - >>> dev = qml.device('default.qubit', wires=2) - >>> qnode = qml.QNode(qfunc, dev) - >>> print(qml.draw(qnode)(1, 2)) - 0: ──H──╭||──X──┤ ⟨Z⟩ - 1: ──H──╰||─────┤ + .. details:: + :title: Usage Details + Consider the following quantum function: - We can remove the Barrier by running the ``remove_barrier`` transform: + .. code-block:: python - >>> optimized_qfunc = remove_barrier(qfunc) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - >>> print(qml.draw(optimized_qnode)(1, 2)) - 0: ──H──X──┤ ⟨Z⟩ - 1: ──H─────┤ + def qfunc(x, y): + qml.Hadamard(wires=0) + qml.Hadamard(wires=1) + qml.Barrier(wires=[0,1]) + qml.PauliX(wires=0) + return qml.expval(qml.PauliZ(0)) + + The circuit before optimization: + + >>> dev = qml.device('default.qubit', wires=2) + >>> qnode = qml.QNode(qfunc, dev) + >>> print(qml.draw(qnode)(1, 2)) + 0: ──H──╭||──X──┤ ⟨Z⟩ + 1: ──H──╰||─────┤ + + + We can remove the Barrier by running the ``remove_barrier`` transform: + + >>> optimized_qfunc = remove_barrier(qfunc) + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + >>> print(qml.draw(optimized_qnode)(1, 2)) + 0: ──H──X──┤ ⟨Z⟩ + 1: ──H─────┤ """ # Make a working copy of the list to traverse diff --git a/pennylane/transforms/optimization/single_qubit_fusion.py b/pennylane/transforms/optimization/single_qubit_fusion.py index 21ef18ab68b..e3b89a9e18e 100644 --- a/pennylane/transforms/optimization/single_qubit_fusion.py +++ b/pennylane/transforms/optimization/single_qubit_fusion.py @@ -16,7 +16,7 @@ from typing import Sequence, Callable from pennylane.tape import QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.ops.qubit import Rot from pennylane.math import allclose, stack, is_abstract from pennylane.queuing import QueuingManager @@ -36,7 +36,7 @@ def single_qubit_fusion( (on the same qubit) with that property defined will be fused into one ``Rot``. Args: - tape (QuantumTape): A quantum tape. + tape (QNode or QuantumTape or Callable): A quantum circuit. atol (float): An absolute tolerance for which to apply a rotation after fusion. After fusion of gates, if the fused angles :math:`\theta` are such that :math:`|\theta|\leq \text{atol}`, no rotation gate will be applied. @@ -45,18 +45,18 @@ def single_qubit_fusion( be fused will be fused. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. **Example** - Consider the following quantum function. + >>> dev = qml.device('default.qubit', wires=1) + + You can apply the transform directly on :class:`QNode`: .. code-block:: python + @single_qubit_fusion + @qml.qnode(device=dev) def qfunc(r1, r2): qml.Hadamard(wires=0) qml.Rot(*r1, wires=0) @@ -65,20 +65,36 @@ def qfunc(r1, r2): qml.RZ(r2[0], wires=0) return qml.expval(qml.PauliX(0)) - The circuit before optimization: + The single qubit gates are fused before execution. - >>> dev = qml.device('default.qubit', wires=1) - >>> qnode = qml.QNode(qfunc, dev) - >>> print(qml.draw(qnode)([0.1, 0.2, 0.3], [0.4, 0.5, 0.6])) - 0: ──H──Rot(0.1, 0.2, 0.3)──Rot(0.4, 0.5, 0.6)──RZ(0.1)──RZ(0.4)──┤ ⟨X⟩ + .. details:: + :title: Usage Details + + Consider the following quantum function. + + .. code-block:: python + + def qfunc(r1, r2): + qml.Hadamard(wires=0) + qml.Rot(*r1, wires=0) + qml.Rot(*r2, wires=0) + qml.RZ(r1[0], wires=0) + qml.RZ(r2[0], wires=0) + return qml.expval(qml.PauliX(0)) + + The circuit before optimization: + + >>> qnode = qml.QNode(qfunc, dev) + >>> print(qml.draw(qnode)([0.1, 0.2, 0.3], [0.4, 0.5, 0.6])) + 0: ──H──Rot(0.1, 0.2, 0.3)──Rot(0.4, 0.5, 0.6)──RZ(0.1)──RZ(0.4)──┤ ⟨X⟩ - Full single-qubit gate fusion allows us to collapse this entire sequence into a - single ``qml.Rot`` rotation gate. + Full single-qubit gate fusion allows us to collapse this entire sequence into a + single ``qml.Rot`` rotation gate. - >>> optimized_qfunc = single_qubit_fusion(qfunc) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - >>> print(qml.draw(optimized_qnode)([0.1, 0.2, 0.3], [0.4, 0.5, 0.6])) - 0: ──Rot(3.57, 2.09, 2.05)──┤ ⟨X⟩ + >>> optimized_qfunc = single_qubit_fusion(qfunc) + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + >>> print(qml.draw(optimized_qnode)([0.1, 0.2, 0.3], [0.4, 0.5, 0.6])) + 0: ──Rot(3.57, 2.09, 2.05)──┤ ⟨X⟩ """ # Make a working copy of the list to traverse diff --git a/pennylane/transforms/optimization/undo_swaps.py b/pennylane/transforms/optimization/undo_swaps.py index be35951cd91..2f8dbcf3d95 100644 --- a/pennylane/transforms/optimization/undo_swaps.py +++ b/pennylane/transforms/optimization/undo_swaps.py @@ -16,7 +16,7 @@ # pylint: disable=too-many-branches from typing import Sequence, Callable -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.tape import QuantumTape from pennylane.wires import Wires @@ -29,22 +29,22 @@ def undo_swaps(tape: QuantumTape) -> (Sequence[QuantumTape], Callable): to left through the circuit changing the position of the qubits accordingly. Args: - tape (QuantumTape): A quantum tape. + tape (QNode or QuantumTape or Callable): A quantum circuit. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. **Example** - Consider the following quantum function: + >>> dev = qml.device('default.qubit', wires=3) + + You can apply the transform directly on a :class:`QNode` .. code-block:: python - def qfunc(): + @undo_swaps + @qml.qnode(device=dev) + def circuit(): qml.Hadamard(wires=0) qml.PauliX(wires=1) qml.SWAP(wires=[0,1]) @@ -52,24 +52,41 @@ def qfunc(): qml.PauliY(wires=0) return qml.expval(qml.PauliZ(0)) - The circuit before optimization: + The SWAP gates are removed before execution. - >>> dev = qml.device('default.qubit', wires=3) - >>> qnode = qml.QNode(qfunc, dev) - >>> print(qml.draw(qnode)()) - 0: ──H──╭SWAP──╭SWAP──Y──┤ ⟨Z⟩ - 1: ──X──╰SWAP──│─────────┤ - 2: ────────────╰SWAP─────┤ + .. details:: + :title: Usage Details + + Consider the following quantum function: + + .. code-block:: python + + def qfunc(): + qml.Hadamard(wires=0) + qml.PauliX(wires=1) + qml.SWAP(wires=[0,1]) + qml.SWAP(wires=[0,2]) + qml.PauliY(wires=0) + return qml.expval(qml.PauliZ(0)) + + The circuit before optimization: + + >>> dev = qml.device('default.qubit', wires=3) + >>> qnode = qml.QNode(qfunc, dev) + >>> print(qml.draw(qnode)()) + 0: ──H──╭SWAP──╭SWAP──Y──┤ ⟨Z⟩ + 1: ──X──╰SWAP──│─────────┤ + 2: ────────────╰SWAP─────┤ - We can remove the SWAP gates by running the ``undo_swap`` transform: + We can remove the SWAP gates by running the ``undo_swap`` transform: - >>> optimized_qfunc = undo_swaps(qfunc) - >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) - >>> print(qml.draw(optimized_qnode)()) - 0: ──Y──┤ ⟨Z⟩ - 1: ──H──┤ - 2: ──X──┤ + >>> optimized_qfunc = undo_swaps(qfunc) + >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) + >>> print(qml.draw(optimized_qnode)()) + 0: ──Y──┤ ⟨Z⟩ + 1: ──H──┤ + 2: ──X──┤ """ # Make a working copy of the list to traverse diff --git a/pennylane/transforms/qcut/cutcircuit.py b/pennylane/transforms/qcut/cutcircuit.py index ae110eed667..b4199c8e6c1 100644 --- a/pennylane/transforms/qcut/cutcircuit.py +++ b/pennylane/transforms/qcut/cutcircuit.py @@ -21,7 +21,7 @@ import pennylane as qml from pennylane.measurements import ExpectationMP from pennylane.tape import QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.wires import Wires from .cutstrategy import CutStrategy @@ -90,7 +90,7 @@ def cut_circuit( Only circuits that return a single expectation value are supported. Args: - tape (QuantumTape): the tape of the full circuit to be cut + tape (QNode or QuantumTape): the quantum circuit to be cut auto_cutter (Union[bool, Callable]): Toggle for enabling automatic cutting with the default :func:`~.kahypar_cut` partition method. Can also pass a graph partitioning function that takes an input graph and returns a list of edges to be cut based on a given set of @@ -112,10 +112,10 @@ def cut_circuit( :func:`~.find_and_place_cuts` and :func:`~.kahypar_cut` for the available arguments. Returns: - Callable: Function which accepts the same arguments as the QNode. - When called, this function will perform a process tomography of the - partitioned circuit fragments and combine the results via tensor - contractions. + qnode (QNode) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will perform a process tomography of the partitioned circuit fragments and combine the results via tensor contractions. **Example** diff --git a/pennylane/transforms/qcut/montecarlo.py b/pennylane/transforms/qcut/montecarlo.py index 430105ce9ab..675ddd0cbb0 100644 --- a/pennylane/transforms/qcut/montecarlo.py +++ b/pennylane/transforms/qcut/montecarlo.py @@ -26,7 +26,7 @@ from pennylane.measurements import SampleMP from pennylane.queuing import AnnotatedQueue from pennylane.tape import QuantumScript, QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.wires import Wires from .cutstrategy import CutStrategy @@ -87,7 +87,7 @@ def cut_circuit_mc( an expectation value will be evaluated. Args: - tape (QuantumTape): the tape of the full circuit to be cut + tape (QNode or QuantumTape): the quantum circuit to be cut. classical_processing_fn (callable): A classical postprocessing function to be applied to the reconstructed bitstrings. The expected input is a bitstring; a flat array of length ``wires``, and the output should be a single number within the interval :math:`[-1, 1]`. @@ -111,9 +111,10 @@ def cut_circuit_mc( :func:`~.find_and_place_cuts` and :func:`~.kahypar_cut` for the available arguments. Returns: - Callable: Function which accepts the same arguments as the QNode. - When called, this function will sample from the partitioned circuit fragments - and combine the results using a Monte Carlo method. + qnode (QNode) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will sample from the partitioned circuit fragments and combine the results using a Monte Carlo method. **Example** diff --git a/pennylane/transforms/sign_expand/sign_expand.py b/pennylane/transforms/sign_expand/sign_expand.py index 312625beca9..211af90cb8a 100644 --- a/pennylane/transforms/sign_expand/sign_expand.py +++ b/pennylane/transforms/sign_expand/sign_expand.py @@ -19,7 +19,7 @@ import pennylane as qml from pennylane import numpy as np -from pennylane.transforms.core import transform +from pennylane.transforms import transform def controlled_pauli_evolution(theta, wires, pauli_word, controls): @@ -208,7 +208,7 @@ def sign_expand( # pylint: disable=too-many-arguments For the calculation of variances, one assumes an even distribution of shots among the groups. Args: - tape (.QuantumTape): the tape used when calculating the expectation value of the Hamiltonian + tape (QNode or QuantumTape): the quantum circuit used when calculating the expectation value of the Hamiltonian circuit (bool): Toggle the calculation of the analytical Xi decomposition or if True constructs the circuits of the approximate sign decomposition to measure the expectation value @@ -219,9 +219,7 @@ def sign_expand( # pylint: disable=too-many-arguments Hadamard test and the quantum signal processing part on, have to be wires on the device Returns: - tuple[list[.QuantumTape], function]: Returns a tuple containing a list of quantum tapes - to be evaluated, and a function to be applied to these tape executions to compute the - expectation value. + qnode (pennylane.QNode) or tuple[List[.QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. **Example** diff --git a/pennylane/transforms/split_non_commuting.py b/pennylane/transforms/split_non_commuting.py index ef4c64b3266..4399680bcad 100644 --- a/pennylane/transforms/split_non_commuting.py +++ b/pennylane/transforms/split_non_commuting.py @@ -21,7 +21,7 @@ import pennylane as qml from pennylane.measurements import ProbabilityMP, SampleMP -from pennylane.transforms.core import transform +from pennylane.transforms import transform @transform @@ -30,15 +30,11 @@ def split_non_commuting(tape: qml.tape.QuantumTape) -> (Sequence[qml.tape.Quantu Splits a qnode measuring non-commuting observables into groups of commuting observables. Args: - qnode (pennylane.QNode or .QuantumTape): quantum tape or QNode that contains a list of + tape (QNode or QuantumTape or Callable): A circuit that contains a list of non-commuting observables to measure. Returns: - qnode (pennylane.QNode) or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode capable of handling non-commuting groups. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions to restore the ordering of the inputs. + qnode (QNode) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. **Example** diff --git a/pennylane/transforms/transpile.py b/pennylane/transforms/transpile.py index 5999e9a8c68..b8a17595b40 100644 --- a/pennylane/transforms/transpile.py +++ b/pennylane/transforms/transpile.py @@ -5,7 +5,7 @@ import networkx as nx -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane import Hamiltonian from pennylane.operation import Tensor from pennylane.ops import __all__ as all_ops @@ -27,16 +27,12 @@ def transpile( is passed which contains these types of measurements, a ``NotImplementedError`` will be raised. Args: - tape (QuantumTape): A quantum tape. + tape (QNode or QuantumTape or Callable): A quantum tape. coupling_map (list[tuple(int, int)] or nx.Graph): Either a list of tuples(int, int) or an instance of `networkx.Graph` specifying the couplings between different qubits. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. **Example** @@ -74,7 +70,7 @@ def circuit(): with the circuit, to the transpile function to get a circuit which can be executed for the specified coupling map: >>> dev = qml.device('default.qubit', wires=[0, 1, 2, 3]) - >>> transpiled_circuit = qml.transforms.transpile(coupling_map=[(0, 1), (1, 3), (3, 2), (2, 0)])(circuit) + >>> transpiled_circuit = qml.transforms.transpile(circuit, coupling_map=[(0, 1), (1, 3), (3, 2), (2, 0)]) >>> transpiled_qnode = qml.QNode(transpiled_circuit, dev) >>> print(qml.draw(transpiled_qnode)()) 0: ─╭●────────────────╭●─┤ ╭Probs diff --git a/pennylane/transforms/unitary_to_rot.py b/pennylane/transforms/unitary_to_rot.py index 36ddc3f4b7d..23b4251d481 100644 --- a/pennylane/transforms/unitary_to_rot.py +++ b/pennylane/transforms/unitary_to_rot.py @@ -18,7 +18,7 @@ from pennylane.queuing import QueuingManager from pennylane.tape import QuantumTape -from pennylane.transforms.core import transform +from pennylane.transforms import transform import pennylane as qml from pennylane.transforms.decompositions import one_qubit_decomposition, two_qubit_decomposition @@ -41,14 +41,10 @@ def unitary_to_rot(tape: QuantumTape) -> (Sequence[QuantumTape], Callable): operations. See usage details below. Args: - tape (QuantumTape): a quantum tape + tape (QNode or QuantumTape or Callable): A quantum circuit. Returns: - pennylane.QNode or qfunc or tuple[List[.QuantumTape], function]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. **Example** diff --git a/pennylane/transforms/zx/converter.py b/pennylane/transforms/zx/converter.py index efbe3d6ed5c..79bd1c5daab 100644 --- a/pennylane/transforms/zx/converter.py +++ b/pennylane/transforms/zx/converter.py @@ -21,7 +21,7 @@ import pennylane as qml from pennylane.tape import QuantumScript, QuantumTape from pennylane.transforms.op_transforms import OperationTransformError -from pennylane.transforms.core import transform +from pennylane.transforms import transform from pennylane.wires import Wires @@ -54,10 +54,16 @@ def to_zx(tape, expand_measurements=False): # pylint: disable=unused-argument The graph can be optimized and transformed by well-known ZX-calculus reductions. Args: - tape(QuantumTape, Operation): The PennyLane quantum circuit. + tape(QNode or QuantumTape or Callable or Operation): The PennyLane quantum circuit. expand_measurements(bool): The expansion will be applied on measurements that are not in the Z-basis and rotations will be added to the operations. + Returns: + graph (pyzx.Graph) or qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the ZX graph in the form of a PyZX graph. + **Example** You can use the transform decorator directly on your :class:`~.QNode`, quantum function and executing it will produce a @@ -227,7 +233,8 @@ def mod_5_4(): wires = qml.wires.Wires([4, 3, 0, 2, 1]) wires_map = dict(zip(tape_opt.wires, wires)) - tape_opt_reorder = qml.map_wires(input=tape_opt, wire_map=wires_map)[0][0] + tapes_opt_reorder, fn = qml.map_wires(input=tape_opt, wire_map=wires_map)[0][0] + tape_opt_reorder = fn(tapes_opt_reorder) @qml.qnode(device=dev) def mod_5_4(): diff --git a/tests/transforms/test_experimental/test_transform_dispatcher.py b/tests/transforms/test_experimental/test_transform_dispatcher.py index 5dbb1d6bc5f..bcbc823f417 100644 --- a/tests/transforms/test_experimental/test_transform_dispatcher.py +++ b/tests/transforms/test_experimental/test_transform_dispatcher.py @@ -607,3 +607,18 @@ def test_old_device_transform_error(self, valid_transform): ): dispatched_transform = transform(valid_transform, expand_transform=valid_transform) dispatched_transform(device, index=0) + + def test_sphinx_build(self, monkeypatch): + """Test that transforms are not created during Sphinx builds""" + monkeypatch.setenv("SPHINX_BUILD", "1") + + with pytest.warns(UserWarning, match="Transforms have been disabled, as a Sphinx"): + + @qml.transforms.core.transform + def custom_transform( # pylint:disable=unused-variable + tape: qml.tape.QuantumTape, index: int + ) -> (Sequence[qml.tape.QuantumTape], Callable): + """A valid transform.""" + tape = tape.copy() + tape._ops.pop(index) # pylint:disable=protected-access + return [tape], lambda x: x diff --git a/tests/transforms/test_specs.py b/tests/transforms/test_specs.py index 0904ddb99c3..be31aaf1365 100644 --- a/tests/transforms/test_specs.py +++ b/tests/transforms/test_specs.py @@ -54,9 +54,7 @@ def circ(): if diff_method == "parameter-shift": assert info["num_gradient_executions"] == 0 - assert ( - info["gradient_fn"] == "pennylane.transforms.core.transform_dispatcher.param_shift" - ) + assert info["gradient_fn"] == "pennylane.gradients.parameter_shift.param_shift" @pytest.mark.parametrize( "diff_method, len_info", [("backprop", 11), ("parameter-shift", 12), ("adjoint", 11)] @@ -187,8 +185,8 @@ def circuit(): with pytest.warns(UserWarning, match="gradient of a tape with no trainable parameters"): info = qml.specs(circuit)() - assert info["diff_method"] == "pennylane.transforms.core.transform_dispatcher.param_shift" - assert info["gradient_fn"] == "pennylane.transforms.core.transform_dispatcher.param_shift" + assert info["diff_method"] == "pennylane.gradients.parameter_shift.param_shift" + assert info["gradient_fn"] == "pennylane.gradients.parameter_shift.param_shift" def test_custom_gradient_transform(self): """Test that a custom gradient transform is properly labelled""" @@ -203,8 +201,8 @@ def circuit(): return qml.probs(wires=0) info = qml.specs(circuit)() - assert info["diff_method"] == "pennylane.transforms.core.transform_dispatcher.my_transform" - assert info["gradient_fn"] == "pennylane.transforms.core.transform_dispatcher.my_transform" + assert info["diff_method"] == "test_specs.my_transform" + assert info["gradient_fn"] == "test_specs.my_transform" @pytest.mark.parametrize( "device,num_wires",