diff --git a/doc/development/plugins.rst b/doc/development/plugins.rst index 398a747633c..ac9574d466e 100644 --- a/doc/development/plugins.rst +++ b/doc/development/plugins.rst @@ -60,20 +60,21 @@ For example: circuit() -This execute method works in tandem with the optional :meth:`Device.preprocess `, described below in more detail. -Preprocessing turns generic circuits into ones supported by the device, or raises an error if the circuit is invalid. Execution produces numerical -results from those supported circuits. +This execute method works in tandem with the optional :meth:`Device.preprocess_transforms ` +and :meth:`Device.setup_execution_config`, described below in more detail. Preprocessing transforms +turns generic circuits into ones supported by the device, or raises an error if the circuit is invalid. +Execution produces numerical results from those supported circuits. In a more minimal example, for any initial batch of quantum tapes and a config object, we expect to be able to do: .. code-block:: python - transform_program, execution_config = dev.preprocess(initial_config) + execution_config = dev.setup_execution_config(initial_config) + transform_program = dev.preprocess_transforms(execution_config) circuit_batch, postprocessing = transform_program(initial_circuit_batch) results = dev.execute(circuit_batch, execution_config) final_results = postprocessing(results) - Shots ----- @@ -114,30 +115,34 @@ then finite shots exist. If ``shots`` is falsy, then an analytic execution shoul Preprocessing ------------- -The :meth:`~.devices.Device.preprocess` method has two main responsibilities: +There are two components of preprocessing circuits for device execution: 1) Create a :class:`~.TransformProgram` capable of turning an arbitrary batch of :class:`~.QuantumScript`\ s into a new batch of tapes supported by the ``execute`` method. 2) Setup the :class:`~.ExecutionConfig` dataclass by filling in device options and making decisions about differentiation. -These two tasks can be extracted into private methods or helper functions if that improves source -code organization. Once the transform program has been applied to a batch of circuits, the result +These two tasks are performed by :meth:`~.devices.Device.setup_execution_config` and :meth:`~.devices.Device.preprocess_transforms` +respectively. Once the transform program has been applied to a batch of circuits, the result circuit batch produced by the program should be run via ``Device.execute`` without error: .. code-block:: python - transform_program, execution_config = dev.preprocess(initial_config) + execution_config = dev.setup_execution_config(initial_config) + transform_program = dev.preprocess_transforms(execution_config) batch, fn = transform_program(initial_batch) fn(dev.execute(batch, execution_config)) -PennyLane can potentially provide a default implementation of the preprocessing program which should -be sufficient for most plugin devices. This requires that a TOML-formatted configuration file is -defined for your device. The details of this configuration file is described :ref:`the next section `. The -default preprocessing program will be constructed based on what is declared in this file if provided. +This section will focus on :meth:`~.devices.Device.preprocess_transforms`, see the section on the :ref:`**Execution Config** ` +below for more information on :meth:`~.devices.Device.setup_execution_config`. + +PennyLane can potentially provide a default implementation of a transform program through :meth:`~.devices.Device.preprocess_transforms`, +which should be sufficient for most plugin devices. This requires that a TOML-formatted configuration +file is defined for your device. The details of this configuration file is described :ref:`the next section `. +The default preprocessing program will be constructed based on what is declared in this file if provided. -Alternatively, you could override the :meth:`~.devices.Device.preprocess` method with a completely -customized implementation. +You could override the :meth:`~.devices.Device.preprocess_transforms` method with a completely +customized implementation, or extend the default behaviour by adding new transforms. -The :meth:`~.devices.Device.preprocess` method should start with creating a transform program: +The :meth:`~.devices.Device.preprocess_transforms` method should start with creating a transform program: .. code-block:: python @@ -180,9 +185,9 @@ classical component of any decompositions or compilation. For example, Allows the user to see that both ``IsingXX`` and ``CH`` are decomposed by the device, and that the diagonalizing gates for ``qml.expval(qml.X(0))`` are applied. -Even with these benefits, devices can still opt to -place some transforms inside the ``execute`` method. For example, ``default.qubit`` maps wires to simulation indices -inside ``execute`` instead of in ``preprocess``. +Even with these benefits, devices can still opt to place some transforms inside the ``execute`` +method. For example, ``default.qubit`` maps wires to simulation indices inside ``execute`` instead +of in ``preprocess_transforms``. The :meth:`~.devices.Device.execute` method can assume that device preprocessing has been performed on the input tapes, and has no obligation to re-validate the input or provide sensible error messages. In the below example, @@ -214,8 +219,6 @@ or can include in-built transforms such as: * :func:`pennylane.devices.preprocess.validate_adjoint_trainable_params` * :func:`pennylane.devices.preprocess.no_sampling` -See the section on the :ref:`**Execution Config** ` below for more information on step 2. - .. _device_capabilities: Device Capabilities @@ -236,8 +239,8 @@ supported by your device, i.e., what the :meth:`~pennylane.devices.Device.execut config_filepath = path.join(path.dirname(__file__), "relative/path/to/config.toml") This configuration file will be loaded into another class variable :attr:`~pennylane.devices.Device.capabilities` -that is used in the default implementation of :meth:`~pennylane.devices.Device.preprocess` if you -choose not to override it yourself as described above. Note that this file must be declared as +that is used in the default implementation of :meth:`~pennylane.devices.Device.preprocess_transforms` +if you choose not to override it yourself as described above. Note that this file must be declared as package data as instructed at the end of :ref:`this section `. Below is an example configuration file defining all accepted fields, with inline descriptions of @@ -377,12 +380,12 @@ Devices can now either: 3) Strictly require specific wire labels Option 2 allows workflows to change the number and labeling of wires over time, but sometimes users want -to enforce a wire convention and labels. If a user does provide wires, :meth:`~.devices.Device.preprocess` should +to enforce a wire convention and labels. If a user does provide wires, :meth:`~.devices.Device.preprocess_transforms` should validate that submitted circuits only have wires in the requested range. >>> dev = qml.device('default.qubit', wires=1) >>> circuit = qml.tape.QuantumScript([qml.CNOT((0,1))], [qml.state()]) ->>> dev.preprocess()[0]((circuit,)) +>>> dev.preprocess_transforms()((circuit,)) WireError: Cannot run circuit(s) of default.qubit as they contain wires not found on the device. PennyLane wires can be any hashable object, where wire labels are distinguished by their equality and hash. @@ -429,22 +432,24 @@ The execution config stores two kinds of information: **Device options:** -Device options are any device specific options used to configure the behavior of an execution. For example, ``default.qubit`` -has ``max_workers``, ``rng``, and ``prng_key``. ``default.tensor`` has ``contract``, ``cutoff``, ``dtype``, ``method``, and ``max_bond_dim``. -These options are often set with default values on initialization. These values should be placed into the ``ExecutionConfig.device_options`` -dictionary on preprocessing. +Device options are any device specific options used to configure the behavior of an execution. For +example, ``default.qubit`` has ``max_workers``, ``rng``, and ``prng_key``. ``default.tensor`` has +``contract``, ``cutoff``, ``dtype``, ``method``, and ``max_bond_dim``. These options are often set +with default values on initialization. These values should be placed into the ``ExecutionConfig.device_options`` +dictionary in :meth:`~.devices.Device.setup_execution_config`. Note that we do provide a default +implementation of this method, but you will most likely need to override it yourself. >>> dev = qml.device('default.tensor', wires=2, max_bond_dim=4, contract="nonlocal", dtype=np.complex64) ->>> dev.preprocess()[1].device_options +>>> dev.setup_execution_config().device_options {'contract': 'nonlocal', 'cutoff': 1.1920929e-07, 'dtype': numpy.complex64, 'method': 'mps', 'max_bond_dim': 4} -Even if the property is stored as an attribute on the device, execution should pull the -value of these properties from the config instead of from the device instance. -While not yet integrated at the top user level, we aim to allow dynamic configuration of the device. +Even if the property is stored as an attribute on the device, execution should pull the value of +these properties from the config instead of from the device instance. While not yet integrated at +the top user level, we aim to allow dynamic configuration of the device. >>> dev = qml.device('default.qubit') >>> config = qml.devices.ExecutionConfig(device_options={"rng": 42}) @@ -454,13 +459,12 @@ array([1, 0, 1, 1, 0, 1, 1, 1, 0, 0]) >>> dev.execute(tape, config) array([1, 0, 1, 1, 0, 1, 1, 1, 0, 0]) -By pulling options from this dictionary instead of from device properties, -we unlock two key pieces of functionality. +By pulling options from this dictionary instead of from device properties, we unlock two key +pieces of functionality: 1) Track and specify the exact configuration of the execution by only inspecting the ``ExecutionConfig`` object 2) Dynamically configure the device over the course of a workflow. - **Workflow Configuration:** Note that these properties are only applicable to devices that provided derivatives or VJPs. If your device @@ -468,13 +472,13 @@ does not provide derivatives, you can safely ignore these properties. The workflow options are ``use_device_gradient``, ``use_device_jacobian_product``, and ``grad_on_execution``. ``use_device_gradient=True`` indicates that workflow should request derivatives from the device. -``grad_on_execution=True`` indicates a preference -to use ``execute_and_compute_derivatives`` instead of ``execute`` followed by ``compute_derivatives``. Finally, -``use_device_jacobian_product`` indicates a request to call ``compute_vjp`` instead of ``compute_derivatives``. Note that -if ``use_device_jacobian_product`` is ``True``, this takes precedence over calculating the full jacobian. +``grad_on_execution=True`` indicates a preference to use ``execute_and_compute_derivatives`` instead +of ``execute`` followed by ``compute_derivatives``. Finally, ``use_device_jacobian_product`` indicates +a request to call ``compute_vjp`` instead of ``compute_derivatives``. Note that if ``use_device_jacobian_product`` +is ``True``, this takes precedence over calculating the full jacobian. >>> config = qml.devices.ExecutionConfig(gradient_method="adjoint") ->>> processed_config = qml.device('default.qubit').preprocess(config)[1] +>>> processed_config = qml.device('default.qubit').setup_execution_config(config) >>> processed_config.use_device_jacobian_product True >>> processed_config.use_device_gradient diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 6f369935041..a27bbe29056 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -4,13 +4,19 @@

New features since last release

+* Two new methods: `setup_execution_config` and `preprocess_transforms` are added to the `Device` + class. Device developers are encouraged to override these two methods separately instead of the + `preprocess` method. For now, to avoid ambiguity, a device is allowed to override either these + two methods or `preprocess`, but not both. In the long term, we will slowly phase out the use of + `preprocess` in favour of these two methods for better separation of concerns. + [(#6617)](https://github.com/PennyLaneAI/pennylane/pull/6617) + * Developers of plugin devices now have the option of providing a TOML-formatted configuration file to declare the capabilities of the device. See [Device Capabilities](https://docs.pennylane.ai/en/latest/development/plugins.html#device-capabilities) for details. - [(#6407)](https://github.com/PennyLaneAI/pennylane/pull/6407) - [(#6433)](https://github.com/PennyLaneAI/pennylane/pull/6433) * An internal module `pennylane.devices.capabilities` is added that defines a new `DeviceCapabilites` data class, as well as functions that load and parse the TOML-formatted configuration files. + [(#6407)](https://github.com/PennyLaneAI/pennylane/pull/6407) ```pycon >>> from pennylane.devices.capabilities import DeviceCapabilities @@ -22,6 +28,7 @@ * Devices that extends `qml.devices.Device` now has an optional class attribute `capabilities` that is an instance of the `DeviceCapabilities` data class, constructed from the configuration file if it exists. Otherwise, it is set to `None`. + [(#6433)](https://github.com/PennyLaneAI/pennylane/pull/6433) ```python from pennylane.devices import Device @@ -83,6 +90,9 @@ visualizations, allowing global and per-wire customization with options like `color`, `linestyle`, and `linewidth`. [(#6486)](https://github.com/PennyLaneAI/pennylane/pull/6486) +* `QNode` and `qml.execute` now forbid certain keyword arguments from being passed positionally. + [(#6610)](https://github.com/PennyLaneAI/pennylane/pull/6610) + * Shortened the string representation for the `qml.S`, `qml.T`, and `qml.SX` operators. [(#6542)](https://github.com/PennyLaneAI/pennylane/pull/6542) diff --git a/pennylane/devices/device_api.py b/pennylane/devices/device_api.py index defbdfea9c6..0c081d50572 100644 --- a/pennylane/devices/device_api.py +++ b/pennylane/devices/device_api.py @@ -133,6 +133,14 @@ class Device(abc.ABC): def __init_subclass__(cls, **kwargs): if cls.config_filepath is not None: cls.capabilities = DeviceCapabilities.from_toml_file(cls.config_filepath) + if cls.preprocess is not Device.preprocess and ( + cls.setup_execution_config is not Device.setup_execution_config + or cls.preprocess_transforms is not Device.preprocess_transforms + ): + raise ValueError( + "A device should implement either `preprocess` or `setup_execution_config` " + "and `preprocess_transforms`, but not both." + ) super().__init_subclass__(**kwargs) @property @@ -229,7 +237,7 @@ def wires(self) -> Wires: def preprocess( self, - execution_config: ExecutionConfig = DefaultExecutionConfig, + execution_config: Optional[ExecutionConfig] = None, ) -> tuple[TransformProgram, ExecutionConfig]: """Device preprocessing function. @@ -323,12 +331,139 @@ def execute_fn(tapes): Only then is the classical postprocessing called on the result object. """ - if self.supports_derivatives(execution_config) and execution_config.gradient_method in { - "best", - None, - }: - return TransformProgram(), replace(execution_config, gradient_method="device") - return TransformProgram(), execution_config + execution_config = self.setup_execution_config(execution_config) + transform_program = self.preprocess_transforms(execution_config) + return transform_program, execution_config + + def setup_execution_config(self, config: Optional[ExecutionConfig] = None) -> ExecutionConfig: + """Sets up an ``ExecutionConfig`` that configures the execution behaviour. + + The execution config stores information on how the device should perform the execution, + as well as how PennyLane should interact with the device. See :class:`ExecutionConfig` + for all available options and what they mean. + + An ``ExecutionConfig`` is constructed from arguments passed to the ``QNode``, and this + method allows the device to update the config object based on device-specific requirements + or preferences. See :ref:`execution_config` for more details. + + Args: + config (ExecutionConfig): The initial ExecutionConfig object that describes the + parameters needed to configure the execution behaviour. + + Returns: + ExecutionConfig: The updated ExecutionConfig object + + """ + + if self.__class__.preprocess is not Device.preprocess: + if config: + return self.preprocess(config)[1] + return self.preprocess()[1] + + if config is None: + config = ExecutionConfig() + + if self.supports_derivatives(config) and config.gradient_method in ("best", None): + return replace(config, gradient_method="device") + + return config + + def preprocess_transforms( + self, execution_config: Optional[ExecutionConfig] = None + ) -> TransformProgram: + """Returns the transform program to preprocess a circuit for execution. + + Args: + execution_config (ExecutionConfig): The execution configuration object + + Returns: + TransformProgram: A transform program that is called before execution + + The transform program is composed of a list of individual transforms, which may include: + + * Decomposition of operations and measurements to what is supported by the device. + * Splitting a circuit with measurements of non-commuting observables or Hamiltonians into multiple executions. + * Splitting a circuit with batched parameters into multiple executions. + * Validation of wires, measurements, and observables. + * Gradient specific preprocessing, such as making sure trainable operators have generators. + + **Example** + + All transforms that are part of the preprocessing transform program need to respect the + transform contract defined in :func:`pennylane.transform`. + + .. code-block:: python + + from pennylane.tape import QuantumScriptBatch + from pennylane.typing import PostprocessingFn + + @qml.transform + def my_preprocessing_transform(tape: qml.tape.QuantumScript) -> tuple[QuantumScriptBatch, PostprocessingFn]: + # e.g. valid the measurements, expand the tape for the hardware execution, ... + + def blank_processing_fn(results): + return results[0] + + return [tape], processing_fn + + A transform program can hold an arbitrary number of individual transforms: + + .. code-block:: python + + def preprocess(self, config): + program = TransformProgram() + program.add_transform(my_preprocessing_transform) + return program + + .. seealso:: :func:`~.pennylane.transform.core.transform` and :class:`~.pennylane.transform.core.TransformProgram` + + .. details:: + :title: Post processing function and derivatives + + Derivatives and Jacobian products will be bound to the machine learning library before + the postprocessing function is called on the results. Therefore, the machine learning + library will be responsible for combining and post-processing derivatives returned from + the device. + + .. code-block:: python + + from pennylane.interfaces.jax import execute as jax_boundary + + def f(x): + circuit = qml.tape.QuantumScript([qml.Rot(*x, wires=0)], [qml.expval(qml.Z(0))]) + config = ExecutionConfig(gradient_method="adjoint") + config = dev.setup_execution_config(config) + program = dev.preprocess_transforms(config) + circuit_batch, postprocessing = program((circuit, )) + + def execute_fn(tapes): + return dev.execute_and_compute_derivatives(tapes, config) + + results = jax_boundary(circuit_batch, dev, execute_fn, None, {}) + return postprocessing(results) + + x = jax.numpy.array([1.0, 2.0, 3.0]) + jax.grad(f)(x) + + In the above code, the quantum derivatives are registered with jax in the ``jax_boundary`` + function. Only then is the classical postprocessing called on the result object. + + """ + + # TODO: this is obviously not pretty but it's a temporary solution to ensure backwards + # compatibility. Basically there are three scenarios: + # 1. The device does not override anything, then this method returns the default + # transform program, and preprocess calls this method, all good. + # 2. The device overrides preprocess, but not this method, then this method will + # return what is returned from the overridden preprocess method. + # 3. The device overrides this method and not preprocess (recommended and what we + # are ultimately aiming for), then preprocess calls the overridden method. + if self.__class__.preprocess is not Device.preprocess: + if execution_config: + return self.preprocess(execution_config)[0] + return self.preprocess()[0] + + return TransformProgram() @abc.abstractmethod @overload diff --git a/pennylane/devices/tests/test_gates.py b/pennylane/devices/tests/test_gates.py index 34e85d2ec8c..cd2aebd7830 100644 --- a/pennylane/devices/tests/test_gates.py +++ b/pennylane/devices/tests/test_gates.py @@ -364,7 +364,7 @@ def test_supported_gates_can_be_implemented(self, device_kwargs, operation): pytest.skip("operation not supported.") else: if ops[operation].name == "QubitDensityMatrix": - prog = dev.preprocess()[0] + prog = dev.preprocess_transforms() tape = qml.tape.QuantumScript([ops[operation]]) try: prog((tape,)) diff --git a/pennylane/devices/tests/test_measurements.py b/pennylane/devices/tests/test_measurements.py index 1d378633e94..263e4f251b3 100644 --- a/pennylane/devices/tests/test_measurements.py +++ b/pennylane/devices/tests/test_measurements.py @@ -1847,7 +1847,7 @@ def process(self, tape, device): if isinstance(dev, qml.devices.Device): tape = qml.tape.QuantumScript([], [MyMeasurement()]) try: - dev.preprocess()[0]((tape,)) + dev.preprocess_transforms()((tape,)) except qml.DeviceError: pytest.xfail("Device does not support custom measurement transforms.") diff --git a/pennylane/devices/tests/test_templates.py b/pennylane/devices/tests/test_templates.py index 12ca3edccf8..183ce593f06 100644 --- a/pennylane/devices/tests/test_templates.py +++ b/pennylane/devices/tests/test_templates.py @@ -37,7 +37,7 @@ def check_op_supported(op, dev): if op.name not in dev.operations: pytest.skip("operation not supported.") else: - prog, _ = dev.preprocess() + prog = dev.preprocess_transforms() tape = qml.tape.QuantumScript([op]) try: prog((tape,)) diff --git a/pennylane/optimize/qnspsa.py b/pennylane/optimize/qnspsa.py index d0f3502f3c2..660f7e9ff75 100644 --- a/pennylane/optimize/qnspsa.py +++ b/pennylane/optimize/qnspsa.py @@ -443,7 +443,7 @@ def _apply_blocking(self, cost, args, kwargs, params_next): tape = qml.workflow.construct_tape(cost)(*params_next, **kwargs) tape_loss_next = tape.copy(copy_operations=True) - program, _ = cost.device.preprocess() + program = cost.device.preprocess_transforms() loss_curr, loss_next = qml.execute( [tape_loss_curr, tape_loss_next], cost.device, None, transform_program=program diff --git a/pennylane/optimize/riemannian_gradient.py b/pennylane/optimize/riemannian_gradient.py index e7cc7c720a0..665c8edb922 100644 --- a/pennylane/optimize/riemannian_gradient.py +++ b/pennylane/optimize/riemannian_gradient.py @@ -404,7 +404,7 @@ def get_omegas(self): circuits, self.circuit.device, diff_method=None ) # pragma: no cover - program, _ = self.circuit.device.preprocess() + program = self.circuit.device.preprocess_transforms() circuits_plus = np.array(circuits[: len(circuits) // 2]).reshape( len(self.coeffs), len(self.lie_algebra_basis_names) diff --git a/pennylane/transforms/core/transform_program.py b/pennylane/transforms/core/transform_program.py index 928ceb006eb..2c8a86ddcb3 100644 --- a/pennylane/transforms/core/transform_program.py +++ b/pennylane/transforms/core/transform_program.py @@ -16,7 +16,7 @@ """ from collections.abc import Sequence from functools import partial -from typing import Optional, Union +from typing import Optional, overload import pennylane as qml from pennylane.tape import QuantumScriptBatch @@ -25,6 +25,70 @@ from .transform_dispatcher import TransformContainer, TransformDispatcher, TransformError +def _numpy_jac(*_, **__) -> qml.typing.TensorLike: + raise qml.QuantumFunctionError("No trainable parameters.") + + +def _autograd_jac(classical_function, argnums, *args, **kwargs) -> qml.typing.TensorLike: + if not qml.math.get_trainable_indices(args) and argnums is None: + raise qml.QuantumFunctionError("No trainable parameters.") + return qml.jacobian(classical_function, argnum=argnums)(*args, **kwargs) + + +# pylint: disable=import-outside-toplevel, unused-argument +def _tf_jac(classical_function, argnums, *args, **kwargs) -> qml.typing.TensorLike: + if not qml.math.get_trainable_indices(args): + raise qml.QuantumFunctionError("No trainable parameters.") + import tensorflow as tf + + with tf.GradientTape() as tape: + gate_params = classical_function(*args, **kwargs) + return tape.jacobian(gate_params, args) + + +# pylint: disable=import-outside-toplevel, unused-argument +def _torch_jac(classical_function, argnums, *args, **kwargs) -> qml.typing.TensorLike: + if not qml.math.get_trainable_indices(args): + raise qml.QuantumFunctionError("No trainable parameters.") + from torch.autograd.functional import jacobian + + return jacobian(partial(classical_function, **kwargs), args) + + +# pylint: disable=import-outside-toplevel +def _jax_jac(classical_function, argnums, *args, **kwargs) -> qml.typing.TensorLike: + import jax + + if argnums is None: + if not isinstance(args[0], jax.numpy.ndarray): + raise qml.QuantumFunctionError("No trainable parameters.") + argnums = 0 + return jax.jacobian(classical_function, argnums=argnums)(*args, **kwargs) + + +_jac_map = { + None: _numpy_jac, + "numpy": _numpy_jac, + "autograd": _autograd_jac, + "tf": _tf_jac, + "torch": _torch_jac, + "jax": _jax_jac, + "jax-jit": _jax_jac, +} + + +# pylint: disable=unused-argument +def _classical_preprocessing(qnode, program, *args, argnums=None, **kwargs): + """Returns the trainable gate parameters for a given QNode input.""" + tape = qml.workflow.construct_tape(qnode, level=0)(*args, **kwargs) + tapes, _ = program((tape,)) + res = tuple(qml.math.stack(tape.get_parameters(trainable_only=True)) for tape in tapes) + # autograd and tf cant handle pytrees, so need to squeeze batches + if len(tapes) == 1: + return res[0] + return res + + def _jax_argnums_to_tape_trainable(qnode, argnums, program, args, kwargs): """This function gets the tape parameters from the QNode construction given some argnums (only for Jax). The tape parameters are transformed to JVPTracer if they are from argnums. This function imitates the behaviour @@ -188,18 +252,22 @@ def __iter__(self): """list[TransformContainer]: Return an iterator to the underlying transform program.""" return self._transform_program.__iter__() - def __len__(self): + def __len__(self) -> int: """int: Return the number transforms in the program.""" return len(self._transform_program) - def __getitem__(self, idx) -> Union["TransformProgram", "TransformContainer"]: + @overload + def __getitem__(self, idx: int) -> "TransformContainer": ... + @overload + def __getitem__(self, idx: slice) -> "TransformProgram": ... + def __getitem__(self, idx): """(TransformContainer, List[TransformContainer]): Return the indexed transform container from underlying transform program""" if isinstance(idx, slice): return TransformProgram(self._transform_program[idx]) return self._transform_program[idx] - def __bool__(self): + def __bool__(self) -> bool: return bool(self._transform_program) def __add__(self, other): @@ -223,7 +291,7 @@ def __eq__(self, other) -> bool: return self._transform_program == other._transform_program - def __contains__(self, obj): + def __contains__(self, obj) -> bool: if isinstance(obj, TransformContainer): return obj in self._transform_program if isinstance(obj, TransformDispatcher): @@ -380,7 +448,10 @@ def set_classical_component(self, qnode, args, kwargs): if hybrid: argnums = self[-1].kwargs.pop("argnums", None) # pylint: disable=no-member - self._set_all_classical_jacobians(qnode, args, kwargs, argnums) + self._classical_jacobians = [ + self._get_classical_jacobian(index, qnode, args, kwargs, argnums) + for index, _ in enumerate(self) + ] self._set_all_argnums(qnode, args, kwargs, argnums) def prune_dynamic_transform(self, type_to_keep=1): @@ -410,95 +481,26 @@ def prune_dynamic_transform(self, type_to_keep=1): i -= 1 return found - def _set_all_classical_jacobians( - self, qnode, args, kwargs, argnums - ): # pylint: disable=too-many-statements - """It can be called inside the QNode to get all the classical Jacobians for a gradient transform.""" - - def classical_preprocessing(program, *args, **kwargs): - """Returns the trainable gate parameters for a given QNode input.""" - kwargs.pop("shots", None) - kwargs.pop("argnums", None) - tape = qml.workflow.construct_tape(qnode, level=0)(*args, **kwargs) - tapes, _ = program((tape,)) - res = tuple(qml.math.stack(tape.get_parameters(trainable_only=True)) for tape in tapes) - if len(tapes) == 1: - return res[0] - return res - - def jacobian(classical_function, program, argnums, *args, **kwargs): - indices = qml.math.get_trainable_indices(args) - - if qnode.interface in ["jax", "jax-jit"]: - import jax # pylint: disable=import-outside-toplevel - - if isinstance(args[0], jax.numpy.ndarray): - argnums = 0 if argnums is None else argnums - - if not indices and argnums is None: - raise qml.QuantumFunctionError("No trainable parameters.") - - classical_function = partial(classical_function, program) - jac = None - if qnode.interface == "autograd": - jac = qml.jacobian(classical_function, argnum=argnums)(*args, **kwargs) - - if qnode.interface == "tf": - import tensorflow as tf # pylint: disable=import-outside-toplevel - - def _jacobian(*args, **kwargs): - with tf.GradientTape() as tape: - gate_params = classical_function(*args, **kwargs) - - jac = tape.jacobian(gate_params, args) - return jac - - jac = _jacobian(*args, **kwargs) - - if qnode.interface == "torch": - import torch # pylint: disable=import-outside-toplevel - - def _jacobian(*args, **kwargs): # pylint: disable=unused-argument - jac = torch.autograd.functional.jacobian(classical_function, args) - return jac - - jac = _jacobian(*args, **kwargs) - - if qnode.interface in ["jax", "jax-jit"]: - import jax # pylint: disable=import-outside-toplevel - - argnums = 0 if argnums is None else argnums - - def _jacobian(*args, **kwargs): - return jax.jacobian(classical_function, argnums=argnums)(*args, **kwargs) - - jac = _jacobian(*args, **kwargs) + # pylint: disable=too-many-arguments, too-many-positional-arguments + def _get_classical_jacobian(self, index: int, qnode, args, kwargs, argnums): + if not self[index].classical_cotransform: + return None + if qnode.interface == "jax" and "argnum" in self[index].kwargs: + raise qml.QuantumFunctionError( + "argnum does not work with the Jax interface. You should use argnums instead." + ) - return jac + f = partial(_classical_preprocessing, qnode, self[:index]) + classical_jacobian = _jac_map[qnode.interface](f, argnums, *args, **kwargs) - classical_jacobians = [] - for index, transform in enumerate(self): - if transform.classical_cotransform: - argnum = transform._kwargs.get("argnum", None) # pylint: disable=protected-access - if qnode.interface == "jax" and argnum: - raise qml.QuantumFunctionError( - "argnum does not work with the Jax interface. You should use argnums instead." - ) - sub_program = TransformProgram(self[0:index]) - classical_jacobian = jacobian( - classical_preprocessing, sub_program, argnums, *args, **kwargs - ) - tape = qml.workflow.construct_tape(qnode, level=0)(*args, **kwargs) - tapes, _ = sub_program((tape,)) - multi_tapes = len(tapes) > 1 - if not multi_tapes: - classical_jacobian = [classical_jacobian] - classical_jacobians.append(classical_jacobian) - else: - classical_jacobians.append(None) - self._classical_jacobians = classical_jacobians - # Reset the initial tape - qnode.construct(args, kwargs) + # autograd and tf cant handle pytrees, so need to unsqueeze the squeezing + # done in _classical_preprocessing + tape = qml.workflow.construct_tape(qnode, level=0)(*args, **kwargs) + tapes, _ = self[:index]((tape,)) # pylint: disable=not-callable + multi_tapes = len(tapes) > 1 + if not multi_tapes: + classical_jacobian = [classical_jacobian] + return classical_jacobian def _set_all_argnums(self, qnode, args, kwargs, argnums): """It can be used inside the QNode to set all argnums (tape level) using argnums from the argnums at the QNode @@ -510,17 +512,13 @@ def _set_all_argnums(self, qnode, args, kwargs, argnums): argnums = [0] if qnode.interface in ["jax", "jax-jit"] and argnums is None else argnums # pylint: disable=protected-access if (transform._use_argnum or transform.classical_cotransform) and argnums: - params = _jax_argnums_to_tape_trainable( - qnode, argnums, TransformProgram(self[0:index]), args, kwargs - ) + params = _jax_argnums_to_tape_trainable(qnode, argnums, self[:index], args, kwargs) argnums_list.append([qml.math.get_trainable_indices(param) for param in params]) else: argnums_list.append(None) self._argnums = argnums_list - qnode.construct(args, kwargs) - def __call__( self, tapes: QuantumScriptBatch ) -> tuple[QuantumScriptBatch, BatchPostprocessingFn]: diff --git a/pennylane/workflow/_resolve_diff_method.py b/pennylane/workflow/_resolve_diff_method.py index 9ff31f61e05..028bf0f6a57 100644 --- a/pennylane/workflow/_resolve_diff_method.py +++ b/pennylane/workflow/_resolve_diff_method.py @@ -59,7 +59,7 @@ def _resolve_diff_method( return initial_config if device.supports_derivatives(initial_config, circuit=tape): - new_config = device.preprocess(initial_config)[1] + new_config = device.setup_execution_config(initial_config) return new_config if diff_method in {"backprop", "adjoint", "device"}: diff --git a/pennylane/workflow/construct_batch.py b/pennylane/workflow/construct_batch.py index 99b674e5080..adb3ebde984 100644 --- a/pennylane/workflow/construct_batch.py +++ b/pennylane/workflow/construct_batch.py @@ -68,7 +68,7 @@ def _get_full_transform_program( ) config = _make_execution_config(qnode, gradient_fn) - return program + qnode.device.preprocess(config)[0] + return program + qnode.device.preprocess_transforms(config) def get_transform_program( diff --git a/pennylane/workflow/execution.py b/pennylane/workflow/execution.py index 0cb8074b43f..d4fc3ecab52 100644 --- a/pennylane/workflow/execution.py +++ b/pennylane/workflow/execution.py @@ -249,6 +249,7 @@ def execute( device: Union["qml.devices.LegacyDevice", "qml.devices.Device"], diff_method: Optional[Union[Callable, str, qml.transforms.core.TransformDispatcher]] = None, interface: Optional[str] = "auto", + *, transform_program=None, inner_transform=None, config=None, @@ -411,9 +412,16 @@ def cost_fn(params, x): gradient_kwargs = gradient_kwargs or {} mcm_config = mcm_config or {} - config = config or _get_execution_config( - diff_method, grad_on_execution, interface, device, device_vjp, mcm_config, gradient_kwargs - ) + if not config: + config = qml.devices.ExecutionConfig( + interface=interface, + gradient_method=diff_method, + grad_on_execution=None if grad_on_execution == "best" else grad_on_execution, + use_device_jacobian_product=device_vjp, + mcm_config=mcm_config, + gradient_keyword_arguments=gradient_kwargs, + ) + config = device.setup_execution_config(config) is_gradient_transform = isinstance(diff_method, qml.transforms.core.TransformDispatcher) transform_program, inner_transform = _make_transform_programs( @@ -591,29 +599,13 @@ def _make_transform_programs( # inner execute (inside the ml boundary). if is_gradient_transform: if inner_transform is None: - inner_transform = device.preprocess(config)[0] + inner_transform = device.preprocess_transforms(config) if transform_program is None: transform_program = qml.transforms.core.TransformProgram() else: if inner_transform is None: inner_transform = qml.transforms.core.TransformProgram() if transform_program is None: - transform_program = device.preprocess(config)[0] + transform_program = device.preprocess_transforms(config) return transform_program, inner_transform - - -def _get_execution_config( - diff_method, grad_on_execution, interface, device, device_vjp, mcm_config, gradient_kwargs -): - """Helper function to get the execution config.""" - config = qml.devices.ExecutionConfig( - interface=interface, - gradient_method=diff_method, - grad_on_execution=None if grad_on_execution == "best" else grad_on_execution, - use_device_jacobian_product=device_vjp, - mcm_config=mcm_config, - gradient_keyword_arguments=gradient_kwargs, - ) - - return device.preprocess(config)[1] diff --git a/pennylane/workflow/get_best_diff_method.py b/pennylane/workflow/get_best_diff_method.py index b7705ef4b85..c0a729a78a2 100644 --- a/pennylane/workflow/get_best_diff_method.py +++ b/pennylane/workflow/get_best_diff_method.py @@ -64,7 +64,7 @@ def wrapper(*args, **kwargs): config = _make_execution_config(None, "best") if device.supports_derivatives(config, circuit=tape): - new_config = device.preprocess(config)[1] + new_config = device.setup_execution_config(config) transform = new_config.gradient_method return handle_return(transform) diff --git a/pennylane/workflow/interfaces/jax_jit.py b/pennylane/workflow/interfaces/jax_jit.py index 9021113edf3..3cd5779a5de 100644 --- a/pennylane/workflow/interfaces/jax_jit.py +++ b/pennylane/workflow/interfaces/jax_jit.py @@ -169,7 +169,9 @@ def pure_callback_wrapper(p): # first order way of determining native parameter broadcasting support # will be inaccurate when inclusion of broadcast_expand depends on ExecutionConfig values (like adjoint) - device_supports_vectorization = qml.transforms.broadcast_expand not in device.preprocess()[0] + device_supports_vectorization = ( + qml.transforms.broadcast_expand not in device.preprocess_transforms() + ) out = jax.pure_callback( pure_callback_wrapper, shape_dtype_structs, params, vectorized=device_supports_vectorization ) diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py index b567ed3b6d1..34abc47c57f 100644 --- a/pennylane/workflow/qnode.py +++ b/pennylane/workflow/qnode.py @@ -607,6 +607,7 @@ def __init__( device: SupportedDeviceAPIs, interface: SupportedInterfaceUserInput = "auto", diff_method: Union[TransformDispatcher, SupportedDiffMethods] = "best", + *, grad_on_execution: Literal[True, False, "best"] = "best", cache: Union[Cache, Literal["auto", True, False]] = "auto", cachesize: int = 10000, @@ -777,7 +778,7 @@ def get_gradient_fn( config = _make_execution_config(None, diff_method) if device.supports_derivatives(config, circuit=tape): - new_config = device.preprocess(config)[1] + new_config = device.setup_execution_config(config) return new_config.gradient_method, {}, device if diff_method in {"backprop", "adjoint", "device"}: # device-only derivatives @@ -868,7 +869,7 @@ def get_best_method( config = _make_execution_config(None, "best") if device.supports_derivatives(config, circuit=tape): - new_config = device.preprocess(config)[1] + new_config = device.setup_execution_config(config) return new_config.gradient_method, {}, device if tape and any(isinstance(o, qml.operation.CV) for o in tape): diff --git a/tests/devices/default_qubit/test_default_qubit.py b/tests/devices/default_qubit/test_default_qubit.py index 2f9a53eca92..00f94cef5bb 100644 --- a/tests/devices/default_qubit/test_default_qubit.py +++ b/tests/devices/default_qubit/test_default_qubit.py @@ -932,7 +932,7 @@ def test_derivatives_single_circuit(self, max_workers): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="adjoint") - batch, _ = dev.preprocess(config)[0]((qs,)) + batch, _ = dev.preprocess_transforms(config)((qs,)) qs = batch[0] expected_grad = -qml.math.sin(x) actual_grad = dev.compute_derivatives(qs, self.ec) @@ -951,7 +951,7 @@ def test_derivatives_list_with_single_circuit(self, max_workers): x = np.array(np.pi / 7) qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="adjoint") - batch, _ = dev.preprocess(config)[0]((qs,)) + batch, _ = dev.preprocess_transforms(config)((qs,)) qs = batch[0] expected_grad = -qml.math.sin(x) actual_grad = dev.compute_derivatives([qs], self.ec) @@ -1008,7 +1008,7 @@ def test_jvps_single_circuit(self, max_workers): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="adjoint") - batch, _ = dev.preprocess(config)[0]((qs,)) + batch, _ = dev.preprocess_transforms(config)((qs,)) qs = batch[0] expected_grad = -qml.math.sin(x) * tangent[0] @@ -1031,7 +1031,7 @@ def test_jvps_list_with_single_circuit(self, max_workers): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="adjoint") - batch, _ = dev.preprocess(config)[0]((qs,)) + batch, _ = dev.preprocess_transforms(config)((qs,)) qs = batch[0] expected_grad = -qml.math.sin(x) * tangent[0] @@ -1107,7 +1107,7 @@ def test_vjps_single_circuit(self, max_workers): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="adjoint") - batch, _ = dev.preprocess(config)[0]((qs,)) + batch, _ = dev.preprocess_transforms(config)((qs,)) qs = batch[0] expected_grad = -qml.math.sin(x) * cotangent[0] @@ -1127,7 +1127,7 @@ def test_vjps_list_with_single_circuit(self, max_workers): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="adjoint") - batch, _ = dev.preprocess(config)[0]((qs,)) + batch, _ = dev.preprocess_transforms(config)((qs,)) qs = batch[0] expected_grad = -qml.math.sin(x) * cotangent[0] diff --git a/tests/devices/default_qubit/test_default_qubit_preprocessing.py b/tests/devices/default_qubit/test_default_qubit_preprocessing.py index fec4593bfee..40f45a9383e 100644 --- a/tests/devices/default_qubit/test_default_qubit_preprocessing.py +++ b/tests/devices/default_qubit/test_default_qubit_preprocessing.py @@ -71,7 +71,7 @@ def test_snapshot_multiprocessing_execute(): [qml.expval(qml.PauliX(0))], ) with pytest.raises(RuntimeError, match="ProcessPoolExecutor cannot execute a QuantumScript"): - program, _ = dev.preprocess() + program = dev.preprocess_transforms() program([tape]) @@ -87,7 +87,7 @@ def test_error_if_device_option_not_available(self): def test_choose_best_gradient_method(self): """Test that preprocessing chooses backprop as the best gradient method.""" config = qml.devices.ExecutionConfig(gradient_method="best") - _, config = qml.device("default.qubit").preprocess(config) + config = qml.device("default.qubit").setup_execution_config(config) assert config.gradient_method == "backprop" assert config.use_device_gradient assert not config.grad_on_execution @@ -97,7 +97,7 @@ def test_config_choices_for_adjoint(self): config = qml.devices.ExecutionConfig( gradient_method="adjoint", use_device_gradient=None, grad_on_execution=None ) - _, new_config = qml.device("default.qubit").preprocess(config) + new_config = qml.device("default.qubit").setup_execution_config(config) assert new_config.use_device_gradient assert new_config.grad_on_execution @@ -107,7 +107,7 @@ def test_chose_adjoint_as_best_if_max_workers_on_device(self): dev = qml.device("default.qubit", max_workers=2) config = qml.devices.ExecutionConfig(gradient_method="best") - _, config = dev.preprocess(config) + config = dev.setup_execution_config(config) assert config.gradient_method == "adjoint" assert config.use_device_gradient assert config.grad_on_execution @@ -120,7 +120,7 @@ def test_chose_adjoint_as_best_if_max_workers_on_config(self): config = qml.devices.ExecutionConfig( gradient_method="best", device_options={"max_workers": 2} ) - _, config = dev.preprocess(config) + config = dev.setup_execution_config(config) assert config.gradient_method == "adjoint" assert config.use_device_gradient assert config.grad_on_execution @@ -154,7 +154,7 @@ def test_chooses_best_gradient_method(self): gradient_method="best", use_device_gradient=None, grad_on_execution=None ) - _, new_config = dev.preprocess(config) + new_config = dev.setup_execution_config(config) assert new_config.gradient_method == "backprop" assert new_config.use_device_gradient @@ -168,7 +168,7 @@ def test_config_choices_for_adjoint(self): gradient_method="adjoint", use_device_gradient=None, grad_on_execution=None ) - _, new_config = dev.preprocess(config) + new_config = dev.setup_execution_config(config) assert new_config.use_device_gradient assert new_config.grad_on_execution @@ -179,7 +179,7 @@ def test_config_choices_for_threading(self, max_workers): dev = DefaultQubit() config = ExecutionConfig(device_options={"max_workers": max_workers}) - _, new_config = dev.preprocess(config) + new_config = dev.setup_execution_config(config) assert new_config.device_options["max_workers"] == max_workers @@ -187,19 +187,19 @@ def test_circuit_wire_validation(self): """Test that preprocessing validates wires on the circuits being executed.""" dev = DefaultQubit(wires=3) circuit_valid_0 = qml.tape.QuantumScript([qml.PauliX(0)]) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() circuits, _ = program([circuit_valid_0]) assert circuits[0].circuit == circuit_valid_0.circuit circuit_valid_1 = qml.tape.QuantumScript([qml.PauliX(1)]) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() circuits, _ = program([circuit_valid_0, circuit_valid_1]) assert circuits[0].circuit == circuit_valid_0.circuit assert circuits[1].circuit == circuit_valid_1.circuit invalid_circuit = qml.tape.QuantumScript([qml.PauliX(4)]) with pytest.raises(qml.wires.WireError, match=r"Cannot run circuit\(s\) on"): - program, _ = dev.preprocess() + program = dev.preprocess_transforms() program( [ invalid_circuit, @@ -207,7 +207,7 @@ def test_circuit_wire_validation(self): ) with pytest.raises(qml.wires.WireError, match=r"Cannot run circuit\(s\) on"): - program, _ = dev.preprocess() + program = dev.preprocess_transforms() program([circuit_valid_0, invalid_circuit]) @pytest.mark.parametrize( @@ -224,7 +224,7 @@ def test_measurement_is_swapped_out(self, mp_fn, mp_cls, shots): original_mp = mp_fn() exp_z = qml.expval(qml.PauliZ(0)) qs = qml.tape.QuantumScript([qml.Hadamard(0)], [original_mp, exp_z], shots=shots) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() tapes, _ = program([qs]) assert len(tapes) == 1 tape = tapes[0] @@ -256,9 +256,9 @@ def test_accepted_operator(self, op, expected): def test_adjoint_only_one_wire(self): """Tests adjoint accepts operators with no parameters or a single parameter and a generator.""" - program = qml.device("default.qubit").preprocess( + program = qml.device("default.qubit").preprocess_transforms( ExecutionConfig(gradient_method="adjoint") - )[0] + ) class MatOp(qml.operation.Operation): """Dummy operation for expanding circuit.""" @@ -342,7 +342,7 @@ def test_validate_measurements(self, shots, measurements, supported): device = qml.device("default.qubit") tape = qml.tape.QuantumScript(measurements=measurements, shots=shots) - program, _ = device.preprocess() + program = device.preprocess_transforms() if not supported: with pytest.raises(qml.DeviceError): @@ -362,7 +362,7 @@ def test_batch_transform_no_batching(self): device = qml.device("default.qubit") - program, _ = device.preprocess() + program = device.preprocess_transforms() tapes, _ = program([tape]) assert len(tapes) == 1 @@ -377,7 +377,7 @@ def test_batch_transform_broadcast_not_adjoint(self): tape = qml.tape.QuantumScript(ops=ops, measurements=measurements) device = qml.devices.DefaultQubit() - program, _ = device.preprocess() + program = device.preprocess_transforms() tapes, _ = program([tape]) assert len(tapes) == 1 @@ -394,7 +394,7 @@ def test_batch_transform_broadcast_adjoint(self): device = qml.devices.DefaultQubit() - program, _ = device.preprocess(execution_config=execution_config) + program = device.preprocess_transforms(execution_config=execution_config) tapes, _ = program([tape]) expected_ops = [ [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(np.pi, wires=1)], @@ -417,7 +417,7 @@ def test_preprocess_batch_transform_not_adjoint(self): qml.tape.QuantumScript(ops=ops, measurements=[measurements[1]]), ] - program, _ = qml.device("default.qubit").preprocess() + program = qml.device("default.qubit").preprocess_transforms() res_tapes, batch_fn = program(tapes) assert len(res_tapes) == 2 @@ -446,7 +446,9 @@ def test_preprocess_batch_transform_adjoint(self): execution_config = ExecutionConfig(gradient_method="adjoint") - program, _ = qml.device("default.qubit").preprocess(execution_config=execution_config) + program = qml.device("default.qubit").preprocess_transforms( + execution_config=execution_config + ) res_tapes, batch_fn = program(tapes) expected_ops = [ @@ -479,7 +481,7 @@ def test_preprocess_expand(self): qml.tape.QuantumScript(ops=ops, measurements=measurements[1]), ] - program, _ = qml.device("default.qubit").preprocess() + program = qml.device("default.qubit").preprocess_transforms() res_tapes, batch_fn = program(tapes) expected = [qml.Hadamard(0), qml.PauliX(1), qml.PauliY(1), qml.RZ(0.123, wires=1)] @@ -503,7 +505,7 @@ def test_preprocess_split_and_expand_not_adjoint(self): qml.tape.QuantumScript(ops=ops, measurements=[measurements[1]]), ] - program, _ = qml.device("default.qubit").preprocess() + program = qml.device("default.qubit").preprocess_transforms() res_tapes, batch_fn = program(tapes) expected_ops = [ qml.Hadamard(0), @@ -538,7 +540,9 @@ def test_preprocess_split_and_expand_adjoint(self): execution_config = ExecutionConfig(gradient_method="adjoint") - program, _ = qml.device("default.qubit").preprocess(execution_config=execution_config) + program = qml.device("default.qubit").preprocess_transforms( + execution_config=execution_config + ) res_tapes, batch_fn = program(tapes) expected_ops = [ @@ -571,7 +575,7 @@ def test_preprocess_check_validity_fail(self): qml.tape.QuantumScript(ops=ops, measurements=measurements[1]), ] - program, _ = qml.device("default.qubit").preprocess() + program = qml.device("default.qubit").preprocess_transforms() with pytest.raises(qml.DeviceError, match="Operator NoMatNoDecompOp"): program(tapes) @@ -592,7 +596,7 @@ def test_preprocess_invalid_tape_adjoint(self, ops, measurement, message): qs = qml.tape.QuantumScript(ops, measurement) execution_config = qml.devices.ExecutionConfig(gradient_method="adjoint") - program, _ = qml.device("default.qubit").preprocess(execution_config) + program = qml.device("default.qubit").preprocess_transforms(execution_config) with pytest.raises(qml.DeviceError, match=message): program([qs]) @@ -607,7 +611,7 @@ def test_preprocess_tape_for_adjoint(self): ) execution_config = qml.devices.ExecutionConfig(gradient_method="adjoint") - program, _ = qml.device("default.qubit").preprocess(execution_config) + program = qml.device("default.qubit").preprocess_transforms(execution_config) expanded_tapes, _ = program([qs]) assert len(expanded_tapes) == 1 @@ -744,7 +748,7 @@ def test_finite_shots_analytic_diff_method(self, diff_method): tape = qml.tape.QuantumScript([], [qml.expval(qml.PauliZ(0))], shots=100) execution_config = ExecutionConfig(gradient_method=diff_method) - program, _ = qml.device("default.qubit").preprocess(execution_config) + program = qml.device("default.qubit").preprocess_transforms(execution_config) msg = "Finite shots are not supported with" with pytest.raises(qml.DeviceError, match=msg): @@ -757,9 +761,9 @@ def test_non_diagonal_non_expval(self): measurements = [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliX(3))] qs = qml.tape.QuantumScript(ops=[], measurements=measurements) - program = qml.device("default.qubit").preprocess( + program = qml.device("default.qubit").preprocess_transforms( ExecutionConfig(gradient_method="adjoint") - )[0] + ) with pytest.raises( qml.DeviceError, @@ -776,9 +780,9 @@ def test_unsupported_op_decomposed(self): [qml.expval(qml.PauliZ(2))], ) batch = (qs,) - program = qml.device("default.qubit").preprocess( + program = qml.device("default.qubit").preprocess_transforms( ExecutionConfig(gradient_method="adjoint") - )[0] + ) res, _ = program(batch) res = res[0] assert isinstance(res, qml.tape.QuantumScript) @@ -799,9 +803,9 @@ def test_trainable_params_decomposed(self): qs = qml.tape.QuantumScript(ops, [qml.expval(qml.PauliZ(0))]) qs.trainable_params = [0] - program = qml.device("default.qubit").preprocess( + program = qml.device("default.qubit").preprocess_transforms( ExecutionConfig(gradient_method="adjoint") - )[0] + ) res, _ = program((qs,)) res = res[0] @@ -839,9 +843,9 @@ def test_u3_non_trainable_params(self): ) qs.trainable_params = [0, 2] - program = qml.device("default.qubit").preprocess( + program = qml.device("default.qubit").preprocess_transforms( ExecutionConfig(gradient_method="adjoint") - )[0] + ) res, _ = program((qs,)) res = res[0] assert isinstance(res, qml.tape.QuantumScript) @@ -860,9 +864,9 @@ def test_trainable_hermitian_warns(self): qs.trainable_params = {0} - program = qml.device("default.qubit").preprocess( + program = qml.device("default.qubit").preprocess_transforms( ExecutionConfig(gradient_method="adjoint") - )[0] + ) with pytest.warns( UserWarning, match="Differentiating with respect to the input parameters of Hermitian" @@ -880,9 +884,9 @@ def test_valid_tape_no_expand(self, G): measurements=[qml.expval(qml.PauliZ(0))], ) - program = qml.device("default.qubit").preprocess( + program = qml.device("default.qubit").preprocess_transforms( ExecutionConfig(gradient_method="adjoint") - )[0] + ) qs.trainable_params = [1] qs_valid, _ = program((qs,)) @@ -909,9 +913,9 @@ def test_valid_tape_with_expansion(self): measurements=[qml.expval(qml.PauliZ(0))], ) - program = qml.device("default.qubit").preprocess( + program = qml.device("default.qubit").preprocess_transforms( ExecutionConfig(gradient_method="adjoint") - )[0] + ) qs.trainable_params = {1, 2, 3} qs_valid, _ = program((qs,)) diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index bf452a76107..26f46ab44e9 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -179,7 +179,7 @@ def test_kwargs_mps(max_bond_dim, cutoff): dev = qml.device("default.tensor", method=method, max_bond_dim=max_bond_dim, cutoff=cutoff) - _, config = dev.preprocess() + config = dev.setup_execution_config() assert config.device_options["method"] == method assert config.device_options["max_bond_dim"] == max_bond_dim assert config.device_options["cutoff"] == cutoff @@ -192,7 +192,7 @@ def test_kwargs_tn(): method = "tn" dev = qml.device("default.tensor", method=method) - _, config = dev.preprocess() + config = dev.setup_execution_config() assert config.device_options["method"] == method assert config.device_options["contract"] == "auto-split-gate" diff --git a/tests/devices/default_tensor/test_tensor_expval.py b/tests/devices/default_tensor/test_tensor_expval.py index 2a9b663dd92..5fd73c4e6ad 100644 --- a/tests/devices/default_tensor/test_tensor_expval.py +++ b/tests/devices/default_tensor/test_tensor_expval.py @@ -49,7 +49,7 @@ def dev(request): def calculate_reference(tape): """Calculate the reference value of the tape using DefaultQubit.""" ref_dev = DefaultQubit(max_workers=1) - program, _ = ref_dev.preprocess() + program = ref_dev.preprocess_transforms() tapes, transf_fn = program([tape]) results = ref_dev.execute(tapes) return transf_fn(results) diff --git a/tests/devices/default_tensor/test_tensor_var.py b/tests/devices/default_tensor/test_tensor_var.py index 794798db8a5..b477d5da31a 100644 --- a/tests/devices/default_tensor/test_tensor_var.py +++ b/tests/devices/default_tensor/test_tensor_var.py @@ -48,7 +48,7 @@ def dev(request): def calculate_reference(tape): """Calculate the reference value of the tape using DefaultQubit.""" ref_dev = DefaultQubit(max_workers=1) - program, _ = ref_dev.preprocess() + program = ref_dev.preprocess_transforms() tapes, transf_fn = program([tape]) results = ref_dev.execute(tapes) return transf_fn(results) diff --git a/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py b/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py index f72f433ff51..cf81524e5dc 100644 --- a/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py +++ b/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py @@ -67,7 +67,7 @@ def test_chooses_best_gradient_method(self): config = ExecutionConfig(gradient_method="best") - _, new_config = dev.preprocess(config) + new_config = dev.setup_execution_config(config) assert new_config.gradient_method == "backprop" assert not new_config.use_device_gradient @@ -78,18 +78,18 @@ def test_circuit_wire_validation(self): dev = DefaultQutritMixed(wires=3) circuit_valid_0 = qml.tape.QuantumScript([qml.TShift(0)]) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() circuits, _ = program([circuit_valid_0]) assert circuits[0].circuit == circuit_valid_0.circuit circuit_valid_1 = qml.tape.QuantumScript([qml.TShift(1)]) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() circuits, _ = program([circuit_valid_0, circuit_valid_1]) assert circuits[0].circuit == circuit_valid_0.circuit assert circuits[1].circuit == circuit_valid_1.circuit invalid_circuit = qml.tape.QuantumScript([qml.TShift(4)]) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() with pytest.raises(qml.wires.WireError, match=r"Cannot run circuit\(s\) on"): program([invalid_circuit]) @@ -111,7 +111,7 @@ def test_measurement_is_swapped_out(self, mp_fn, mp_cls, shots): original_mp = mp_fn() exp_z = qml.expval(qml.GellMann(0, 3)) qs = qml.tape.QuantumScript([qml.THadamard(0)], [original_mp, exp_z], shots=shots) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() tapes, _ = program([qs]) assert len(tapes) == 1 tape = tapes[0] @@ -165,7 +165,7 @@ def test_batch_transform_no_batching(self): tape = qml.tape.QuantumScript(ops=ops, measurements=measurements) device = DefaultQutritMixed() - program, _ = device.preprocess() + program = device.preprocess_transforms() tapes, _ = program([tape]) assert len(tapes) == 1 @@ -179,7 +179,7 @@ def test_batch_transform_broadcast(self): tape = qml.tape.QuantumScript(ops=ops, measurements=measurements) device = DefaultQutritMixed() - program, _ = device.preprocess() + program = device.preprocess_transforms() tapes, _ = program([tape]) assert len(tapes) == 1 @@ -195,7 +195,7 @@ def test_preprocess_batch_transform(self): qml.tape.QuantumScript(ops=ops, measurements=[measurements[1]]), ] - program, _ = DefaultQutritMixed().preprocess() + program = DefaultQutritMixed().preprocess_transforms() res_tapes, batch_fn = program(tapes) assert len(res_tapes) == 2 @@ -239,7 +239,7 @@ def test_preprocess_batch_and_expand(self): qml.tape.QuantumScript(ops=ops, measurements=[measurements[1]]), ] - program, _ = DefaultQutritMixed().preprocess() + program = DefaultQutritMixed().preprocess_transforms() res_tapes, batch_fn = program(tapes) expected_ops = [ qml.THadamard(0), @@ -267,7 +267,7 @@ def test_preprocess_check_validity_fail(self): qml.tape.QuantumScript(ops=ops, measurements=measurements[1]), ] - program, _ = DefaultQutritMixed().preprocess() + program = DefaultQutritMixed().preprocess_transforms() with pytest.raises(qml.DeviceError, match="Operator NoMatNoDecompOp"): program(tapes) @@ -303,7 +303,7 @@ def test_preprocess_warns_measurement_error_state( device = DefaultQutritMixed( readout_relaxation_probs=relaxations, readout_misclassification_probs=misclassifications ) - program, _ = device.preprocess() + program = device.preprocess_transforms() with warnings.catch_warnings(record=True) as warning: program(tapes) diff --git a/tests/devices/test_default_clifford.py b/tests/devices/test_default_clifford.py index 9f3b55933af..ad420ee8120 100644 --- a/tests/devices/test_default_clifford.py +++ b/tests/devices/test_default_clifford.py @@ -484,9 +484,9 @@ def test_max_worker_clifford(): ) tapes = (qscript, qscript) - _, conf_d = dev_c.preprocess() + conf_d = dev_c.setup_execution_config() res_c = dev_c.execute(tapes, conf_d) - _, conf_q = dev_q.preprocess() + conf_q = dev_q.setup_execution_config() res_q = dev_q.execute(tapes, conf_q) assert np.allclose(res_q, res_c) @@ -504,9 +504,9 @@ def test_tracker(): tapes = tuple([qscript]) with qml.Tracker(dev_c) as tracker: - _, conf_d = dev_c.preprocess() + conf_d = dev_c.setup_execution_config() res_c = dev_c.execute(tapes, conf_d) - _, conf_q = dev_q.preprocess() + conf_q = dev_q.setup_execution_config() res_q = dev_q.execute(tapes, conf_q) assert np.allclose(res_q, res_c) @@ -601,7 +601,7 @@ def circuit_fn(): qnode_clfrd = qml.QNode(circuit_fn, dev_c) qnode_clfrd() - conf_c, tape_c = dev_c.preprocess()[1], qnode_clfrd.tape + conf_c, tape_c = dev_c.setup_execution_config(), qnode_clfrd.tape with pytest.raises( NotImplementedError, diff --git a/tests/devices/test_device_api.py b/tests/devices/test_device_api.py index 93b1a5ae18b..8a7f1c6e9cc 100644 --- a/tests/devices/test_device_api.py +++ b/tests/devices/test_device_api.py @@ -14,16 +14,18 @@ """ Tests for the basic default behavior of the Device API. """ - -# pylint:disable=unused-argument,too-few-public-methods,unused-variable +from typing import Optional import pytest import pennylane as qml from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig from pennylane.devices.capabilities import DeviceCapabilities +from pennylane.transforms.core import TransformProgram from pennylane.wires import Wires +# pylint:disable=unused-argument,too-few-public-methods,unused-variable + def test_execute_method_abstract(): """Test that a device can't be instantiated without an execute method.""" @@ -158,7 +160,7 @@ def test_preprocess_single_circuit(self): a = (1,) assert fn(a) == (1,) - assert config is qml.devices.DefaultExecutionConfig + assert config == qml.devices.DefaultExecutionConfig def test_preprocess_batch_circuits(self): """Test that preprocessing a batch doesn't do anything.""" @@ -234,6 +236,31 @@ def test_wires_are_read_only(self): self.dev.wires = [0, 1] # pylint:disable=attribute-defined-outside-init +def test_device_with_ambiguous_preprocess(): + """Tests that an error is raised when defining a device with ambiguous preprocess.""" + + with pytest.raises(ValueError, match="A device should implement either"): + + class InvalidDevice(Device): + """A device with ambiguous preprocess.""" + + def preprocess(self, execution_config=None): + return TransformProgram(), ExecutionConfig() + + def setup_execution_config( + self, config: Optional[ExecutionConfig] = None + ) -> ExecutionConfig: + return ExecutionConfig() + + def preprocess_transforms( + self, execution_config: Optional[ExecutionConfig] = None + ) -> TransformProgram: + return TransformProgram() + + def execute(self, circuits, execution_config: ExecutionConfig = DefaultExecutionConfig): + return (0,) + + class TestProvidingDerivatives: """Tests logic when derivatives, vjp, or jvp are overridden.""" diff --git a/tests/devices/test_legacy_facade.py b/tests/devices/test_legacy_facade.py index 9dcd55ab2f4..5f425ab5bb8 100644 --- a/tests/devices/test_legacy_facade.py +++ b/tests/devices/test_legacy_facade.py @@ -215,7 +215,7 @@ def test_preprocessing_program(): """Test the population of the preprocessing program.""" dev = DummyDevice(wires=(0, 1)) - program, _ = LegacyDeviceFacade(dev).preprocess() + program = LegacyDeviceFacade(dev).preprocess_transforms() assert ( program[0].transform == legacy_device_batch_transform.transform @@ -252,7 +252,7 @@ class MidMeasureDev(DummyDevice): _capabilities = {"supports_mid_measure": True} dev = MidMeasureDev() - program, _ = LegacyDeviceFacade(dev).preprocess() + program = LegacyDeviceFacade(dev).preprocess_transforms() assert qml.defer_measurements not in program @@ -372,7 +372,7 @@ class DeviceDerivatives(DummyDevice): assert dev._validate_device_method(qml.tape.QuantumScript()) config = qml.devices.ExecutionConfig(gradient_method="best") - _, processed_config = dev.preprocess(config) + processed_config = dev.setup_execution_config(config) assert processed_config.use_device_gradient is True assert processed_config.grad_on_execution is True @@ -404,4 +404,4 @@ class BackpropDevice(DummyDevice): assert dev.supports_derivatives(qml.devices.ExecutionConfig(gradient_method="backprop")) config = qml.devices.ExecutionConfig(gradient_method="backprop", use_device_gradient=True) - assert dev.preprocess(config)[1] is config # unchanged + assert dev.setup_execution_config(config) is config # unchanged diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py index 928a4c7ea90..4f79494c851 100644 --- a/tests/devices/test_null_qubit.py +++ b/tests/devices/test_null_qubit.py @@ -72,7 +72,7 @@ class MyOp(qml.operation.Operator): tape = qml.tape.QuantumScript([MyOp(wires=(0, 1))], [qml.expval(qml.Z(0))], shots=shots) dev = NullQubit() - program, _ = dev.preprocess() + program = dev.preprocess_transforms() batch, _ = program((tape,)) assert isinstance(batch[0][0], MyOp) @@ -167,7 +167,7 @@ def test_swaps_adjoint_to_mean_device(self): """Test that null.qubit interprets 'adjoint' as device derivatives.""" dev = NullQubit() config = ExecutionConfig(gradient_method="adjoint") - _, config = dev.preprocess(config) + config = dev.setup_execution_config(config) assert config.gradient_method == "device" assert dev.supports_derivatives(config) is True assert dev.supports_jvp(config) is True @@ -755,7 +755,7 @@ def test_derivatives_single_circuit(self): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="device") - [qs], _ = dev.preprocess(config)[0]((qs,)) + [qs], _ = dev.preprocess_transforms(config)((qs,)) actual_grad = dev.compute_derivatives(qs, config) assert isinstance(actual_grad, np.ndarray) assert actual_grad.shape == () # pylint: disable=no-member @@ -771,7 +771,8 @@ def test_derivatives_list_with_single_circuit(self): x = np.array(np.pi / 7) qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="device") - [qs], _ = dev.preprocess(config)[0]((qs,)) + transform_program = dev.preprocess_transforms(config) + [qs], _ = transform_program((qs,)) actual_grad = dev.compute_derivatives([qs], self.ec) assert actual_grad == (np.array(0.0),) @@ -817,7 +818,7 @@ def test_jvps_single_circuit(self): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="device") - [qs], _ = dev.preprocess(config)[0]((qs,)) + [qs], _ = dev.preprocess_transforms(config)((qs,)) actual_grad = dev.compute_jvp(qs, tangent, self.ec) assert isinstance(actual_grad, np.ndarray) @@ -836,7 +837,7 @@ def test_jvps_list_with_single_circuit(self): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="device") - [qs], _ = dev.preprocess(config)[0]((qs,)) + [qs], _ = dev.preprocess_transforms(config)((qs,)) actual_grad = dev.compute_jvp([qs], [tangent], self.ec) assert actual_grad == (np.array(0.0),) @@ -891,7 +892,7 @@ def test_vjps_single_circuit(self): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="device") - [qs], _ = dev.preprocess(config)[0]((qs,)) + [qs], _ = dev.preprocess_transforms(config)((qs,)) actual_grad = dev.compute_vjp(qs, cotangent, self.ec) assert actual_grad == (0.0,) @@ -907,7 +908,7 @@ def test_vjps_list_with_single_circuit(self): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) config = ExecutionConfig(gradient_method="device") - [qs], _ = dev.preprocess(config)[0]((qs,)) + [qs], _ = dev.preprocess_transforms(config)((qs,)) actual_grad = dev.compute_vjp([qs], [cotangent], self.ec) assert actual_grad == ((0.0,),) diff --git a/tests/gradients/core/test_metric_tensor.py b/tests/gradients/core/test_metric_tensor.py index 387a7f15d63..e585a43483d 100644 --- a/tests/gradients/core/test_metric_tensor.py +++ b/tests/gradients/core/test_metric_tensor.py @@ -17,7 +17,7 @@ import importlib # pylint: disable=too-many-arguments,too-many-public-methods,too-few-public-methods -# pylint: disable=not-callable,too-many-statements +# pylint: disable=not-callable,too-many-statements, too-many-positional-arguments import pytest from scipy.linalg import block_diag @@ -1360,10 +1360,13 @@ def test_jax(self, diff_method, tol, ansatz, weights): def cost_full(*weights): return qml.metric_tensor(qnode, approx=None)(*weights) - _cost_full = autodiff_metric_tensor(ansatz, num_wires=3) - assert qml.math.allclose(_cost_full(*weights), cost_full(*weights), atol=tol, rtol=0) - jac = jax.jacobian(cost_full)(*weights) - expected_full = qml.jacobian(_cost_full)(*weights) + weights_jax = tuple(jax.numpy.array(w) for w in weights) + _cost_full_autograd = autodiff_metric_tensor(ansatz, num_wires=3) + v1 = _cost_full_autograd(*weights) + v2 = cost_full(*weights_jax) + assert qml.math.allclose(v1, v2, atol=tol, rtol=0) + jac = jax.jacobian(cost_full)(*weights_jax) + expected_full = qml.jacobian(_cost_full_autograd)(*weights) assert qml.math.allclose(expected_full, jac, atol=tol, rtol=0) @pytest.mark.tf diff --git a/tests/interfaces/test_autograd.py b/tests/interfaces/test_autograd.py index cff5f77369b..482588c00e9 100644 --- a/tests/interfaces/test_autograd.py +++ b/tests/interfaces/test_autograd.py @@ -574,7 +574,7 @@ def cost_fn(a, p): gradient_method=_gradient_method, grad_on_execution=execute_kwargs.get("grad_on_execution", None), ) - program, _ = device.preprocess(execution_config=config) + program = device.preprocess_transforms(execution_config=config) return execute([tape], device, **execute_kwargs, transform_program=program)[0] a = pnp.array(0.1, requires_grad=False) diff --git a/tests/interfaces/test_jax.py b/tests/interfaces/test_jax.py index 09eb9dd6fa2..491f336c51d 100644 --- a/tests/interfaces/test_jax.py +++ b/tests/interfaces/test_jax.py @@ -514,7 +514,7 @@ def cost_fn(a, p): gradient_method=_gradient_method, grad_on_execution=execute_kwargs.get("grad_on_execution", None), ) - program, _ = device.preprocess(execution_config=conf) + program = device.preprocess_transforms(execution_config=conf) return execute([tape], device, **execute_kwargs, transform_program=program)[0] a = jnp.array(0.1) diff --git a/tests/interfaces/test_tensorflow.py b/tests/interfaces/test_tensorflow.py index 7cdc8778245..5fe780f0f5a 100644 --- a/tests/interfaces/test_tensorflow.py +++ b/tests/interfaces/test_tensorflow.py @@ -509,7 +509,7 @@ def cost_fn(a, p): gradient_method=_gradient_method, grad_on_execution=execute_kwargs.get("grad_on_execution", None), ) - program, _ = device.preprocess(execution_config=config) + program = device.preprocess_transforms(execution_config=config) return execute([tape], device, **execute_kwargs, transform_program=program)[0] a = tf.constant(0.1) diff --git a/tests/interfaces/test_torch.py b/tests/interfaces/test_torch.py index 103c54d614d..2ca4d4bc792 100644 --- a/tests/interfaces/test_torch.py +++ b/tests/interfaces/test_torch.py @@ -553,7 +553,7 @@ def cost_fn(a, p): gradient_method=_gradient_method, grad_on_execution=execute_kwargs.get("grad_on_execution", None), ) - program, _ = device.preprocess(execution_config=config) + program = device.preprocess_transforms(execution_config=config) return execute([tape], device, **execute_kwargs, transform_program=program)[0] a = torch.tensor(0.1, requires_grad=False) diff --git a/tests/measurements/test_measurements.py b/tests/measurements/test_measurements.py index 61be7d41359..1a9fda57843 100644 --- a/tests/measurements/test_measurements.py +++ b/tests/measurements/test_measurements.py @@ -680,7 +680,7 @@ def test_custom_measurement(self): class CountTapesMP(MeasurementTransform, SampleMeasurement): def process(self, tape, device): - program, _ = device.preprocess() + program = device.preprocess_transforms() tapes, _ = program([tape]) return len(tapes) diff --git a/tests/measurements/test_probs.py b/tests/measurements/test_probs.py index 03e4b78d797..9f6114a0c80 100644 --- a/tests/measurements/test_probs.py +++ b/tests/measurements/test_probs.py @@ -379,11 +379,11 @@ def circuit(): @pytest.mark.parametrize("shots", [None, 1111, [1111, 1111]]) @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3)) def test_observable_is_measurement_value( - self, shots, phi, tol, tol_stochastic + self, shots, phi, tol, tol_stochastic, seed ): # pylint: disable=too-many-arguments """Test that probs for mid-circuit measurement values are correct for a single measurement value.""" - dev = qml.device("default.qubit", wires=2, shots=shots) + dev = qml.device("default.qubit", wires=2, shots=shots, seed=seed) @qml.qnode(dev) def circuit(phi): diff --git a/tests/measurements/test_state.py b/tests/measurements/test_state.py index 4be631e38b7..1e2b43e115b 100644 --- a/tests/measurements/test_state.py +++ b/tests/measurements/test_state.py @@ -302,7 +302,7 @@ def func(): return state() state_val = func() - program, _ = dev.preprocess() + program = dev.preprocess_transforms() scripts, _ = program([func.tape]) assert len(scripts) == 1 expected_state, _ = qml.devices.qubit.get_final_state(scripts[0]) diff --git a/tests/tape/test_qscript.py b/tests/tape/test_qscript.py index 545b41a88d4..3aceeea1111 100644 --- a/tests/tape/test_qscript.py +++ b/tests/tape/test_qscript.py @@ -1275,7 +1275,7 @@ def test_output_shapes_single_qnode_check(self, measurement, expected_shape, sho ops = [qml.RY(a, 0), qml.RX(b, 0)] qs = QuantumScript(ops, [measurement], shots=shots) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() # TODO: test diff_method is not None when the interface `execute` functions are implemented res = qml.execute([qs], dev, diff_method=None, transform_program=program)[0] @@ -1394,7 +1394,7 @@ def test_broadcasting_single(self, measurement, expected_shape, shots): qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() expected_shape = qml.execute([tape], dev, diff_method=None, transform_program=program)[ 0 ].shape @@ -1425,7 +1425,7 @@ def test_broadcasting_multi(self, measurement, expected, shots): qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() expected = qml.execute([tape], dev, diff_method=None, transform_program=program)[0] actual = tape.shape(dev) @@ -1476,7 +1476,7 @@ def test_multi_measure_sample_wires_shot_vector(self): res = qs.shape(dev) assert res == expected - program, _ = dev.preprocess() + program = dev.preprocess_transforms() expected = qml.execute([qs], dev, diff_method=None, transform_program=program)[0] expected_shape = tuple(tuple(e_.shape for e_ in e) for e in expected) diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index d3d47027985..683f396e106 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -1846,7 +1846,7 @@ def test_output_shapes_single_qnode_check(self, measurement, _, shots): qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() res = qml.execute( [tape], dev, diff_method=qml.gradients.param_shift, transform_program=program )[0] @@ -2002,7 +2002,7 @@ def test_broadcasting_single(self, measurement, _, shots): qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() expected = qml.execute([tape], dev, diff_method=None, transform_program=program)[0] assert tape.shape(dev) == expected.shape @@ -2030,7 +2030,7 @@ def test_broadcasting_multi(self, measurement, expected, shots): qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() expected = qml.execute([tape], dev, diff_method=None, transform_program=program)[0] expected = tuple(i.shape for i in expected) assert tape.shape(dev) == expected diff --git a/tests/templates/test_subroutines/test_approx_time_evolution.py b/tests/templates/test_subroutines/test_approx_time_evolution.py index 1ca12531cf5..bbd44029239 100644 --- a/tests/templates/test_subroutines/test_approx_time_evolution.py +++ b/tests/templates/test_subroutines/test_approx_time_evolution.py @@ -458,7 +458,7 @@ def cost(coeffs, t): if diff_method is qml.gradients.param_shift and dev_name != "default.qubit": tape = dev.expand_fn(tape) return qml.execute([tape], dev, diff_method)[0] - program, _ = dev.preprocess() + program = dev.preprocess_transforms() return qml.execute([tape], dev, diff_method=diff_method, transform_program=program)[0] t = pnp.array(0.54, requires_grad=True) diff --git a/tests/templates/test_subroutines/test_qpe.py b/tests/templates/test_subroutines/test_qpe.py index a047ffbd71f..32583f42c67 100644 --- a/tests/templates/test_subroutines/test_qpe.py +++ b/tests/templates/test_subroutines/test_qpe.py @@ -164,7 +164,7 @@ def test_phase_estimated(self, phase): qml.probs(estimation_wires) tape = qml.tape.QuantumScript.from_queue(q) - tapes, _ = dev.preprocess()[0]([tape]) + tapes, _ = dev.preprocess_transforms()([tape]) assert len(tapes) == 1 res = dev.execute(tapes)[0].flatten() @@ -216,7 +216,7 @@ def test_phase_estimated_two_qubit(self): qml.probs(estimation_wires) tape = qml.tape.QuantumScript.from_queue(q) - tapes, _ = dev.preprocess()[0]([tape]) + tapes, _ = dev.preprocess_transforms()([tape]) assert len(tapes) == 1 res = dev.execute(tapes)[0].flatten() @@ -264,7 +264,7 @@ def test_phase_estimated_single_ops(self, param): [qml.probs(estimation_wires)], ) - tapes, _ = dev.preprocess()[0]([tape]) + tapes, _ = dev.preprocess_transforms()([tape]) res = dev.execute(tapes)[0].flatten() assert len(tapes) == 1 @@ -309,7 +309,7 @@ def test_phase_estimated_ops(self, param): [qml.probs(estimation_wires)], ) - tapes, _ = dev.preprocess()[0]([tape]) + tapes, _ = dev.preprocess_transforms()([tape]) assert len(tapes) == 1 res = dev.execute(tapes)[0].flatten() diff --git a/tests/test_return_types.py b/tests/test_return_types.py index 04b6b155796..ed676de849d 100644 --- a/tests/test_return_types.py +++ b/tests/test_return_types.py @@ -44,7 +44,7 @@ def circuit(x): if dev.shots: pytest.skip("cannot return analytic measurements with finite shots.") - program, _ = dev.preprocess() + program = dev.preprocess_transforms() res = qml.execute( tapes=[qnode.tape], device=dev, @@ -1228,7 +1228,7 @@ def return_type(self): with pytest.raises( qml.DeviceError, match="not accepted for analytic simulation on default.qubit" ): - program, _ = dev.preprocess() + program = dev.preprocess_transforms() qml.execute(tapes=[tape], device=dev, diff_method=None, transform_program=program) def test_state_return_with_other_types(self): @@ -1258,7 +1258,7 @@ def test_entropy_no_custom_wires(self): qml.vn_entropy(wires=["a"]) tape = qml.tape.QuantumScript.from_queue(q) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() res = qml.execute(tapes=[tape], device=dev, diff_method=None, transform_program=program) assert res == (0,) @@ -1272,6 +1272,6 @@ def test_custom_wire_labels_error(self): qml.mutual_info(wires0=["a"], wires1=["b"]) tape = qml.tape.QuantumScript.from_queue(q) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() res = qml.execute(tapes=[tape], device=dev, diff_method=None, transform_program=program) assert res == (0,) diff --git a/tests/transforms/core/test_transform_dispatcher.py b/tests/transforms/core/test_transform_dispatcher.py index 0ded4a61783..37c8db20400 100644 --- a/tests/transforms/core/test_transform_dispatcher.py +++ b/tests/transforms/core/test_transform_dispatcher.py @@ -622,8 +622,8 @@ def test_device_transform(self, valid_transform): assert new_dev.original_device is dev assert repr(new_dev).startswith("Transformed Device") - program, _ = dev.preprocess() - new_program, _ = new_dev.preprocess() + program = dev.preprocess_transforms() + new_program = new_dev.preprocess_transforms() assert isinstance(program, qml.transforms.core.TransformProgram) assert isinstance(new_program, qml.transforms.core.TransformProgram) @@ -651,8 +651,8 @@ def test_old_device_transform(self, valid_transform): assert new_dev.original_device is dev assert repr(new_dev).startswith("Transformed Device") - program, _ = dev.preprocess() - new_program, _ = new_dev.preprocess() + program = dev.preprocess_transforms() + new_program = new_dev.preprocess_transforms() assert isinstance(program, qml.transforms.core.TransformProgram) assert isinstance(new_program, qml.transforms.core.TransformProgram) diff --git a/tests/transforms/test_add_noise.py b/tests/transforms/test_add_noise.py index 1835b613d13..779dcf91e36 100644 --- a/tests/transforms/test_add_noise.py +++ b/tests/transforms/test_add_noise.py @@ -205,14 +205,14 @@ def test_add_noise_dev(self, dev_name): in_tape = QuantumScript.from_queue(q_in_tape) dev = qml.device(dev_name, wires=2) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() res_without_noise = qml.execute( [in_tape], dev, qml.gradients.param_shift, transform_program=program ) c, n = qml.noise.op_in([qml.RX, qml.RY]), qml.noise.partial_wires(qml.PhaseShift, 0.4) new_dev = add_noise(dev, noise_model=qml.NoiseModel({c: n})) - new_program, _ = new_dev.preprocess() + new_program = new_dev.preprocess_transforms() [tape], _ = new_program([in_tape]) res_with_noise = qml.execute( [in_tape], new_dev, qml.gradients.param_shift, transform_program=new_program diff --git a/tests/transforms/test_cliffordt_transform.py b/tests/transforms/test_cliffordt_transform.py index e9c371d3f95..ac5e01d9072 100644 --- a/tests/transforms/test_cliffordt_transform.py +++ b/tests/transforms/test_cliffordt_transform.py @@ -147,7 +147,7 @@ def test_decomposition(self, circuit): ) dev = qml.device("default.qubit") - transform_program, _ = dev.preprocess() + transform_program = dev.preprocess_transforms() res1, res2 = qml.execute( [old_tape, new_tape], device=dev, transform_program=transform_program ) @@ -221,7 +221,7 @@ def circuit(): ) dev = qml.device("default.qubit") - transform_program, _ = dev.preprocess() + transform_program = dev.preprocess_transforms() res1, res2 = qml.execute( [old_tape, new_tape], device=dev, transform_program=transform_program ) @@ -249,7 +249,7 @@ def circuit(): ) dev = qml.device("default.qubit") - transform_program, _ = dev.preprocess() + transform_program = dev.preprocess_transforms() res1, res2 = qml.execute( [old_tape, new_tape], device=dev, transform_program=transform_program ) diff --git a/tests/transforms/test_insert_ops.py b/tests/transforms/test_insert_ops.py index a56fea89787..34311fbd8db 100644 --- a/tests/transforms/test_insert_ops.py +++ b/tests/transforms/test_insert_ops.py @@ -426,13 +426,13 @@ def test_insert_dev(dev_name): in_tape = QuantumScript.from_queue(q_in_tape) dev = qml.device(dev_name, wires=2) - program, _ = dev.preprocess() + program = dev.preprocess_transforms() res_without_noise = qml.execute( [in_tape], dev, qml.gradients.param_shift, transform_program=program ) new_dev = insert(dev, qml.PhaseShift, 0.4) - new_program, _ = new_dev.preprocess() + new_program = new_dev.preprocess_transforms() tapes, _ = new_program([in_tape]) tape = tapes[0] res_with_noise = qml.execute( diff --git a/tests/transforms/test_tape_expand.py b/tests/transforms/test_tape_expand.py index e99570441d8..5f76c7db597 100644 --- a/tests/transforms/test_tape_expand.py +++ b/tests/transforms/test_tape_expand.py @@ -800,15 +800,15 @@ def circuit(): assert len(tape.operations) == 1 assert tape.operations[0].name == "CNOT" - assert dev.preprocess()[0][2].transform.__name__ == "decompose" - assert dev.preprocess()[0][2].kwargs.get("decomposer", None) is None + assert dev.preprocess_transforms()[2].transform.__name__ == "decompose" + assert dev.preprocess_transforms()[2].kwargs.get("decomposer", None) is None # Test within the context manager with qml.transforms.set_decomposition({qml.CNOT: custom_cnot}, dev): _ = circuit() - assert dev.preprocess()[0][2].transform.__name__ == "decompose" - assert dev.preprocess()[0][2].kwargs.get("decomposer", None) is not None + assert dev.preprocess_transforms()[2].transform.__name__ == "decompose" + assert dev.preprocess_transforms()[2].kwargs.get("decomposer", None) is not None tape = spy.call_args_list[1][0][0][0] ops_in_context = tape.operations @@ -824,8 +824,8 @@ def circuit(): ops_in_context = tape.operations assert len(tape.operations) == 1 assert tape.operations[0].name == "CNOT" - assert dev.preprocess()[0][2].transform.__name__ == "decompose" - assert dev.preprocess()[0][2].kwargs.get("decomposer", None) is None + assert dev.preprocess_transforms()[2].transform.__name__ == "decompose" + assert dev.preprocess_transforms()[2].kwargs.get("decomposer", None) is None # pylint: disable=cell-var-from-loop diff --git a/tests/transforms/test_transpile.py b/tests/transforms/test_transpile.py index de4b6df844f..fca172508d5 100644 --- a/tests/transforms/test_transpile.py +++ b/tests/transforms/test_transpile.py @@ -363,7 +363,7 @@ def test_transpile_state_with_device(self): assert batch[0][2] == qml.CNOT((0, 1)) assert batch[0][3] == qml.state() - pre, post = dev.preprocess()[0]((tape,)) + pre, post = dev.preprocess_transforms()((tape,)) original_results = post(dev.execute(pre)) transformed_results = fn(dev.execute(batch)) assert qml.math.allclose(original_results, transformed_results) @@ -389,7 +389,7 @@ def test_transpile_state_with_device_multiple_measurements(self): assert batch[0][3] == qml.state() assert batch[0][4] == qml.expval(qml.PauliZ(1)) - pre, post = dev.preprocess()[0]((tape,)) + pre, post = dev.preprocess_transforms()((tape,)) original_results = post(dev.execute(pre)) transformed_results = fn(dev.execute(batch)) assert qml.math.allclose(original_results[0][0], transformed_results[0]) @@ -421,7 +421,7 @@ def test_transpile_probs_sample_filled_in_wires(self): assert batch[0].measurements[0] == qml.probs(wires=(0, 2, 1)) assert batch[0].measurements[1] == qml.sample(wires=(0, 2, 1)) - pre, post = dev.preprocess()[0]((tape,)) + pre, post = dev.preprocess_transforms()((tape,)) original_results = post(dev.execute(pre))[0] transformed_results = fn(dev.execute(batch)) assert qml.math.allclose(original_results[0], transformed_results[0]) diff --git a/tests/workflow/test_construct_batch.py b/tests/workflow/test_construct_batch.py index 25c685c3f27..f11eaee459c 100644 --- a/tests/workflow/test_construct_batch.py +++ b/tests/workflow/test_construct_batch.py @@ -107,7 +107,7 @@ def circuit(): assert p_none == p_dev assert len(p_dev) == 9 config = qml.devices.ExecutionConfig(interface=getattr(circuit, "interface", None)) - assert p_dev == p_grad + dev.preprocess(config)[0] + assert p_dev == p_grad + dev.preprocess_transforms(config) # slicing p_sliced = get_transform_program(circuit, slice(2, 7, 2)) @@ -147,7 +147,7 @@ def circuit(x): gradient_method="adjoint", use_device_jacobian_product=False, ) - dev_program = dev.preprocess(config)[0] + dev_program = dev.preprocess_transforms(config) expected = TransformProgram() expected.add_transform(qml.transforms.split_non_commuting) @@ -203,7 +203,9 @@ def circuit(): dev_program = get_transform_program(circuit, level="device") config = qml.devices.ExecutionConfig(interface=getattr(circuit, "interface", None)) - assert len(dev_program) == 4 + len(circuit.device.preprocess(config)[0]) # currently 8 + assert len(dev_program) == 4 + len( + circuit.device.preprocess_transforms(config) + ) # currently 8 assert dev_program[-1].transform == qml.metric_tensor.transform full_program = get_transform_program(circuit)