From 46f999741ebc81053342a140ca27452555d178a5 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 13:24:43 +0800 Subject: [PATCH 01/13] Remove tape subclasses and old QNode --- doc/code/qml_tape.rst | 5 +- pennylane/__init__.py | 1 - pennylane/qnode_old.py | 1204 -------------------- pennylane/tape/__init__.py | 8 +- pennylane/tape/cv_param_shift.py | 548 --------- pennylane/tape/jacobian_tape.py | 885 --------------- pennylane/tape/qubit_param_shift.py | 501 --------- pennylane/tape/reversible.py | 278 ----- tests/tape/test_cv_param_shift.py | 899 --------------- tests/tape/test_jacobian_tape.py | 878 --------------- tests/tape/test_qnode_old.py | 1541 -------------------------- tests/tape/test_qubit_param_shift.py | 776 ------------- tests/tape/test_reversible.py | 545 --------- 13 files changed, 4 insertions(+), 8065 deletions(-) delete mode 100644 pennylane/qnode_old.py delete mode 100644 pennylane/tape/cv_param_shift.py delete mode 100644 pennylane/tape/jacobian_tape.py delete mode 100644 pennylane/tape/qubit_param_shift.py delete mode 100644 pennylane/tape/reversible.py delete mode 100644 tests/tape/test_cv_param_shift.py delete mode 100644 tests/tape/test_jacobian_tape.py delete mode 100644 tests/tape/test_qnode_old.py delete mode 100644 tests/tape/test_qubit_param_shift.py delete mode 100644 tests/tape/test_reversible.py diff --git a/doc/code/qml_tape.rst b/doc/code/qml_tape.rst index 468542378e5..0b971cff87e 100644 --- a/doc/code/qml_tape.rst +++ b/doc/code/qml_tape.rst @@ -1,11 +1,10 @@ qml.tape ======== -Quantum tapes are a datastructure that can represent quantum circuits and measurement statistics in PennyLane. They are queuing contexts that can record quantum operations, execute devices, and compute gradients. +Quantum tapes are a datastructure that can represent quantum circuits and measurement statistics in PennyLane. They are queuing contexts that can record and process quantum operations and measurements. In addition to being created internally by QNodes, quantum tapes can also be created, -nested, expanded (via :meth:`~.QuantumTape.expand`), and executed manually. Tape subclasses also provide -additional gradient methods: +nested, expanded (via :meth:`~.QuantumTape.expand`), and executed manually. Finally, quantum tapes are fully compatible with autodifferentiating via NumPy/Autograd, TensorFlow, and PyTorch. diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 8515fe7dbe0..3ce57925958 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -51,7 +51,6 @@ from pennylane.templates.subroutines import * from pennylane import qaoa from pennylane.qnode import QNode, qnode -import pennylane.qnode_old from pennylane.transforms import ( adjoint, adjoint_metric_tensor, diff --git a/pennylane/qnode_old.py b/pennylane/qnode_old.py deleted file mode 100644 index 64a7755e597..00000000000 --- a/pennylane/qnode_old.py +++ /dev/null @@ -1,1204 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the QNode class and qnode decorator. -""" -# pylint: disable=import-outside-toplevel -# pylint:disable=too-many-branches -from collections.abc import Sequence -from functools import lru_cache, update_wrapper -import warnings -import inspect - -import numpy as np - -import pennylane as qml -from pennylane import Device - -from pennylane.operation import State - -from pennylane.interfaces.autograd import AutogradInterface, np as anp -from pennylane.tape import ( - JacobianTape, - QubitParamShiftTape, - CVParamShiftTape, - ReversibleTape, -) - - -class QNode: - """Represents a quantum node in the hybrid computational graph. - - .. warning:: - - This QNode is deprecated and due to be removed in an upcoming - release. Please use :class:`qml.QNode <.pennylane.QNode>` instead. - - A *quantum node* contains a :ref:`quantum function ` - (corresponding to a :ref:`variational circuit `) - and the computational device it is executed on. - - The QNode calls the quantum function to construct a :class:`~.QuantumTape` instance representing - the quantum circuit. - - Args: - func (callable): a quantum function - device (~.Device): a PennyLane-compatible device - interface (str): The interface that will be used for classical backpropagation. - This affects the types of objects that can be passed to/returned from the QNode: - - * ``"autograd"``: Allows autograd to backpropagate - through the QNode. The QNode accepts default Python types - (floats, ints, lists) as well as NumPy array arguments, - and returns NumPy arrays. - - * ``"jax"``: Allows JAX to backpropogate through the QNode. - The QNode accepts and returns a single expectation value or variance. - - * ``"torch"``: Allows PyTorch to backpropogate - through the QNode. The QNode accepts and returns Torch tensors. - - * ``"tf"``: Allows TensorFlow in eager mode to backpropogate - through the QNode. The QNode accepts and returns - TensorFlow ``tf.Variable`` and ``tf.tensor`` objects. - - * ``None``: The QNode accepts default Python types - (floats, ints, lists) as well as NumPy array arguments, - and returns NumPy arrays. It does not connect to any - machine learning library automatically for backpropagation. - - diff_method (str): the method of differentiation to use in the created QNode - - * ``"best"``: Best available method. Uses classical backpropagation or the - device directly to compute the gradient if supported, otherwise will use - the analytic parameter-shift rule where possible with finite-difference as a fallback. - - * ``"device"``: Queries the device directly for the gradient. - Only allowed on devices that provide their own gradient computation. - - * ``"backprop"``: Use classical backpropagation. Only allowed on simulator - devices that are classically end-to-end differentiable, for example - :class:`default.tensor.tf <~.DefaultTensorTF>`. Note that the returned - QNode can only be used with the machine-learning framework supported - by the device. - - * ``"reversible"``: Uses a reversible method for computing the gradient. - This method is similar to ``"backprop"``, but trades off increased - runtime with significantly lower memory usage. Compared to the - parameter-shift rule, the reversible method can be faster or slower, - depending on the density and location of parametrized gates in a circuit. - Only allowed on (simulator) devices with the "reversible" capability, - for example :class:`default.qubit <~.DefaultQubit>`. - - * ``"adjoint"``: Uses an `adjoint method `__ that - reverses through the circuit after a forward pass by iteratively applying the inverse - (adjoint) gate. This method is similar to the reversible method, but has a lower time - overhead and a similar memory overhead. Only allowed on simulator devices such as - :class:`default.qubit <~.DefaultQubit>`. - - * ``"parameter-shift"``: Use the analytic parameter-shift - rule for all supported quantum operation arguments, with finite-difference - as a fallback. - - * ``"finite-diff"``: Uses numerical finite-differences for all quantum operation - arguments. - - * ``None``: QNode cannot be differentiated. Works the same as ``interface=None``. - - mutable (bool): If True, the underlying quantum circuit is re-constructed with - every evaluation. This is the recommended approach, as it allows the underlying - quantum structure to depend on (potentially trainable) QNode input arguments, - however may add some overhead at evaluation time. If this is set to False, the - quantum structure will only be constructed on the *first* evaluation of the QNode, - and is stored and re-used for further quantum evaluations. Only set this to False - if it is known that the underlying quantum structure is **independent of QNode input**. - max_expansion (int): The number of times the internal circuit should be expanded when - executed on a device. Expansion occurs when an operation or measurement is not - supported, and results in a gate decomposition. If any operations in the decomposition - remain unsupported by the device, another expansion occurs. - h (float): step size for the finite difference method - order (int): The order of the finite difference method to use. ``1`` corresponds - to forward finite differences, ``2`` to centered finite differences. - shift (float): the size of the shift for two-term parameter-shift gradient computations - adjoint_cache (bool): For TensorFlow and PyTorch interfaces and adjoint differentiation, - this indicates whether to save the device state after the forward pass. Doing so saves a - forward execution. Device state automatically reused with autograd and JAX interfaces. - argnum (int, list(int), None): Which argument(s) to compute the Jacobian - with respect to. When there are fewer parameters specified than the - total number of trainable parameters, the jacobian is being estimated. Note - that this option is only applicable for the following differentiation methods: - ``"parameter-shift"``, ``"finite-diff"`` and ``"reversible"``. - kwargs: used to catch all unrecognized keyword arguments and provide a user warning - about them - - **Example** - - >>> def circuit(x): - ... qml.RX(x, wires=0) - ... return expval(qml.PauliZ(0)) - >>> dev = qml.device("default.qubit", wires=1) - >>> qnode = qml.QNode(circuit, dev) - """ - - # pylint:disable=too-many-instance-attributes,too-many-arguments - - def __init__( - self, - func, - device, - interface="autograd", - diff_method="best", - mutable=True, - max_expansion=10, - h=1e-7, - order=1, - shift=np.pi / 2, - adjoint_cache=True, - argnum=None, - **kwargs, - ): - warnings.warn( - "qml.qnode_old.QNode is deprecated, and will be removed in an " - "upcoming release. Please use qml.QNode instead.", - UserWarning, - ) - - if diff_method is None: - # TODO: update this behaviour once the new differentiable pipeline is the default - # We set "best" to allow internals to work and leverage the interface = None - # feature to restrict differentiability - diff_method = "best" - interface = None - - if interface is not None and interface not in self.INTERFACE_MAP: - raise qml.QuantumFunctionError( - f"Unknown interface {interface}. Interface must be " - f"one of {list(self.INTERFACE_MAP.keys())}." - ) - - if not isinstance(device, Device): - raise qml.QuantumFunctionError( - "Invalid device. Device must be a valid PennyLane device." - ) - - if "shots" in inspect.signature(func).parameters: - warnings.warn( - "Detected 'shots' as an argument to the given quantum function. " - "The 'shots' argument name is reserved for overriding the number of shots " - "taken by the device. Its use outside of this context should be avoided.", - UserWarning, - ) - self._qfunc_uses_shots_arg = True - else: - self._qfunc_uses_shots_arg = False - - if kwargs: - for key in kwargs: - warnings.warn( - f"'{key}' is unrecognized, and will not be included in your computation. " - "Please review the QNode class or qnode decorator for the list of available " - "keyword variables.", - UserWarning, - ) - - diff_options = { - "h": h, - "order": order, - "shift": shift, - "adjoint_cache": adjoint_cache, - "argnum": argnum, - } - - self.mutable = mutable - self.func = func - self._original_device = device - self.qtape = None - self.qfunc_output = None - # store the user-specified differentiation method - self.diff_method = diff_method - self.diff_method_change = False - - self._tape, self.interface, self.device, tape_diff_options = self.get_tape( - device, interface, diff_method - ) - # if diff_method is best, then set it to the actual diff method being used - if self.diff_method == "best": - self.diff_method_change = True - self.diff_method = self._get_best_diff_method(tape_diff_options) - - # The arguments to be passed to JacobianTape.jacobian - self.diff_options = diff_options - self.diff_options.update(tape_diff_options) - - self.dtype = np.float64 - self.max_expansion = max_expansion - - def __repr__(self): - """String representation.""" - detail = "" - return detail.format( - self.device.num_wires, - self.device.short_name, - self.interface, - self.diff_method, - ) - - @staticmethod - def _get_best_diff_method(tape_diff_options): - """Update diff_method to reflect which method has been selected""" - if tape_diff_options["method"] == "device": - method = "device" - elif tape_diff_options["method"] == "backprop": - method = "backprop" - elif tape_diff_options["method"] == "best": - method = "parameter-shift" - elif tape_diff_options["method"] == "numeric": - method = "finite-diff" - return method - - # pylint: disable=too-many-return-statements - @staticmethod - def get_tape(device, interface, diff_method="best"): - """Determine the best JacobianTape, differentiation method, interface, and device - for a requested device, interface, and diff method. - - Args: - device (.Device): PennyLane device - interface (str): name of the requested interface - diff_method (str): The requested method of differentiation. One of - ``"best"``, ``"backprop"``, ``"reversible"``, ``"adjoint"``, ``"device"``, - ``"parameter-shift"``, or ``"finite-diff"``. - - Returns: - tuple[.JacobianTape, str, .Device, dict[str, str]]: Tuple containing the compatible - JacobianTape, the interface to apply, the device to use, and the method argument - to pass to the ``JacobianTape.jacobian`` method. - """ - - if diff_method == "best": - return QNode.get_best_method(device, interface) - - if diff_method == "backprop": - return QNode._validate_backprop_method(device, interface) - - if diff_method == "reversible": - return QNode._validate_reversible_method(device, interface) - - if diff_method == "adjoint": - return QNode._validate_adjoint_method(device, interface) - - if diff_method == "device": - return QNode._validate_device_method(device, interface) - - if diff_method == "parameter-shift": - return ( - QNode._get_parameter_shift_tape(device), - interface, - device, - {"method": "analytic"}, - ) - - if diff_method == "finite-diff": - return JacobianTape, interface, device, {"method": "numeric"} - - raise qml.QuantumFunctionError( - f"Differentiation method {diff_method} not recognized. Allowed " - "options are ('best', 'parameter-shift', 'backprop', 'finite-diff', 'device', 'reversible', 'adjoint')." - ) - - @staticmethod - def get_best_method(device, interface): - """Returns the 'best' JacobianTape and differentiation method - for a particular device and interface combination. - - This method attempts to determine support for differentiation - methods using the following order: - - * ``"device"`` - * ``"backprop"`` - * ``"parameter-shift"`` - * ``"finite-diff"`` - - The first differentiation method that is supported (going from - top to bottom) will be returned. - - Args: - device (.Device): PennyLane device - interface (str): name of the requested interface - - Returns: - tuple[.JacobianTape, str, .Device, dict[str, str]]: Tuple containing the compatible - JacobianTape, the interface to apply, the device to use, and the method argument - to pass to the ``JacobianTape.jacobian`` method. - """ - try: - return QNode._validate_device_method(device, interface) - except qml.QuantumFunctionError: - try: - return QNode._validate_backprop_method(device, interface) - except qml.QuantumFunctionError: - try: - return ( - QNode._get_parameter_shift_tape(device), - interface, - device, - {"method": "best"}, - ) - except qml.QuantumFunctionError: - return JacobianTape, interface, device, {"method": "numeric"} - - @staticmethod - def _validate_backprop_method(device, interface): - """Validates whether a particular device and JacobianTape interface - supports the ``"backprop"`` differentiation method. - - Args: - device (.Device): PennyLane device - interface (str): name of the requested interface - - Returns: - tuple[.JacobianTape, str, .Device, dict[str, str]]: Tuple containing the compatible - JacobianTape, the interface to apply, the device to use, and the method argument - to pass to the ``JacobianTape.jacobian`` method. - - Raises: - qml.QuantumFunctionError: if the device does not support backpropagation, or the - interface provided is not compatible with the device - """ - if device.shots is not None: - raise qml.QuantumFunctionError( - "Devices with finite shots are incompatible with backpropogation. " - "Please set shots=None or choose a different diff_method." - ) - - # determine if the device supports backpropagation - backprop_interface = device.capabilities().get("passthru_interface", None) - - # determine if the device has any child devices that support backpropagation - backprop_devices = device.capabilities().get("passthru_devices", None) - - if getattr(device, "cache", 0): - raise qml.QuantumFunctionError( - "Device caching is incompatible with the backprop diff_method" - ) - - if backprop_interface is not None: - # device supports backpropagation natively - - if interface == backprop_interface: - return JacobianTape, interface, device, {"method": "backprop"} - - raise qml.QuantumFunctionError( - f"Device {device.short_name} only supports diff_method='backprop' when using the " - f"{backprop_interface} interface." - ) - - if device.shots is None and backprop_devices is not None: - - # device is analytic and has child devices that support backpropagation natively - - if interface in backprop_devices: - # TODO: need a better way of passing existing device init options - # to a new device? - device = qml.device( - backprop_devices[interface], - wires=device.wires, - shots=device.shots, - ) - return JacobianTape, interface, device, {"method": "backprop"} - - raise qml.QuantumFunctionError( - f"Device {device.short_name} only supports diff_method='backprop' when using the " - f"{list(backprop_devices.keys())} interfaces." - ) - - raise qml.QuantumFunctionError( - f"The {device.short_name} device does not support native computations with " - "autodifferentiation frameworks." - ) - - @staticmethod - def _validate_reversible_method(device, interface): - """Validates whether a particular device and JacobianTape interface - supports the ``"reversible"`` differentiation method. - - Args: - device (.Device): PennyLane device - interface (str): name of the requested interface - - Returns: - tuple[.JacobianTape, str, .Device, dict[str, str]]: Tuple containing the compatible - JacobianTape, the interface to apply, the device to use, and the method argument - to pass to the ``JacobianTape.jacobian`` method. - - Raises: - qml.QuantumFunctionError: if the device does not support reversible backprop - """ - # TODO: update when all capabilities keys changed to "supports_reversible_diff" - supports_reverse = device.capabilities().get("supports_reversible_diff", False) - supports_reverse = supports_reverse or device.capabilities().get("reversible_diff", False) - - if not supports_reverse: - raise ValueError( - f"The {device.short_name} device does not support reversible differentiation." - ) - - if device.shots is not None: - warnings.warn( - "Requested reversible differentiation to be computed with finite shots." - " Reversible differentiation always calculated exactly.", - UserWarning, - ) - - return ReversibleTape, interface, device, {"method": "analytic"} - - @staticmethod - def _validate_adjoint_method(device, interface): - """Validates whether a particular device and JacobianTape interface - supports the ``"adjoint"`` differentiation method. - - Args: - device (.Device): PennyLane device - interface (str): name of the requested interface - - Returns: - tuple[.JacobianTape, str, .Device, dict[str, str]]: Tuple containing the compatible - JacobianTape, the interface to apply, the device to use, and the method argument - to pass to the ``JacobianTape.jacobian`` method. - - Raises: - qml.QuantumFunctionError: if the device does not support adjoint backprop - """ - supported_device = hasattr(device, "_apply_operation") - supported_device = supported_device and hasattr(device, "_apply_unitary") - supported_device = supported_device and device.capabilities().get("returns_state") - supported_device = supported_device and hasattr(device, "adjoint_jacobian") - # The above provides a minimal set of requirements that we can likely improve upon in - # future, or alternatively summarize within a single device capability. Moreover, we also - # need to inspect the circuit measurements to ensure only expectation values are taken. This - # cannot be done here since we don't yet know the composition of the circuit. - - if not supported_device: - raise ValueError( - f"The {device.short_name} device does not support adjoint differentiation." - ) - - if device.shots is not None: - warnings.warn( - "Requested adjoint differentiation to be computed with finite shots." - " Adjoint differentiation always calculated exactly.", - UserWarning, - ) - - jac_options = {"method": "device", "jacobian_method": "adjoint_jacobian"} - # reuse the forward pass - # torch and tensorflow can cache the state - if interface in {"autograd", "jax"}: - jac_options["device_pd_options"] = {"use_device_state": True} - - return ( - JacobianTape, - interface, - device, - jac_options, - ) - - @staticmethod - def _validate_device_method(device, interface): - """Validates whether a particular device and JacobianTape interface - supports the ``"device"`` differentiation method. - - Args: - device (.Device): PennyLane device - interface (str): name of the requested interface - - Returns: - tuple[.JacobianTape, str, .Device, dict[str, str]]: Tuple containing the compatible - JacobianTape, the interface to apply, the device to use, and the method argument - to pass to the ``JacobianTape.jacobian`` method. - - Raises: - qml.QuantumFunctionError: if the device does not provide a native method for computing - the Jacobian - """ - # determine if the device provides its own jacobian method - provides_jacobian = device.capabilities().get("provides_jacobian", False) - - if not provides_jacobian: - raise qml.QuantumFunctionError( - f"The {device.short_name} device does not provide a native " - "method for computing the jacobian." - ) - - return JacobianTape, interface, device, {"method": "device"} - - @staticmethod - def _get_parameter_shift_tape(device): - """Validates whether a particular device - supports the parameter-shift differentiation method, and returns - the correct tape. - - Args: - device (.Device): PennyLane device - - Returns: - .JacobianTape: the compatible JacobianTape - - Raises: - qml.QuantumFunctionError: if the device model does not have a corresponding - parameter-shift rule - """ - # determine if the device provides its own jacobian method - model = device.capabilities().get("model", None) - - if model == "qubit": - return QubitParamShiftTape - - if model == "cv": - return CVParamShiftTape - - raise qml.QuantumFunctionError( - f"Device {device.short_name} uses an unknown model ('{model}') " - "that does not support the parameter-shift rule." - ) - - def construct(self, args, kwargs): - """Call the quantum function with a tape context, ensuring the operations get queued.""" - - if self.interface == "autograd": - # HOTFIX: to maintain compatibility with core, here we treat - # all inputs that do not explicitly specify `requires_grad=False` - # as trainable. This should be removed at some point, forcing users - # to specify `requires_grad=True` for trainable parameters. - args = [ - anp.array(a, requires_grad=True) if not hasattr(a, "requires_grad") else a - for a in args - ] - - self.qtape = self._tape() - - with self.qtape: - self.qfunc_output = self.func(*args, **kwargs) - - if not isinstance(self.qfunc_output, Sequence): - measurement_processes = (self.qfunc_output,) - else: - measurement_processes = self.qfunc_output - - if not all( - isinstance(m, qml.measurements.MeasurementProcess) for m in measurement_processes - ): - raise qml.QuantumFunctionError( - "A quantum function must return either a single measurement, " - "or a nonempty sequence of measurements." - ) - - state_returns = any(m.return_type is State for m in measurement_processes) - - # apply the interface (if any) - - explicit_backprop = self.diff_options["method"] == "backprop" - best_and_passthru = ( - self.diff_options["method"] == "best" - and "passthru_interface" in self.device.capabilities() - ) - backprop_diff = explicit_backprop or best_and_passthru - if not backprop_diff and self.interface is not None: - # pylint: disable=protected-access - if state_returns and self.interface in ["torch", "tf"]: - # The state is complex and we need to indicate this in the to_torch or to_tf - # functions - self.INTERFACE_MAP[self.interface](self, dtype=np.complex128) - else: - self.INTERFACE_MAP[self.interface](self) - - if not all(ret == m for ret, m in zip(measurement_processes, self.qtape.measurements)): - raise qml.QuantumFunctionError( - "All measurements must be returned in the order they are measured." - ) - - for obj in self.qtape.operations + self.qtape.observables: - - if getattr(obj, "num_wires", None) is qml.operation.WiresEnum.AllWires: - # check here only if enough wires - if len(obj.wires) != self.device.num_wires: - raise qml.QuantumFunctionError(f"Operator {obj.name} must act on all wires") - - if ( - isinstance(obj, qml.ops.qubit.SparseHamiltonian) - and self.diff_method != "parameter-shift" - ): - raise qml.QuantumFunctionError( - "SparseHamiltonian observable must be used with the parameter-shift" - " differentiation method" - ) - - # pylint: disable=protected-access - obs_on_same_wire = len(self.qtape._obs_sharing_wires) > 0 - ops_not_supported = any( - isinstance(op, qml.tape.QuantumTape) # nested tapes must be expanded - or not self.device.supports_operation(op.name) # unsupported ops must be expanded - for op in self.qtape.operations - ) - - # expand out the tape, if nested tapes are present, any operations are not supported on the - # device, or multiple observables are measured on the same wire - if ops_not_supported or obs_on_same_wire: - self.qtape = self.qtape.expand( - depth=self.max_expansion, - stop_at=lambda obj: not isinstance(obj, qml.tape.QuantumTape) - and self.device.supports_operation(obj.name), - ) - - # provide the jacobian options - self.qtape.jacobian_options = self.diff_options - - if self.diff_options["method"] == "backprop": - params = self.qtape.get_parameters(trainable_only=False) - self.qtape.trainable_params = qml.math.get_trainable_indices(params) - - def __call__(self, *args, **kwargs): - - # If shots specified in call but not in qfunc signature, - # interpret it as device shots value for this call. - # TODO: make this more functional by passing shots as qtape.execute(.., shots=shots). - original_shots = -1 - if "shots" in kwargs and not self._qfunc_uses_shots_arg: - original_shots = self.device.shots # remember device shots - # remove shots from kwargs and temporarily change on device - self.device.shots = kwargs.pop("shots", None) - - if self.mutable or self.qtape is None: - # construct the tape - self.construct(args, kwargs) - - # Under certain conditions, split tape into multiple tapes and recombine them. - # Else just execute the tape, and let the device take care of things. - hamiltonian_in_obs = "Hamiltonian" in [obs.name for obs in self.qtape.observables] - # if the device does not support Hamiltonians, we split them - supports_hamiltonian = self.device.supports_observable("Hamiltonian") - # if the user wants a finite-shots computation we always split Hamiltonians - finite_shots = self.device.shots is not None - # if a grouping has been computed for all Hamiltonians we assume that they should be split - grouping_known = all( - obs.grouping_indices is not None - for obs in self.qtape.observables - if obs.name == "Hamiltonian" - ) - if hamiltonian_in_obs and ((not supports_hamiltonian or finite_shots) or grouping_known): - try: - tapes, fn = qml.transforms.hamiltonian_expand(self.qtape, group=False) - except ValueError as e: - raise ValueError( - "Can only return the expectation of a single Hamiltonian observable" - ) from e - results = [tape.execute(device=self.device) for tape in tapes] - res = fn(results) - else: - res = self.qtape.execute(device=self.device) - - finite_diff = any( - getattr(x["op"], "grad_method", None) == "F" for x in self.qtape._par_info.values() - ) - if finite_diff and self.diff_method_change: - self.diff_method = "finite-diff" - - # if shots was changed - if original_shots != -1: - # reinstate default on device - self.device.shots = original_shots - - # FIX: If the qnode swapped the device, increase the num_execution value on the original device. - # In the long run, we should make sure that the user's device is the one - # actually run so she has full control. This could be done by changing the class - # of the user's device before and after executing the tape. - if self.device is not self._original_device: - self._original_device._num_executions += 1 # pylint: disable=protected-access - - # Update for state vector simulators that have the _pre_rotated_state attribute - if hasattr(self._original_device, "_pre_rotated_state"): - self._original_device._pre_rotated_state = self.device._pre_rotated_state - - # Update for state vector simulators that have the _state attribute - if hasattr(self._original_device, "_state"): - self._original_device._state = self.device._state - - if isinstance(self.qfunc_output, Sequence) or ( - self.qtape.is_sampled and self.device._has_partitioned_shots() - ): - return res - - return qml.math.squeeze(res) - - def draw( - self, charset="unicode", wire_order=None, show_all_wires=False, max_length=None - ): # pylint: disable=unused-argument - """Draw the quantum tape as a circuit diagram. - - Args: - charset (str, optional): The charset that should be used. Currently, "unicode" and - "ascii" are supported. - wire_order (Sequence[Any]): The order (from top to bottom) to print the wires of the circuit. - If not provided, this defaults to the wire order of the device. - show_all_wires (bool): If True, all wires, including empty wires, are printed. - max_length (int, optional): Maximum string width (columns) when printing the circuit to the CLI. - - Raises: - ValueError: if the given charset is not supported - .QuantumFunctionError: drawing is impossible because the underlying - quantum tape has not yet been constructed - - Returns: - str: the circuit representation of the tape - - **Example** - - Consider the following circuit as an example: - - .. code-block:: python3 - - @qml.qnode(dev) - def circuit(a, w): - qml.Hadamard(0) - qml.CRX(a, wires=[0, 1]) - qml.Rot(*w, wires=[1]) - qml.CRX(-a, wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - We can draw the QNode after execution: - - >>> result = circuit(2.3, [1.2, 3.2, 0.7]) - >>> print(circuit.draw()) - 0: ──H──╭C────────────────────────────╭C─────────╭┤ ⟨Z ⊗ Z⟩ - 1: ─────╰RX(2.3)──Rot(1.2, 3.2, 0.7)──╰RX(-2.3)──╰┤ ⟨Z ⊗ Z⟩ - >>> print(circuit.draw(charset="ascii")) - 0: --H--+C----------------------------+C---------+| - 1: -----+RX(2.3)--Rot(1.2, 3.2, 0.7)--+RX(-2.3)--+| - - Circuit drawing works with devices with custom wire labels: - - .. code-block:: python3 - - dev = qml.device('default.qubit', wires=["a", -1, "q2"]) - - @qml.qnode(dev) - def circuit(): - qml.Hadamard(wires=-1) - qml.CNOT(wires=["a", "q2"]) - qml.RX(0.2, wires="a") - return qml.expval(qml.PauliX(wires="q2")) - - When printed, the wire order matches the order defined on the device: - - >>> print(circuit.draw()) - a: ─────╭C──RX(0.2)──┤ - -1: ──H──│────────────┤ - q2: ─────╰X───────────┤ ⟨X⟩ - - We can use the ``wire_order`` argument to change the wire order: - - >>> print(circuit.draw(wire_order=["q2", "a", -1])) - q2: ──╭X───────────┤ ⟨X⟩ - a: ──╰C──RX(0.2)──┤ - -1: ───H───────────┤ - """ - warnings.warn( - "The QNode.draw method has been deprecated. " - "Please use the qml.draw(qnode)(*args) function instead.", - UserWarning, - ) - - if self.qtape is None: - raise qml.QuantumFunctionError( - "The QNode can only be drawn after its quantum tape has been constructed." - ) - - wire_order = wire_order or self.device.wires - wire_order = qml.wires.Wires(wire_order) - - if show_all_wires and len(wire_order) < self.device.num_wires: - raise ValueError( - "When show_all_wires is enabled, the provided wire order must contain all wires on the device." - ) - - if not self.device.wires.contains_wires(wire_order): - raise ValueError( - f"Provided wire order {wire_order.labels} contains wires not contained on the device: {self.device.wires}." - ) - - return self.qtape.draw( - charset=charset, - wire_order=wire_order, - show_all_wires=show_all_wires, - max_length=max_length, - ) - - @property - def specs(self): - """Resource information about a quantum circuit. - - Returns: - dict[str, Union[defaultdict,int]]: dictionaries that contain QNode specifications - - **Example** - - .. code-block:: python3 - - dev = qml.device('default.qubit', wires=2) - @qml.qnode(dev) - def circuit(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=1) - qml.CNOT(wires=(0,1)) - return qml.probs(wires=(0,1)) - - x = np.array([0.1, 0.2]) - res = circuit(x) - - >>> circuit.specs - {'gate_sizes': defaultdict(int, {1: 2, 2: 1}), - 'gate_types': defaultdict(int, {'RX': 1, 'RY': 1, 'CNOT': 1}), - 'num_operations': 3, - 'num_observables': 1, - 'num_diagonalizing_gates': 0, - 'num_used_wires': 2, - 'depth': 2, - 'num_device_wires': 2, - 'device_name': 'default.qubit.autograd', - 'diff_method': 'backprop'} - - """ - if self.qtape is None: - raise qml.QuantumFunctionError( - "The QNode specifications can only be calculated after its quantum tape has been constructed." - ) - - info = self.qtape.specs.copy() - - info["num_device_wires"] = self.device.num_wires - info["device_name"] = self.device.short_name - info["diff_method"] = self.diff_method - - # tapes do not accurately track parameters for backprop - # TODO: calculate number of trainable parameters in backprop - # find better syntax for determining if backprop - if info["diff_method"] == "backprop": - del info["num_trainable_params"] - - return info - - def to_tf(self, dtype=None): - """Apply the TensorFlow interface to the internal quantum tape. - - Args: - dtype (tf.dtype): The dtype that the TensorFlow QNode should - output. If not provided, the default is ``tf.float64``. - - Raises: - .QuantumFunctionError: if TensorFlow >= 2.1 is not installed - """ - # pylint: disable=import-outside-toplevel - try: - import tensorflow as tf - from pennylane.interfaces.tf import TFInterface - - if self.interface != "tf" and self.interface is not None: - # Since the interface is changing, need to re-validate the tape class. - # if method was changed from "best", set it back to best - if self.diff_method_change: - diff_method = "best" - else: - diff_method = self.diff_method - self._tape, interface, self.device, diff_options = self.get_tape( - self._original_device, "tf", diff_method - ) - if self.diff_method_change: - self.diff_method = self._get_best_diff_method(diff_options) - self.interface = interface - self.diff_options.update(diff_options) - else: - self.interface = "tf" - - if not isinstance(self.dtype, tf.DType): - self.dtype = None - - self.dtype = dtype or self.dtype or TFInterface.dtype - - if self.qtape is not None: - TFInterface.apply(self.qtape, dtype=tf.as_dtype(self.dtype)) - - except ImportError as e: - raise qml.QuantumFunctionError( - "TensorFlow not found. Please install the latest " - "version of TensorFlow to enable the 'tf' interface." - ) from e - - def to_torch(self, dtype=None): - """Apply the Torch interface to the internal quantum tape. - - Args: - dtype (tf.dtype): The dtype that the Torch QNode should - output. If not provided, the default is ``torch.float64``. - - Raises: - .QuantumFunctionError: if PyTorch >= 1.3 is not installed - """ - # pylint: disable=import-outside-toplevel - try: - import torch - from pennylane.interfaces.torch import TorchInterface - - if self.interface != "torch" and self.interface is not None: - # Since the interface is changing, need to re-validate the tape class. - # if method was changed from "best", set it back to best - if self.diff_method_change: - diff_method = "best" - else: - diff_method = self.diff_method - self._tape, interface, self.device, diff_options = self.get_tape( - self._original_device, "torch", diff_method - ) - if self.diff_method_change: - self.diff_method = self._get_best_diff_method(diff_options) - self.interface = interface - self.diff_options.update(diff_options) - else: - self.interface = "torch" - - if not isinstance(self.dtype, torch.dtype): - self.dtype = None - - self.dtype = dtype or self.dtype or TorchInterface.dtype - - if self.dtype is np.complex128: - self.dtype = torch.complex128 - - if self.qtape is not None: - TorchInterface.apply(self.qtape, dtype=self.dtype) - - except ImportError as e: - raise qml.QuantumFunctionError( - "PyTorch not found. Please install the latest " - "version of PyTorch to enable the 'torch' interface." - ) from e - - def to_autograd(self): - """Apply the Autograd interface to the internal quantum tape.""" - self.dtype = AutogradInterface.dtype - - if self.interface != "autograd" and self.interface is not None: - # Since the interface is changing, need to re-validate the tape class. - # if method was changed from "best", set it back to best - if self.diff_method_change: - diff_method = "best" - else: - diff_method = self.diff_method - self._tape, interface, self.device, diff_options = self.get_tape( - self._original_device, "autograd", diff_method - ) - if self.diff_method_change: - self.diff_method = self._get_best_diff_method(diff_options) - self.interface = interface - self.diff_options.update(diff_options) - else: - self.interface = "autograd" - - if self.qtape is not None: - AutogradInterface.apply(self.qtape) - - def to_jax(self): - """Apply the JAX interface to the internal quantum tape. - - Args: - dtype (tf.dtype): The dtype that the JAX QNode should - output. If not provided, the default is ``jnp.float64``. - - Raises: - .QuantumFunctionError: if TensorFlow >= 2.1 is not installed - """ - # pylint: disable=import-outside-toplevel - try: - from pennylane.interfaces.jax import JAXInterface - - if self.interface != "jax" and self.interface is not None: - # Since the interface is changing, need to re-validate the tape class. - # if method was changed from "best", set it back to best - if self.diff_method_change: - diff_method = "best" - else: - diff_method = self.diff_method - self._tape, interface, self.device, diff_options = self.get_tape( - self._original_device, "jax", diff_method - ) - if self.diff_method_change: - self.diff_method = self._get_best_diff_method(diff_options) - self.interface = interface - self.diff_options.update(diff_options) - else: - self.interface = "jax" - - if self.qtape is not None: - JAXInterface.apply(self.qtape) - - except ImportError as e: - raise qml.QuantumFunctionError( - "JAX not found. Please install the latest " - "version of JAX to enable the 'jax' interface." - ) from e - - INTERFACE_MAP = { - "autograd": to_autograd, - "torch": to_torch, - "tf": to_tf, - "jax": to_jax, - } - - -# pylint:disable=too-many-arguments -def qnode( - device, - interface="autograd", - diff_method="best", - mutable=True, - max_expansion=10, - h=1e-7, - order=1, - shift=np.pi / 2, - adjoint_cache=True, - argnum=None, - **kwargs, -): - """Decorator for creating QNodes. - - This decorator is used to indicate to PennyLane that the decorated function contains a - :ref:`quantum variational circuit ` that should be bound to a - compatible device. - - The QNode calls the quantum function to construct a :class:`~.QuantumTape` instance representing - the quantum circuit. - - Args: - func (callable): a quantum function - device (~.Device): a PennyLane-compatible device - interface (str): The interface that will be used for classical backpropagation. - This affects the types of objects that can be passed to/returned from the QNode: - - * ``"autograd"``: Allows autograd to backpropogate - through the QNode. The QNode accepts default Python types - (floats, ints, lists) as well as NumPy array arguments, - and returns NumPy arrays. - - * ``"torch"``: Allows PyTorch to backpropogate - through the QNode. The QNode accepts and returns Torch tensors. - - * ``"tf"``: Allows TensorFlow in eager mode to backpropogate - through the QNode. The QNode accepts and returns - TensorFlow ``tf.Variable`` and ``tf.tensor`` objects. - - * ``None``: The QNode accepts default Python types - (floats, ints, lists) as well as NumPy array arguments, - and returns NumPy arrays. It does not connect to any - machine learning library automatically for backpropagation. - - diff_method (str): the method of differentiation to use in the created QNode. - - * ``"best"``: Best available method. Uses classical backpropagation or the - device directly to compute the gradient if supported, otherwise will use - the analytic parameter-shift rule where possible with finite-difference as a fallback. - - * ``"backprop"``: Use classical backpropagation. Only allowed on simulator - devices that are classically end-to-end differentiable, for example - :class:`default.tensor.tf <~.DefaultTensorTF>`. Note that the returned - QNode can only be used with the machine-learning framework supported - by the device; a separate ``interface`` argument should not be passed. - - * ``"reversible"``: Uses a reversible method for computing the gradient. - This method is similar to ``"backprop"``, but trades off increased - runtime with significantly lower memory usage. Compared to the - parameter-shift rule, the reversible method can be faster or slower, - depending on the density and location of parametrized gates in a circuit. - Only allowed on (simulator) devices with the "reversible" capability, - for example :class:`default.qubit <~.DefaultQubit>`. - - * ``"adjoint"``: Uses an adjoint `method `__ that - reverses through the circuit after a forward pass by iteratively applying the inverse - (adjoint) gate. This method is similar to the reversible method, but has a lower time - overhead and a similar memory overhead. Only allowed on simulator devices such as - :class:`default.qubit <~.DefaultQubit>`. - - * ``"device"``: Queries the device directly for the gradient. - Only allowed on devices that provide their own gradient rules. - - * ``"parameter-shift"``: Use the analytic parameter-shift - rule for all supported quantum operation arguments, with finite-difference - as a fallback. - - * ``"finite-diff"``: Uses numerical finite-differences for all quantum - operation arguments. - - mutable (bool): If True, the underlying quantum circuit is re-constructed with - every evaluation. This is the recommended approach, as it allows the underlying - quantum structure to depend on (potentially trainable) QNode input arguments, - however may add some overhead at evaluation time. If this is set to False, the - quantum structure will only be constructed on the *first* evaluation of the QNode, - and is stored and re-used for further quantum evaluations. Only set this to False - if it is known that the underlying quantum structure is **independent of QNode input**. - max_expansion (int): The number of times the internal circuit should be expanded when - executed on a device. Expansion occurs when an operation or measurement is not - supported, and results in a gate decomposition. If any operations in the decomposition - remain unsupported by the device, another expansion occurs. - h (float): step size for the finite difference method - order (int): The order of the finite difference method to use. ``1`` corresponds - to forward finite differences, ``2`` to centered finite differences. - shift (float): the size of the shift for two-term parameter-shift gradient computations - adjoint_cache (bool): For TensorFlow and PyTorch interfaces and adjoint differentiation, - this indicates whether to save the device state after the forward pass. Doing so saves a - forward execution. Device state automatically reused with autograd and JAX interfaces. - argnum (int, list(int), None): Which argument(s) to compute the Jacobian - with respect to. When there are fewer parameters specified than the - total number of trainable parameters, the jacobian is being estimated. Note - that this option is only applicable for the following differentiation methods: - ``"parameter-shift"``, ``"finite-diff"`` and ``"reversible"``. - kwargs: used to catch all unrecognized keyword arguments and provide a user warning - about them - - **Example** - - >>> dev = qml.device("default.qubit", wires=1) - >>> @qml.qnode(dev) - >>> def circuit(x): - >>> qml.RX(x, wires=0) - >>> return expval(qml.PauliZ(0)) - """ - - @lru_cache() - def qfunc_decorator(func): - """The actual decorator""" - qn = QNode( - func, - device, - interface=interface, - diff_method=diff_method, - mutable=mutable, - max_expansion=max_expansion, - h=h, - order=order, - shift=shift, - adjoint_cache=adjoint_cache, - argnum=argnum, - **kwargs, - ) - return update_wrapper(qn, func) - - return qfunc_decorator diff --git a/pennylane/tape/__init__.py b/pennylane/tape/__init__.py index 07e0561661d..7a4526020da 100644 --- a/pennylane/tape/__init__.py +++ b/pennylane/tape/__init__.py @@ -12,14 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This subpackage contains various quantum tapes, which track, queue, -validate, execute, and differentiate quantum circuits. +This subpackage contains the quantum tape, which tracks, queues, and +validates quantum operations and measurements. """ from .tape import QuantumTape, get_active_tape -from .jacobian_tape import JacobianTape -from .cv_param_shift import CVParamShiftTape -from .qubit_param_shift import QubitParamShiftTape -from .reversible import ReversibleTape from .operation_recorder import OperationRecorder from .stop_recording import stop_recording from .unwrap import Unwrap, UnwrapTape diff --git a/pennylane/tape/cv_param_shift.py b/pennylane/tape/cv_param_shift.py deleted file mode 100644 index 0794dd72066..00000000000 --- a/pennylane/tape/cv_param_shift.py +++ /dev/null @@ -1,548 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -CV parameter shift quantum tape. - -Provides analytic differentiation for variational circuits with parametrized Gaussian CV gates -and first- and second-order observables. -""" -# pylint: disable=attribute-defined-outside-init,too-many-branches,protected-access -import itertools -import warnings - -import numpy as np - -import pennylane as qml -from pennylane.measurements import MeasurementProcess -from pennylane.tape import QuantumTape - -from .qubit_param_shift import QubitParamShiftTape, _get_operation_recipe - - -class CVParamShiftTape(QubitParamShiftTape): - r"""Quantum tape for CV parameter-shift analytic differentiation method. - - .. warning:: - - The ``CVParamShiftTape`` is deprecated. - Please use a standard :class:`~.QuantumTape`, and apply gradient transforms using - the :mod:`.gradients` module to compute parameter-shift gradients. - - This class extends the :class:`~.jacobian` method of the quantum tape - to support analytic gradients of Gaussian CV operations using the parameter-shift rule. - This gradient method returns *exact* gradients, and can be computed directly - on quantum hardware. Simply pass ``method=analytic`` when computing the Jacobian: - - >>> tape.jacobian(dev, method="analytic") - - For more details on the quantum tape, please see :class:`~.JacobianTape`. - - This tape supports analytic gradients of photonic circuits that satisfy - the following constraints with regards to measurements: - - * Expectation values are restricted to observables that are first- and - second-order in :math:`\hat{x}` :math:`\hat{p}` only. - This includes :class:`~.X`, :class:`~.P`, :class:`~.QuadOperator`, - :class:`~.PolyXP`, and :class:`~.NumberOperator`. - - For second-order observables, the device **must support** :class:`~.PolyXP`. - - * Variances are restricted to observables that are first-order - in :math:`\hat{x}` :math:`\hat{p}` only. This includes :class:`~.X`, :class:`~.P`, - :class:`~.QuadOperator`, and *some* parameter values of :class:`~.PolyXP`. - - The device **must support** :class:`~.PolyXP`. - - Fock state probabilities (tapes that return :func:`~pennylane.probs` or - expectation values of :class:`~.FockStateProjector`) are not supported. - - In addition, the tape operations must fulfill the following requirements: - - * Only Gaussian operations are differentiable. - - * Non-differentiable Fock states and Fock operations may *precede* all differentiable Gaussian, - operations. For example, the following is permissible: - - .. code-block:: python - - with CVParamShiftTape() as tape: - # Non-differentiable Fock operations - qml.FockState(2, wires=0) - qml.Kerr(0.654, wires=1) - - # differentiable Gaussian operations - qml.Displacement(0.6, 0.5, wires=0) - qml.Beamsplitter(0.5, 0.1, wires=[0, 1]) - qml.expval(qml.NumberOperator(0)) - - tape.trainable_params = {2, 3, 4} - - * If a Fock operation succeeds a Gaussian operation, the Fock operation must - not contribute to any measurements. For example, the following is allowed: - - .. code-block:: python - - with CVParamShiftTape() as tape: - qml.Displacement(0.6, 0.5, wires=0) - qml.Beamsplitter(0.5, 0.1, wires=[0, 1]) - qml.Kerr(0.654, wires=1) # there is no measurement on wire 1 - qml.expval(qml.NumberOperator(0)) - - tape.trainable_params = {0, 1, 2} - - If any of the above constraints are not followed, the tape cannot be differentiated - via the CV parameter-shift rule. Please use numerical differentiation instead: - - >>> tape.jacobian(dev, method="numeric") - """ - - def _grad_method(self, idx, use_graph=True, default_method="A"): - op = self._par_info[idx]["op"] - - if op.grad_method in (None, "F"): - return op.grad_method - - if op.grad_method != "A": - raise ValueError(f"Operation {op} has unknown gradient method {op.grad_method}") - - if not use_graph: - raise ValueError( - "The CV parameter-shift rule must always use the " - "graph to determine operation gradient methods" - ) - - # Operation supports the CV parameter-shift rule. - # Create an empty list to store the 'best' partial derivative method - # for each observable - best = [] - - for m in self.measurements: - - if (m.return_type is qml.operation.Probability) or (m.obs.ev_order not in (1, 2)): - # Higher-order observables (including probability) only support finite differences. - best.append("F") - continue - - # get the set of operations betweens the operation and the observable - ops_between = self.graph.nodes_between(op, m.obs) - - if not ops_between: - # if there is no path between the operation and the observable, - # the operator has a zero gradient. - best.append("0") - continue - - # For parameter-shift compatible CV gates, we need to check both the - # intervening gates, and the type of the observable. - best_method = "A" - - if any(not k.supports_heisenberg for k in ops_between): - # non-Gaussian operators present in-between the operation - # and the observable. Must fallback to numeric differentiation. - best_method = "F" - - elif m.obs.ev_order == 2: - - if m.return_type is qml.operation.Expectation: - # If the observable is second order, we must use the second order - # CV parameter shift rule - best_method = "A2" - - elif m.return_type is qml.operation.Variance: - # we only support analytic variance gradients for - # first order observables - best_method = "F" - - best.append(best_method) - - if all(k == "0" for k in best): - # if the operation is independent of *all* observables - # in the circuit, the gradient will be 0 - return "0" - - if "F" in best: - # one non-analytic observable path makes the whole operation - # gradient method fallback to finite-difference - return "F" - - if "A2" in best: - # one second order observable makes the whole operation gradient - # require the second order parameter-shift rule - return "A2" - - return "A" - - @staticmethod - def _transform_observable(obs, Z, device_wires): - """Apply a Gaussian linear transformation to each index of an observable. - - Args: - obs (.Observable): observable to transform - Z (array[float]): Heisenberg picture representation of the linear transformation - device_wires (.Wires): wires on the device the transformed observable is to be - measured on - - Returns: - .Observable: the transformed observable - """ - # Get the Heisenberg representation of the observable - # in the position/momentum basis. The returned matrix/vector - # will have been expanded to act on the entire device. - if obs.ev_order > 2: - raise NotImplementedError("Transforming observables of order > 2 not implemented.") - - A = obs.heisenberg_obs(device_wires) - - if A.ndim != obs.ev_order: - raise ValueError( - "Mismatch between the polynomial order of observable and its Heisenberg representation" - ) - - # transform the observable by the linear transformation Z - A = A @ Z - - if A.ndim == 2: - A = A + A.T - - # TODO: if the A matrix corresponds to a known observable in PennyLane, - # for example qml.X, qml.P, qml.NumberOperator, we should return that - # instead. This will allow for greater device compatibility. - return qml.PolyXP(A, wires=device_wires, do_queue=False) - - def parameter_shift_first_order( - self, idx, params, **options - ): # pylint: disable=unused-argument - """Generate the tapes and postprocessing methods required to compute the gradient of a parameter using the - first order CV parameter-shift method. - - Args: - idx (int): trainable parameter index to differentiate with respect to - params (list[Any]): the quantum tape operation parameters - - Returns: - tuple[list[QuantumTape], function]: A tuple containing the list of generated tapes, - in addition to a post-processing function to be applied to the evaluated - tapes. - """ - - op, p_idx = self.get_operation(idx) - param_shift = _get_operation_recipe(op, p_idx, None) - shift = np.zeros_like(params) - - coeffs = [] - tapes = [] - for c, _a, s in zip(*param_shift): - - shift[idx] = s - - # shifted parameter values - shifted_tape = self.copy(copy_operations=True, tape_cls=QuantumTape) - shifted_tape.set_parameters(params + shift) - coeffs.append(c) - tapes.append(shifted_tape) - - def processing_fn(results): - """Computes the gradient of the parameter at index idx via the - first order CV parameter-shift method. - - Args: - results (list[real]): evaluated quantum tapes - - Returns: - array[float]: 1-dimensional array of length determined by the tape output - measurement statistics - """ - return np.dot(coeffs, results) - - return tapes, processing_fn - - def parameter_shift_second_order(self, idx, params, **options): - """Generate the tapes and postprocessing methods required to compute the gradient of a - parameter using the second order CV parameter-shift method. - - Args: - idx (int): trainable parameter index to differentiate with respect to - params (list[Any]): the quantum tape operation parameters - - Keyword Args: - dev_wires (.Wires): wires on the device the parameter-shift method is computed on - - Returns: - tuple[list[QuantumTape], function]: A tuple containing the list of generated tapes, - in addition to a post-processing function to be applied to the evaluated - tapes. - """ - - op, p_idx = self.get_operation(idx) - param_shift = _get_operation_recipe(op, p_idx, None) - - dev_wires = options["dev_wires"] - - if len(param_shift[0]) != 2: - # The 2nd order CV parameter-shift rule only accepts two-term shifts - raise NotImplementedError( - "Taking the analytic gradient for order-2 operators is " - "unsupported for {op} which contains a parameter with a " - "gradient recipe of more than two terms." - ) - - c1, c2 = param_shift[0] - a1, a2 = param_shift[1] - s1, s2 = param_shift[2] - - shift = np.zeros_like(params) - shift[idx] = s1 - - # evaluate transformed observables at the original parameter point - # first build the Heisenberg picture transformation matrix Z - self.set_parameters(a1 * params + shift) - Z2 = op.heisenberg_tr(dev_wires) - - shift[idx] = s2 - self.set_parameters(a2 * params + shift) - Z1 = op.heisenberg_tr(dev_wires) - - # derivative of the operation - Z = Z2 * c1 + Z1 * c2 - - self.set_parameters(params) - Z0 = op.heisenberg_tr(dev_wires, inverse=True) - Z = Z @ Z0 - - # conjugate Z with all the descendant operations - B = np.eye(1 + 2 * len(dev_wires)) - B_inv = B.copy() - - succ = self.graph.descendants_in_order((op,)) - operation_descendents = itertools.filterfalse(qml.circuit_graph._is_observable, succ) - observable_descendents = filter(qml.circuit_graph._is_observable, succ) - - for BB in operation_descendents: - if not BB.supports_heisenberg: - # if the descendant gate is non-Gaussian in parameter-shift differentiation - # mode, then there must be no observable following it. - continue - - B = BB.heisenberg_tr(dev_wires) @ B - B_inv = B_inv @ BB.heisenberg_tr(dev_wires, inverse=True) - - Z = B @ Z @ B_inv # conjugation - - tape = self.copy(copy_operations=True, tape_cls=QuantumTape) - - # change the observable - # TODO: if the transformation produces only a constant term, - # `_transform_observable` has only a single non-zero element in the - # 0th position, then there is no need to execute the device---the constant term - # represents the gradient. - - # transform the descendant observables into their derivatives using Z - transformed_obs_idx = [] - for obs in observable_descendents: - # get the index of the descendent observable - idx = self.observables.index(obs) - transformed_obs_idx.append(idx) - tape._measurements[idx] = MeasurementProcess( - qml.operation.Expectation, self._transform_observable(obs, Z, dev_wires) - ) - - tapes = [tape] - - def processing_fn(results): - """Computes the gradient of the parameter at index idx via the - second order CV parameter-shift method. - - Args: - results (list[real]): evaluated quantum tapes - - Returns: - array[float]: 1-dimensional array of length determined by the tape output - measurement statistics - """ - res = np.array(results)[0] - grad = np.zeros_like(res) - grad[transformed_obs_idx] = res[transformed_obs_idx] - return grad - - return tapes, processing_fn - - def parameter_shift(self, idx, params, **options): - r"""Partial derivative using the first- or second-order CV parameter-shift rule of a - tape consisting of *only* expectation values of observables. - - .. note:: - - The 2nd order method can handle also first order observables, but - 1st order method may be more efficient unless it's really easy to - experimentally measure arbitrary 2nd order observables. - - .. warning:: - - The 2nd order method can only be executed on devices that support the - :class:`~.PolyXP` observable. - - Args: - idx (int): trainable parameter index to differentiate with respect to - params (list[Any]): the quantum tape operation parameters - - Keyword Args: - force_order2 (bool): iff True, use the order-2 method even if not necessary - device (.Device): A PennyLane device that can execute quantum operations and return - measurement statistics. This keyword argument is required, as the device labels - may be needed to generate the quantum tapes for computing the gradient. - - Returns: - tuple[list[QuantumTape], function]: A tuple containing the list of generated tapes, - in addition to a post-processing function to be applied to the evaluated - tapes. - """ - device = options["device"] - options["dev_wires"] = device.wires - - t_idx = list(self.trainable_params)[idx] - grad_method = self._par_info[t_idx]["grad_method"] - - if options.get("force_order2", False) or grad_method == "A2": - - if "PolyXP" not in device.observables: - # If the device does not support PolyXP, must fallback - # to numeric differentiation. - warnings.warn( - f"The device {device.short_name} does not support " - "the PolyXP observable. The analytic parameter-shift cannot be used for " - "second-order observables; falling back to finite-differences.", - UserWarning, - ) - return self.numeric_pd(idx, params, **options) - - return self.parameter_shift_second_order(idx, params=params, **options) - - return self.parameter_shift_first_order(idx, params=params) - - def parameter_shift_var(self, idx, params, **options): - r"""Partial derivative using the first-order or second-order parameter-shift rule of a tape - consisting of a mixture of expectation values and variances of observables. - - Expectation values may be of first- or second-order observables, - but variances can only be taken of first-order variables. - - .. warning:: - - This method can only be executed on devices that support the - :class:`~.PolyXP` observable. - - Args: - idx (int): trainable parameter index to differentiate with respect to - params (list[Any]): the quantum tape operation parameters - - Keyword Args: - force_order2 (bool): iff True, use the order-2 method even if not necessary - device (.Device): A PennyLane device that can execute quantum operations and return - measurement statistics. This keyword argument is required, as the device labels - may be needed to generate the quantum tapes for computing the gradient. - - Returns: - array[float]: 1-dimensional array of length determined by the tape output - measurement statistics - """ - # pylint: disable=protected-access - device = options["device"] - - if "PolyXP" not in device.observables: - # If the device does not support PolyXP, must fallback - # to numeric differentiation. - warnings.warn( - f"The device {device.short_name} does not support " - "the PolyXP observable. The analytic parameter-shift cannot be used for " - "second-order observables; falling back to finite-differences.", - UserWarning, - ) - - return self.numeric_pd(idx, params, **options) - - tapes = [] - - # Get , the expectation value of the tape with unshifted parameters. - evA_tape = self.copy() - evA_tape.set_parameters(params) - - # Temporarily convert all variance measurements on the tape into expectation values - for i in self.var_idx: - obs = evA_tape._measurements[i].obs - evA_tape._measurements[i] = MeasurementProcess(qml.operation.Expectation, obs=obs) - - # evaluate the analytic derivative of - pdA_tapes, pdA_fn = evA_tape.parameter_shift_first_order(idx, params, **options) - tapes.extend(pdA_tapes) - - pdA2_tape = self.copy() - - for i in self.var_idx: - # We need to calculate d/dp; to do so, we replace the - # observables A in the queue with A^2. - obs = pdA2_tape._measurements[i].obs - - # CV first order observable - # get the heisenberg representation - # This will be a real 1D vector representing the - # first order observable in the basis [I, x, p] - A = obs._heisenberg_rep(obs.parameters) # pylint: disable=protected-access - - # take the outer product of the heisenberg representation - # with itself, to get a square symmetric matrix representing - # the square of the observable - obs = qml.PolyXP(np.outer(A, A), wires=obs.wires, do_queue=False) - pdA2_tape._measurements[i] = MeasurementProcess(qml.operation.Expectation, obs=obs) - - # Here, we calculate the analytic derivatives of the observables. - pdA2_tapes, pdA2_fn = pdA2_tape.parameter_shift_second_order(idx, params, **options) - tapes.extend(pdA2_tapes) - - # Make sure that the expectation value of the tape with unshifted parameters - # is only calculated once, if `self._append_evA_tape` is True. - if self._append_evA_tape: - tapes.append(evA_tape) - - # Now that the tape has been appended, we want to avoid - # appending it for subsequent parameters, as the result can simply - # be re-used. - self._append_evA_tape = False - - def processing_fn(results): - """Computes the gradient of the parameter at index ``idx`` via the - second order CV parameter-shift method for a circuit containing a mixture - of expectation values and variances. - - Args: - results (list[real]): evaluated quantum tapes - - Returns: - array[float]: 1-dimensional array of length determined by the tape output - measurement statistics - """ - pdA = pdA_fn(results[0:2]) - pdA2 = pdA2_fn(results[2:4]) - - # Check if the expectation value of the tape with unshifted parameters - # has already been calculated. - if self._evA_result is None: - # The expectation value hasn't been previously calculated; - # it will be the last element of the `results` argument. - self._evA_result = np.array(results[-1]) - - # return d(var(A))/dp = d/dp -2 * * d/dp for the variances, - # d/dp for plain expectations - return np.where(self.var_mask, pdA2 - 2 * self._evA_result * pdA, pdA) - - return tapes, processing_fn diff --git a/pennylane/tape/jacobian_tape.py b/pennylane/tape/jacobian_tape.py deleted file mode 100644 index 3ce99ef2d01..00000000000 --- a/pennylane/tape/jacobian_tape.py +++ /dev/null @@ -1,885 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the ``JacobianTape``, which adds differentiation methods -to the ``QuantumTape`` class. -""" -# pylint: disable=too-many-branches - -import itertools -import warnings - -import numpy as np - -import pennylane as qml -from pennylane.operation import State -from pennylane.tape import QuantumTape - -# CV ops still need to support state preparation operations prior to any -# other operation for PennyLane-SF tests to pass. -STATE_PREP_OPS = ( - qml.BasisState, - qml.QubitStateVector, - # qml.CatState, - # qml.CoherentState, - # qml.FockDensityMatrix, - # qml.DisplacedSqueezedState, - # qml.FockState, - # qml.FockStateVector, - # qml.ThermalState, - # qml.GaussianState, -) - - -# pylint: disable=too-many-public-methods -class JacobianTape(QuantumTape): - """A quantum tape recorder, that records, validates, executes, - and differentiates variational quantum programs. - - .. note:: - - See :mod:`pennylane.tape` for more details. - - .. warning:: - - The ``JacobianTape`` as well as the ``JacobianTape.jacobian()`` method is deprecated. - Please use a standard :class:`~.QuantumTape`, and apply gradient transforms using - the :mod:`.gradients` module to compute Jacobians. - - Args: - name (str): a name given to the quantum tape - do_queue (bool): Whether to queue this tape in a parent tape context. - - **Example** - - In addition to the functionality of the ``QuantumTape`` class, - the ``JacobianTape`` has the ability to compute Jacobians of a quantum - circuit. Consider the following - - .. code-block:: python - - import pennylane.tape - - with qml.tape.JacobianTape() as tape: - qml.RX(0.432, wires=0) - qml.RY(0.543, wires=0) - qml.CNOT(wires=[0, 'a']) - qml.RX(0.133, wires='a') - qml.expval(qml.PauliZ(wires=[0])) - - The Jacobian is computed using finite difference: - - >>> dev = qml.device('default.qubit', wires=[0, 'a']) - >>> tape.jacobian(dev) - [[-0.35846484 -0.46923704 0. ]] - >>> tape.jacobian(dev, params=[0.1, 0.1, 0.1]) - [[-0.09933471 -0.09933471 0. ]] - - Trainable parameters are taken into account when calculating the Jacobian, - avoiding unnecessary calculations: - - >>> tape.trainable_params = {0} # set only the first parameter as free - >>> tape.set_parameters([0.56]) - >>> tape.jacobian(dev) - [[-0.45478169]] - """ - - def __init__(self, name=None, do_queue=True): - super().__init__(name=name, do_queue=do_queue) - self.jacobian_options = {} - self.hessian_options = {} - - def copy(self, copy_operations=False, tape_cls=None): - copied_tape = super().copy(copy_operations=copy_operations, tape_cls=tape_cls) - copied_tape.jacobian_options = self.jacobian_options - return copied_tape - - def _grad_method(self, idx, use_graph=True, default_method="F"): - """Determine the correct partial derivative computation method for each gate parameter. - - Parameter gradient methods include: - - * ``None``: the parameter does not support differentiation. - - * ``"0"``: the variational circuit output does not depend on this - parameter (the partial derivative is zero). - - * ``"F"``: the parameter has a non-zero derivative that should be computed - using finite-differences. - - * ``"A"``: the parameter has a non-zero derivative that should be computed - using an analytic method. - - .. note:: - - The base ``JacobianTape`` class only supports numerical differentiation, so - this method will always return either ``"F"`` or ``None``. If an inheriting - tape supports analytic differentiation for certain operations, make sure - that this method is overwritten appropriately to return ``"A"`` where - required. - - Args: - idx (int): parameter index - use_graph: whether to use a directed-acyclic graph to determine - if the parameter has a gradient of 0 - default_method (str): the default differentiation value to return - for parameters that where the grad method is not ``"0"`` or ``None`` - - Returns: - str: partial derivative method to be used - """ - op = self._par_info[idx]["op"] - - if op.grad_method is None: - return None - - if (self._graph is not None) or use_graph: - # an empty list to store the 'best' partial derivative method - # for each observable - best = [] - - # loop over all observables - for ob in self.observables: - # check if op is an ancestor of ob - has_path = self.graph.has_path(op, ob) - - # Use finite differences if there is a path, else the gradient is zero - best.append(default_method if has_path else "0") - - if all(k == "0" for k in best): - return "0" - - return default_method - - def _update_gradient_info(self): - """Update the parameter information dictionary with gradient information - of each parameter""" - - for i, info in self._par_info.items(): - - if i not in self.trainable_params: - info["grad_method"] = None - else: - info["grad_method"] = self._grad_method(i, use_graph=True) - - def _grad_method_validation(self, method): - """Validates if the gradient method requested is supported by the trainable - parameters, and returns the allowed parameter gradient methods. - - This method will generate parameter gradient information if it has not already - been generated, and then proceed to validate the gradient method. In particular: - - * An exception will be raised if there exist non-differentiable trainable - parameters on the tape. - - * An exception will be raised if the Jacobian method is ``"analytic"`` but there - exist some trainable parameters on the tape that only support numeric differentiation. - - If all validations pass, this method will return a tuple containing the allowed parameter - gradient methods for each trainable parameter. - - Args: - method (str): the overall Jacobian differentiation method - - Returns: - tuple[str, None]: the allowed parameter gradient methods for each trainable parameter - """ - - if "grad_method" not in self._par_info[0]: - self._update_gradient_info() - - diff_methods = { - idx: info["grad_method"] - for idx, info in self._par_info.items() - if idx in self.trainable_params - } - - # check and raise an error if any parameters are non-differentiable - nondiff_params = {idx for idx, g in diff_methods.items() if g is None} - - if nondiff_params: - raise ValueError(f"Cannot differentiate with respect to parameter(s) {nondiff_params}") - - numeric_params = {idx for idx, g in diff_methods.items() if g == "F"} - - # If explicitly using analytic mode, ensure that all parameters - # support analytic differentiation. - if method == "analytic" and numeric_params: - raise ValueError( - f"The analytic gradient method cannot be used with the argument(s) {numeric_params}." - ) - - return tuple(diff_methods.values()) - - @staticmethod - def _has_trainable_params(params, diff_methods): - """Determines if there are any trainable parameters. - - Args: - params (array[float]): one-dimensional array of parameters - diff_methods (Sequence[str]): The corresponding differentiation method for each parameter. - A differentiation method of ``"0"`` corresponds to a constant parameter. - """ - return params.size and not all(g == "0" for g in diff_methods) - - @staticmethod - def _flatten_processing_result(g): - """Flattens the output from processing_fn in parameter shift methods.""" - if hasattr(g, "dtype") and g.dtype is np.dtype("object"): - # - Object arrays cannot be flattened; must hstack them. - # - We also check that g has attribute dtype first to allow for - # Observables that return arbitrary objects. - g = np.hstack(g) - - if hasattr(g, "flatten"): - # flatten only if g supports flattening to allow for - # objects other than numpy ndarrays - return g.flatten() - return g - - def numeric_pd(self, idx, params=None, **options): - """Generate the tapes and postprocessing methods required to compute the gradient of a parameter using the - finite-difference method. - - Args: - idx (int): trainable parameter index to differentiate with respect to - params (list[Any]): The quantum tape operation parameters. If not provided, - the current tape parameter values are used (via :meth:`~.get_parameters`). - - Keyword Args: - h=1e-7 (float): finite difference method step size - order=1 (int): The order of the finite difference method to use. ``1`` corresponds - to forward finite differences, ``2`` to centered finite differences. - - Returns: - tuple[list[QuantumTape], function]: A tuple containing the list of generated tapes, - in addition to a post-processing function to be applied to the evaluated - tapes. - """ - - if params is None: - params = np.array(self.get_parameters()) - - order = options.get("order", 1) - h = options.get("h", 1e-7) - - shift = np.zeros_like(params, dtype=np.float64) - shift[idx] = h - - if order == 1: - # forward finite-difference. - - tapes = [] - - # get the stored result of the original circuit - y0 = options.get("y0", None) - - shifted = self.copy(copy_operations=True, tape_cls=QuantumTape) - shifted.set_parameters(params + shift) - - tapes.append(shifted) - - if y0 is None: - tapes.append(self) - - def processing_fn(results): - """Computes the gradient of the parameter at index idx via first-order - forward finite differences. - - Args: - results (list[real]): evaluated quantum tapes - - Returns: - array[float]: 1-dimensional array of length determined by the tape output - measurement statistics - """ - shifted = qml.math.to_numpy(results[0]) - unshifted = y0 - - if unshifted is None: - unshifted = np.array(results[1]) - - return (shifted - unshifted) / h - - return tapes, processing_fn - - if order == 2: - # central finite difference - - shifted_forward = self.copy(copy_operations=True, tape_cls=QuantumTape) - shifted_forward.set_parameters(params + shift / 2) - - shifted_backward = self.copy(copy_operations=True, tape_cls=QuantumTape) - shifted_backward.set_parameters(params - shift / 2) - - tapes = [shifted_forward, shifted_backward] - - def second_order_processing_fn(results): - """Computes the gradient of the parameter at index idx via second-order - centered finite differences. - - Args: - results (list[real]): evaluated quantum tapes - - Returns: - array[float]: 1-dimensional array of length determined by the tape output - measurement statistics - """ - res0 = np.array(results[0]) - res1 = np.array(results[1]) - return (res0 - res1) / h - - return tapes, second_order_processing_fn - - raise ValueError("Order must be 1 or 2.") - - def device_pd(self, device, params=None, **options): - """Evaluate the gradient of the tape with respect to - all trainable tape parameters by querying the provided device. - - Args: - device (.Device, .QubitDevice): a PennyLane device - that can execute quantum operations and return measurement statistics - params (list[Any]): The quantum tape operation parameters. If not provided, - the current tape parameter values are used (via :meth:`~.get_parameters`). - """ - jacobian_method = getattr(device, options.get("jacobian_method", "jacobian")) - - if params is None: - params = np.array(self.get_parameters()) - - saved_parameters = self.get_parameters() - - # temporarily mutate the in-place parameters - self.set_parameters(params) - - # TODO: modify devices that have device Jacobian methods to - # accept the quantum tape as an argument - jac = jacobian_method(self, **options.get("device_pd_options", {})) - - # restore original parameters - self.set_parameters(saved_parameters) - return jac - - def analytic_pd(self, idx, params, **options): - """Generate the quantum tapes and classical post-processing function required to compute the - gradient of the tape with respect to a single trainable tape parameter using an analytic - method. - - Args: - idx (int): trainable parameter index to differentiate with respect to - params (list[Any]): the quantum tape operation parameters - - Returns: - tuple[list[QuantumTape], function]: A tuple containing the list of generated tapes, - in addition to a post-processing function to be applied to the evaluated - tapes. - """ - raise NotImplementedError - - def hessian_pd(self, i, j, params, **options): - """Generate the quantum tapes and classical post-processing function required to compute the - Hessian of the tape with respect to two trainable tape parameter using an analytic - method. - - Args: - i (int): trainable parameter index to differentiate with respect to - j (int): trainable parameter index to differentiate with respect to - params (list[Any]): the quantum tape operation parameters - - Returns: - tuple[list[QuantumTape], function]: A tuple containing the list of generated tapes, - in addition to a post-processing function to be applied to the evaluated - tapes. - """ - raise NotImplementedError - - @staticmethod - def _choose_params_with_methods(diff_methods, argnum): - """Chooses the trainable parameters to use for computing the Jacobian - by returning a map of their indices and differentiation methods. - - When there are fewer parameters specified than the total number of - trainable parameters, the Jacobian is estimated by using the parameters - specified using the ``argnum`` keyword argument. - - Args: - diff_methods (list): the ordered list of differentiation methods - for each parameter - argnum (int, list(int), None): Indices for which argument(s) to - compute the Jacobian with respect to. - - Returns: - enumerate or list: map of the trainable parameter indices and - differentiation methods - """ - if argnum is None: - return enumerate(diff_methods) - - if isinstance(argnum, int): - argnum = [argnum] - - num_params = len(argnum) - - if num_params == 0: - warnings.warn( - "No trainable parameters were specified for computing the Jacobian.", - UserWarning, - ) - return [] - - diff_methods_to_use = map(diff_methods.__getitem__, argnum) - return zip(argnum, diff_methods_to_use) - - def jacobian(self, device, params=None, **options): - r"""Compute the Jacobian of the parametrized quantum circuit recorded by the quantum tape. - - .. warning:: - - The ``JacobianTape`` as well as the ``JacobianTape.jacobian()`` method is deprecated. - Please use a standard :class:`~.QuantumTape`, and apply gradient transforms using - the :mod:`.gradients` module to compute Jacobians. - - The quantum tape can be interpreted as a simple :math:`\mathbb{R}^m \to \mathbb{R}^n` function, - mapping :math:`m` (trainable) gate parameters to :math:`n` measurement statistics, - such as expectation values or probabilities. - - By default, the Jacobian will be computed with respect to all parameters on the quantum tape. - This can be modified by setting the :attr:`~.trainable_params` attribute of the tape. - - The Jacobian can be computed using several methods: - - * Finite differences (``'numeric'``). The first-order method evaluates the circuit at - :math:`n+1` points of the parameter space, the second-order method at :math:`2n` points, - where ``n = tape.num_params``. - - * Analytic method (``'analytic'``). Analytic, if implemented by the inheriting quantum tape. - - * Best known method for each parameter (``'best'``): uses the analytic method if - possible, otherwise finite difference. - - * Device method (``'device'``): Delegates the computation of the Jacobian to the - device executing the circuit. Only supported by devices that provide their - own method for computing derivatives; support can be checked by - querying the device capabilities: ``dev.capabilities()['provides_jacobian']`` must - return ``True``. Examples of supported devices include the experimental - :class:`"default.tensor.tf" <~.DefaultTensorTF>` device. - - .. note:: - - The finite difference method is sensitive to statistical noise in the circuit output, - since it compares the output at two points infinitesimally close to each other. Hence - the ``'F'`` method works best with exact expectation values when using simulator - devices. - - Args: - device (.Device, .QubitDevice): a PennyLane device - that can execute quantum operations and return measurement statistics - params (list[Any]): The quantum tape operation parameters. If not provided, - the current tape parameter values are used (via :meth:`~.get_parameters`). - - Keyword Args: - method="best" (str): The differentiation method. Must be one of ``"numeric"``, - ``"analytic"``, ``"best"``, or ``"device"``. - h=1e-7 (float): finite difference method step size - order=1 (int): The order of the finite difference method to use. ``1`` corresponds - to forward finite differences, ``2`` to centered finite differences. - shift=pi/2 (float): the size of the shift for two-term parameter-shift gradient computations - argnum=None (int, list(int), None): Which argument(s) to compute the Jacobian - with respect to. When there are fewer parameters specified than the - total number of trainable parameters, the jacobian is being estimated. - - Returns: - array[float]: 2-dimensional array of shape ``(tape.output_dim, tape.num_params)`` - - **Example** - - .. code-block:: python - - with JacobianTape() as tape: - qml.RX(0.432, wires=0) - qml.RY(0.543, wires=0) - qml.CNOT(wires=[0, 'a']) - qml.RX(0.133, wires='a') - qml.probs(wires=[0, 'a']) - - If parameters are not provided, the existing tape parameters are used: - - >>> dev = qml.device("default.qubit", wires=[0, 'a']) - >>> tape.jacobian(dev) - array([[-0.178441 , -0.23358253, -0.05892804], - [-0.00079144, -0.00103601, 0.05892804], - [ 0.00079144, 0.00103601, 0.00737611], - [ 0.178441 , 0.23358253, -0.00737611]]) - - Parameters can be optionally passed during execution: - - >>> tape.jacobian(dev, params=[1.0, 0.0, 1.0]) - array([[-3.24029934e-01, -9.99200722e-09, -3.24029934e-01], - [-9.67055711e-02, -2.77555756e-09, 3.24029935e-01], - [ 9.67055709e-02, 3.05311332e-09, 9.67055709e-02], - [ 3.24029935e-01, 1.08246745e-08, -9.67055711e-02]]) - - Parameters provided for execution are temporary, and do not affect - the tapes' parameters in-place: - - >>> tape.get_parameters() - [0.432, 0.543, 0.133] - - Explicitly setting the trainable parameters can significantly reduce - computational resources, as non-trainable parameters are ignored - during the computation: - - >>> tape.trainable_params = {0} # set only the first parameter as trainable - >>> tape.jacobian(dev) - array([[-0.178441 ], - [-0.00079144], - [ 0.00079144], - [ 0.178441 ]]) - - If a tape has no trainable parameters, the Jacobian will be empty: - - >>> tape.trainable_params = {} - >>> tape.jacobian(dev) - array([], shape=(4, 0), dtype=float64) - """ - # pylint: disable=too-many-statements - - warnings.warn( - "Differentiating tapes using JacobianTape.jacobian() is deprecated. " - "Please use ta standard QuantumTape with gradient transforms from " - "the qml.gradients module instead." - ) - - if any(m.return_type is State for m in self.measurements): - raise ValueError("The jacobian method does not support circuits that return the state") - - if self.is_sampled: - raise qml.QuantumFunctionError( - "Circuits that include sampling can not be differentiated." - ) - - method = options.get("method", "best") - - if method not in ("best", "numeric", "analytic", "device"): - raise ValueError(f"Unknown gradient method '{method}'") - - if params is None: - params = self.get_parameters() - - params = np.array(params) - - if method == "device": - # Using device mode; simply query the device for the Jacobian - return self.device_pd(device, params=params, **options) - - # perform gradient method validation - diff_methods = self._grad_method_validation(method) - - if not self._has_trainable_params(params, diff_methods): - # Either all parameters have grad method 0, or there are no trainable - # parameters. Simply return an empty Jacobian. - return np.zeros((self.output_dim, len(params)), dtype=float) - - if method == "numeric" or "F" in diff_methods: - # there exist parameters that will be differentiated numerically - - if options.get("order", 1) == 1: - # First order (forward) finite-difference will be performed. - # Compute the value of the tape at the current parameters here. This ensures - # this computation is only performed once, for all parameters. - # convert to float64 to eliminate floating point errors when params float32 - params_f64 = np.array(params, dtype=np.float64) - options["y0"] = qml.math.to_numpy(self.execute_device(params_f64, device)) - - # some gradient methods need the device or the device wires - options["device"] = device - options["dev_wires"] = device.wires - - # collect all circuits (tapes) and postprocessing functions required - # to compute the jacobian - - all_tapes = [] - reshape_info = [] - processing_fns = [] - nonzero_grad_idx = [] - - argnum = options.get("argnum", None) - - params_with_methods = self._choose_params_with_methods(diff_methods, argnum) - - for trainable_idx, param_method in params_with_methods: - if param_method == "0": - continue - - nonzero_grad_idx.append(trainable_idx) - - t_idx = list(self.trainable_params)[trainable_idx] - op = self._par_info[t_idx]["op"] - - if op.name == "Hamiltonian": - # divert Hamiltonian differentiation to special recipe - tapes, processing_fn = qml.gradients.hamiltonian_grad( - self, trainable_idx, params=params - ) - - elif (method == "best" and param_method[0] == "F") or (method == "numeric"): - # numeric method - tapes, processing_fn = self.numeric_pd(trainable_idx, params=params, **options) - - elif (method == "best" and param_method[0] == "A") or (method == "analytic"): - # analytic method - tapes, processing_fn = self.analytic_pd(trainable_idx, params=params, **options) - - processing_fns.append(processing_fn) - - # we create a flat list here to feed at once to the device - all_tapes.extend(tapes) - - # to extract the correct result for this parameter later, remember the number of tapes - reshape_info.append(len(tapes)) - - # execute all tapes at once - results = device.batch_execute(all_tapes) - - # post-process the results with the appropriate function to fill jacobian columns with gradients - jac = None - start = 0 - - for i, processing_fn, res_len in zip(nonzero_grad_idx, processing_fns, reshape_info): - # extract the correct results from the flat list - res = results[start : start + res_len] - start += res_len - - # postprocess results to compute the gradient - g = self._flatten_processing_result(processing_fn(res)) - - if jac is None: - # update the tape's output dimension - try: - self._output_dim = len(g) - except TypeError: - # if g has no len (e.g., because it is not a numpy.ndarray) - # assume the dimension is 1 - self._output_dim = 1 - # create the Jacobian matrix with appropriate dtype - dtype = g.dtype if isinstance(g, (np.ndarray, float)) else np.object_ - jac = np.zeros((self._output_dim, len(params)), dtype=dtype) - - jac[:, i] = g - - return jac - - def hessian(self, device, params=None, **options): - r"""Compute the Hessian of the parametrized quantum circuit recorded by the quantum tape. - - .. warning:: - - The ``JacobianTape`` as well as the ``JacobianTape.hessian()`` method is deprecated. - Please use a standard :class:`~.QuantumTape`, and apply gradient transforms using - the :mod:`.gradients` module to compute Hessians. - - The quantum tape can be interpreted as a simple :math:`\mathbb{R}^m \to \mathbb{R}^n` function, - mapping :math:`m` (trainable) gate parameters to :math:`n` measurement statistics, - such as expectation values or probabilities. - - By default, the Hessian will be computed with respect to all parameters on the quantum tape. - This can be modified by setting the :attr:`~.trainable_params` attribute of the tape. - - The Hessian can be currently computed using only the ``'analytic'`` method. - - Args: - device (.Device, .QubitDevice): a PennyLane device - that can execute quantum operations and return measurement statistics - params (list[Any]): The quantum tape operation parameters. If not provided, - the current tape parameter values are used (via :meth:`~.get_parameters`). - - Keyword Args: - method="analytic" (str): The differentiation method. Currently only - supports ``"analytic"``. - s1=pi/2 (float): the size of the shift for index i in the parameter-shift Hessian computations - s2=pi/2 (float): the size of the shift for index j in the parameter-shift Hessian computations - - Returns: - array[float]: 2-dimensional array of shape ``(tape.num_params, tape.num_params)`` - - **Example** - - .. code-block:: python - - n_wires = 5 - weights = [2.73943676, 0.16289932, 3.4536312, 2.73521126, 2.6412488] - - with QubitParamShiftTape() as tape: - for i in range(n_wires): - qml.RX(weights[i], wires=i) - - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[2, 1]) - qml.CNOT(wires=[3, 1]) - qml.CNOT(wires=[4, 3]) - - qml.expval(qml.PauliZ(1)) - - If parameters are not provided, the existing tape parameters are used: - - >>> dev = qml.device("default.qubit", wires=n_wires) - >>> tape.hessian(dev) - array([[ 0.79380556, 0.05549219, 0.10891309, -0.1452963, 0.], - [ 0.05549219, 0.79380556, -0.04208544, 0.05614438, 0.], - [ 0.10891309, -0.04208544, 0.79380556, 0.11019314, 0.], - [-0.1452963, 0.05614438, 0.11019314, 0.79380556, 0.], - [ 0., 0., 0., 0., 0.]]) - - Parameters can be optionally passed during execution: - - >>> tape.hessian(dev, params=[1.0, 1.0, 2.0, 0, 0]) - array([[ 0.12148432, -0.29466251, 0.41341091, 0., 0.], - [-0.29466251, 0.12148432, 0.41341091, 0., 0.], - [ 0.41341091, 0.41341091, 0.12148432, 0., 0.], - [ 0., 0., 0., 0.12148432, 0.], - [ 0., 0., 0., 0., 0.]]) - - Parameters provided for execution are temporary, and do not affect - the tapes' parameters in-place: - - >>> tape.get_parameters() - [2.73943676, 0.16289932, 3.4536312, 2.73521126, 2.6412488] - - If a tape has no trainable parameters, the Hessian will be empty: - - >>> tape.trainable_params = {} - >>> tape.hessian(dev) - array([], shape=(0, 0), dtype=float64) - """ - warnings.warn( - "Differentiating tapes using JacobianTape.hessian() is deprecated. " - "Please use ta standard QuantumTape with gradient transforms from " - "the qml.gradients module instead." - ) - - if any(m.return_type is State for m in self.measurements): - raise ValueError("The Hessian method does not support circuits that return the state") - - method = options.get("method", "analytic") - - if method != "analytic": - raise ValueError(f"Unknown Hessian method '{method}'") - - if params is None: - params = self.get_parameters() - - params = np.array(params) - - # perform gradient method validation - diff_methods = self._grad_method_validation(method) - - if not self._has_trainable_params(params, diff_methods): - # Either all parameters have grad method 0, or there are no trainable - # parameters. Simply return an empty Hessian. - return np.zeros((len(params), len(params)), dtype=float) - - # The parameter-shift Hessian implementation currently only supports - # the two-term parameter-shift rule. Raise an error for unsupported operations. - supported_ops = ( - "RX", - "RY", - "RZ", - "Rot", - "PhaseShift", - "ControlledPhaseShift", - "MultiRZ", - "PauliRot", - "U1", - "U2", - "U3", - "SingleExcitationMinus", - "SingleExcitationPlus", - "DoubleExcitationMinus", - "DoubleExcitationPlus", - "OrbitalRotation", - ) - - for idx, info in self._par_info.items(): - op = info["op"] - - if idx in self.trainable_params and op.name not in supported_ops: - raise ValueError( - f"The operation {op.name} is currently not supported for the " - f"parameter-shift Hessian.\nPlease decompose the operation in your " - f"QNode by replacing it with '{op.__str__().replace('(', '.decomposition(')}'" - ) - - # some gradient methods need the device or the device wires - options["device"] = device - options["dev_wires"] = device.wires - - # collect all circuits (tapes) and postprocessing functions required - # to compute the Hessian - all_tapes = [] - reshape_info = [] - processing_fns = [] - nonzero_grad_idx = [] - - # From Schwarz's theorem, the Hessian will be symmetric, so we - # can compute the upper triangular part only and symmetrize - # the final Hessian. - for i, j in itertools.combinations_with_replacement(range(len(diff_methods)), 2): - if diff_methods[i] == "0" or diff_methods[j] == "0": - continue - - nonzero_grad_idx.append((i, j)) - - tapes, processing_fn = self.hessian_pd(i, j, params=params, **options) - - processing_fns.append(processing_fn) - - # we create a flat list here to feed at once to the device - all_tapes.extend(tapes) - - # to extract the correct result for this parameter later, remember the number of tapes - reshape_info.append(len(tapes)) - - # execute all tapes at once - results = device.batch_execute(all_tapes) - - hessian = None - start = 0 - - for (i, j), processing_fn, res_len in zip(nonzero_grad_idx, processing_fns, reshape_info): - # extract the correct results from the flat list - res = results[start : start + res_len] - start += res_len - - # postprocess results to compute the gradient - g = self._flatten_processing_result(processing_fn(res)) - - if hessian is None: - # create the Hessian matrix - if self.output_dim is not None: - hessian = np.zeros( - (len(params), len(params), np.prod(self.output_dim)), dtype=float - ) - else: - hessian = np.zeros((len(params), len(params)), dtype=float) - - if i == j: - hessian[i, i] = g - else: - hessian[i, j] = hessian[j, i] = g - - if self.output_dim == 1: - hessian = np.squeeze(hessian, axis=-1) - - return hessian diff --git a/pennylane/tape/qubit_param_shift.py b/pennylane/tape/qubit_param_shift.py deleted file mode 100644 index 9457d83accb..00000000000 --- a/pennylane/tape/qubit_param_shift.py +++ /dev/null @@ -1,501 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Qubit parameter shift quantum tape. - -Provides analytic differentiation for all one-parameter gates where the generator -only has two unique eigenvalues; this includes one-parameter single-qubit gates, -and any gate with an involutory generator. -""" -# pylint: disable=attribute-defined-outside-init,protected-access -import numpy as np - -import pennylane as qml -from pennylane.measurements import MeasurementProcess -from pennylane.tape import QuantumTape -from pennylane.math import toarray - -from .jacobian_tape import JacobianTape - - -def _get_operation_recipe(op, p_idx, shifts): - """Utility function to return the parameter-shift rule - of the operation corresponding to trainable parameter - t_idx on tape. This is a copy from qml.gradients that - is replicated here to avoid circular dependencies, and - that can soon be removed together with QubitParamShiftTape. - - This function performs multiple attempts to obtain the recipe: - - - If the operation has a custom ``grad_recipe`` defined, it is used. - - - If ``parameter_frequencies`` yield a result, the frequencies are - used to construct the general parameter-shift rule via - ``qml.gradients.generate_shift_rule`` - Note that by default, the generator is used to compute the frequencies - if they are not provided by a custom implementation. - - That is, the order of precedence is ``grad_recipe``, custom - ``parameter_frequencies`` and finally ``generator`` via the default - implementation of the frequencies. - """ - # Try to use the stored grad_recipe of the operation - recipe = op.grad_recipe[p_idx] - if recipe is not None: - recipe = np.array(recipe).T - # remove all small coefficients and shifts - recipe[np.abs(recipe) < 1e-10] = 0 - - # remove columns where the coefficients are 0 - recipe = recipe[:, ~(recipe[0] == 0)] - # sort columns according to abs(shift) - return recipe[:, np.argsort(np.abs(recipe)[-1])] - - # Try to obtain frequencies, either via custom implementation or from generator eigvals - try: - frequencies = op.parameter_frequencies[p_idx] - except qml.operation.ParameterFrequenciesUndefinedError as e: - raise qml.operation.OperatorPropertyUndefined( - f"The operation {op.name} does not have a grad_recipe, parameter_frequencies or " - "a generator defined. No parameter shift rule can be applied." - ) from e - - # Create shift rule from frequencies with given shifts - coeffs, shifts = qml.gradients.generate_shift_rule(frequencies, shifts=shifts, order=1) - # The generated shift rules do not include a rescaling of the parameter, only shifts. - mults = np.ones_like(coeffs) - - return coeffs, mults, shifts - - -class QubitParamShiftTape(JacobianTape): - r"""Quantum tape for qubit parameter-shift analytic differentiation method. - - .. warning:: - - The ``QubitParamShiftTape`` is deprecated. - Please use a standard :class:`~.QuantumTape`, and apply gradient transforms using - the :mod:`.gradients` module to compute parameter-shift gradients. - - This class extends the :class:`~.jacobian` method of the quantum tape - to support analytic gradients of qubit operations using the parameter-shift rule. - This gradient method returns *exact* gradients, and can be computed directly - on quantum hardware. Simply pass ``method=analytic`` when computing the Jacobian: - - >>> tape.jacobian(dev, method="analytic") - - For more details on the quantum tape, please see :class:`~.JacobianTape`. - - **Gradients of expectation values** - - For a variational evolution :math:`U(\mathbf{p}) \vert 0\rangle` with - :math:`N` parameters :math:`\mathbf{p}`, - - consider the expectation value of an observable :math:`O`: - - .. math:: - - f(\mathbf{p}) = \langle \hat{O} \rangle(\mathbf{p}) = \langle 0 \vert - U(\mathbf{p})^\dagger \hat{O} U(\mathbf{p}) \vert 0\rangle. - - - The gradient of this expectation value can be calculated using :math:`2N` expectation - values using the parameter-shift rule: - - .. math:: - - \frac{\partial f}{\partial \mathbf{p}} = \frac{1}{2\sin s} \left[ f(\mathbf{p} + s) - - f(\mathbf{p} -s) \right]. - - **Gradients of variances** - - We can extend this to the variance, - :math:`g(\mathbf{p})=\langle \hat{O}^2 \rangle (\mathbf{p}) - [\langle \hat{O} - \rangle(\mathbf{p})]^2`, - by noting that: - - .. math:: - - \frac{\partial g}{\partial \mathbf{p}}= \frac{\partial}{\partial - \mathbf{p}} \langle \hat{O}^2 \rangle (\mathbf{p}) - - 2 f(\mathbf{p}) \frac{\partial f}{\partial \mathbf{p}}. - - This results in :math:`4N + 1` evaluations. - - In the case where :math:`O` is involutory (:math:`\hat{O}^2 = I`), the first term in the above - expression vanishes, and we are simply left with - - .. math:: - - \frac{\partial g}{\partial \mathbf{p}} = - 2 f(\mathbf{p}) - \frac{\partial f}{\partial \mathbf{p}}, - - allowing us to compute the gradient using :math:`2N + 1` evaluations. - """ - - def _update_circuit_info(self): - super()._update_circuit_info() - - # set parameter_shift as the analytic_pd method - self.analytic_pd = self.parameter_shift - - # check if the quantum tape contains any variance measurements - self.var_mask = [m.return_type is qml.operation.Variance for m in self.measurements] - - # Make a copy of the original measurements; we will be mutating them - # during the parameter shift method. - self._original_measurements = self._measurements.copy() - - if any(self.var_mask): - # The tape contains variances. - # Set parameter_shift_var as the analytic_pd method - self.analytic_pd = self.parameter_shift_var - - # Finally, store the locations of any variance measurements in the - # measurement queue. - self.var_idx = np.where(self.var_mask)[0] - - self.hessian_pd = self.parameter_shift_hessian - - def _grad_method(self, idx, use_graph=True, default_method="A"): - op = self._par_info[idx]["op"] - - if op.grad_method == "F": - return "F" - - return super()._grad_method(idx, use_graph=use_graph, default_method=default_method) - - def jacobian(self, device, params=None, **options): - # The parameter_shift_var method needs to evaluate the circuit - # at the unshifted parameter values; the result is stored in the - # self._evA_result attribute. As a result, we want the tape that computes - # the evA tape to only be generated *once*. We keep track of its generation - # via the self._append_evA_tape attribute. - self._append_evA_tape = True - self._evA_result = None - return super().jacobian(device, params, **options) - - def parameter_shift(self, idx, params, **options): - """Generate the tapes and postprocessing methods required to compute the gradient of a - parameter using the parameter-shift method. - - Args: - idx (int): trainable parameter index to differentiate with respect to - params (list[Any]): the quantum tape operation parameters - - Keyword Args: - shift=pi/2 (float): the size of the shift for two-term parameter-shift gradient computations - - Returns: - tuple[list[QuantumTape], function]: A tuple containing the list of generated tapes, - in addition to a post-processing function to be applied to the evaluated - tapes. - """ - # pylint: disable=unused-argument - op, p_idx = self.get_operation(idx) - param_shift = _get_operation_recipe(op, p_idx, None) - - shift = np.zeros_like(params) - coeffs = [] - tapes = [] - - for c, a, s in zip(*param_shift): - shift[idx] = s - shifted_tape = self.copy(copy_operations=True, tape_cls=QuantumTape) - shifted_tape.set_parameters(a * params + shift) - - coeffs.append(c) - tapes.append(shifted_tape) - - def processing_fn(results): - """Computes the gradient of the parameter at index idx via the - parameter-shift method. - - Args: - results (list[real]): evaluated quantum tapes - - Returns: - array[float]: 1-dimensional array of length determined by the tape output - measurement statistics - """ - results = np.squeeze(list(map(toarray, results))) - - if results.dtype is np.dtype("O"): - # The evaluated quantum results are a ragged array. - # Need to use a list comprehension to compute the linear - # combination. - return sum([c * r for c, r in zip(coeffs, results)]) - - # The evaluated quantum results are a valid NumPy array, - # can instead apply the dot product along the first axis. - dot = lambda x: np.dot(coeffs, x) - return np.apply_along_axis(dot, 0, results) - - return tapes, processing_fn - - def parameter_shift_var(self, idx, params, **options): - """Generate the tapes and postprocessing methods required to compute the gradient of a - parameter and its variance using the parameter-shift method. - - Args: - idx (int): trainable parameter index to differentiate with respect to - params (list[Any]): the quantum tape operation parameters - - Keyword Args: - shift=pi/2 (float): the size of the shift for two-term parameter-shift gradient computations - - Returns: - tuple[list[QuantumTape], function]: A tuple containing the list of generated tapes, - in addition to a post-processing function to be applied to the evaluated - tapes. - """ - tapes = [] - - # Get , the expectation value of the tape with unshifted parameters. - evA_tape = self.copy() - evA_tape.set_parameters(params) - - # Convert all variance measurements on the tape into expectation values - for i in self.var_idx: - obs = evA_tape._measurements[i].obs - evA_tape._measurements[i] = MeasurementProcess(qml.operation.Expectation, obs=obs) - - # evaluate the analytic derivative of - pdA_tapes, pdA_fn = evA_tape.parameter_shift(idx, params, **options) - tapes.extend(pdA_tapes) - - # For involutory observables (A^2 = I) we have d/dp = 0. - # Currently, the only observable we have in PL that may be non-involutory is qml.Hermitian - involutory = [i for i in self.var_idx if self.observables[i].name != "Hermitian"] - - # If there are non-involutory observables A present, we must compute d/dp. - non_involutory = set(self.var_idx) - set(involutory) - - if non_involutory: - pdA2_tape = self.copy() - - for i in non_involutory: - # We need to calculate d/dp; to do so, we replace the - # involutory observables A in the queue with A^2. - obs = pdA2_tape._measurements[i].obs - A = obs.get_matrix() - - obs = qml.Hermitian(A @ A, wires=obs.wires, do_queue=False) - pdA2_tape._measurements[i] = MeasurementProcess(qml.operation.Expectation, obs=obs) - - # Non-involutory observables are present; the partial derivative of - # may be non-zero. Here, we calculate the analytic derivatives of the - # observables. - pdA2_tapes, pdA2_fn = pdA2_tape.parameter_shift(idx, params, **options) - tapes.extend(pdA2_tapes) - - # Make sure that the expectation value of the tape with unshifted parameters - # is only calculated once, if `self._append_evA_tape` is True. - if self._append_evA_tape: - tapes.append(evA_tape) - - # Now that the tape has been appended, we want to avoid - # appending it for subsequent parameters, as the result can simply - # be re-used. - self._append_evA_tape = False - - def processing_fn(results): - """Computes the gradient of the parameter at index ``idx`` via the - parameter-shift method for a circuit containing a mixture - of expectation values and variances. - - Args: - results (list[real]): evaluated quantum tapes - - Returns: - array[float]: 1-dimensional array of length determined by the tape output - measurement statistics - """ - pdA = pdA_fn(results[0:2]) - pdA2 = 0 - - if non_involutory: - pdA2 = pdA2_fn(results[2:4]) - - if involutory: - pdA2[np.array(involutory)] = 0 - - # Check if the expectation value of the tape with unshifted parameters - # has already been calculated. - if self._evA_result is None: - # The expectation value hasn't been previously calculated; - # it will be the last element of the `results` argument. - self._evA_result = np.array(results[-1]) - - # return d(var(A))/dp = d/dp -2 * * d/dp for the variances, - # d/dp for plain expectations - return np.where(self.var_mask, pdA2 - 2 * self._evA_result * pdA, pdA) - - return tapes, processing_fn - - def parameter_shift_hessian(self, i, j, params, **options): - """Generate the tapes and postprocessing methods required to compute the - second derivative with respect to tape parameter :math:`i` and :math:`j` - using the second-order parameter-shift method. - - Args: - i (int): trainable parameter index to differentiate with respect to - j (int): trainable parameter index to differentiate with respect to - params (list[Any]): the quantum tape operation parameters - - Keyword Args: - s1=pi/2 (float): the size of the shift for index i in the parameter-shift Hessian computations - s2=pi/2 (float): the size of the shift for index j in the parameter-shift Hessian computations - - Returns: - tuple[list[QuantumTape], function]: A tuple containing the list of generated tapes, - in addition to a post-processing function to be applied to the evaluated - tapes. - """ - idxs = (i, j) - idx_shifts = {i: options.get("s1", np.pi / 2), j: options.get("s2", np.pi / 2)} - - param_shifts = [] - - if i == j and idx_shifts[i] == idx_shifts[j]: - if idx_shifts[i] == np.pi / 2: - # When i = j and s1 = s2 = pi/2, the Hessian parameter shift rule - # can be simplified to two device executions - param_shifts = [(0.5, 1, np.pi), (-0.5, 1, 0)] - elif idx_shifts[i] == np.pi / 4: - # When i = j and s1 = s2 = pi/4, the Hessian parameter shift rule - # can be simplified to three device executions - # TODO: The first and last parameter shift values below are identical - # to those used when computing the Jacobian with s=pi/2. We should find - # a way to re-use those values rather than re-calculating them. - param_shifts = [(0.5, 1, np.pi / 2), (-1, 1, 0), (0.5, 1, -np.pi / 2)] - - coeffs = [] - tapes = [] - shift = np.eye(len(params)) - - if param_shifts: - # Optimizations can be made to reduce amount of tape executions - for c, a, s in param_shifts: - shifted_tape = self.copy(copy_operations=True, tape_cls=QuantumTape) - shifted_tape.set_parameters(a * params + s * shift[i]) - - coeffs.append(c) - tapes.append(shifted_tape) - else: - # No optimizations can be made, generate all 4 tapes - for idx in idxs: - op, p_idx = self.get_operation(idx) - param_shift = _get_operation_recipe(op, p_idx, None) - param_shifts.append(param_shift) - - for c1, a, s1 in zip(*param_shifts[0]): - for c2, _, s2 in zip(*param_shifts[1]): - c = c1 * c2 - s = s1 * shift[i] + s2 * shift[j] - shifted_tape = self.copy(copy_operations=True, tape_cls=QuantumTape) - shifted_tape.set_parameters(a * params + s) - - coeffs.append(c) - tapes.append(shifted_tape) - - def processing_fn(results): - """Computes the second derivative with respect to tape parameters i - and j using the second-order parameter-shift method. - - Args: - results (list[real]): evaluated quantum tapes - - Returns: - array[float]: 1-dimensional array of length determined by the tape output - measurement statistics - """ - results = np.squeeze(results) - - if results.dtype is np.dtype("O"): - # The evaluated quantum results are a ragged array. - # Need to use a list comprehension to compute the linear - # combination. - return sum([c * r for c, r in zip(coeffs, results)]) - - # The evaluated quantum results are a valid NumPy array, - # can instead apply the dot product along the first axis. - dot = lambda x: np.dot(coeffs, x) - return np.apply_along_axis(dot, 0, results) - - return tapes, processing_fn - - @property - def specs(self): - """Resource information about a quantum circuit. - - Returns: - dict[str, Union[defaultdict,int]]: dictionaries that contain tape specifications - - **Example** - - .. code-block:: python3 - - with qml.tape.QubitParamShiftTape() as tape: - qml.Hadamard(wires=0) - qml.RZ(0.26, wires=1) - qml.CNOT(wires=[1, 0]) - qml.Rot(1.8, -2.7, 0.2, wires=0) - qml.Hadamard(wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - Asking for the specs produces a dictionary as shown below: - - >>> tape.specs['gate_sizes'] - defaultdict(int, {1: 4, 2: 2}) - >>> tape.specs['gate_types'] - defaultdict(int, {'Hadamard': 2, 'RZ': 1, 'CNOT': 2, 'Rot': 1}) - - As ``defaultdict`` objects, any key not present in the dictionary returns 0. - - >>> tape.specs['gate_types']['RX'] - 0 - - In parameter-shift tapes, the number of device executions necessary for a gradient - is calulated as well: - - >>> tape.specs['num_parameter_shift_executions] - 9 - - """ - - info = super().specs - - if any(m.return_type is qml.operation.State for m in self.measurements): - return info - - if len(self._par_info) > 0: - if "grad_method" not in self._par_info[0]: - self._update_gradient_info() - - # Initialize with the forward pass execution - num_executions = 1 - # Loop over all variables - for _, grad_info in self._par_info.items(): - - # if this variable uses parameter-shift - if grad_info["grad_method"] == "A": - op = grad_info["op"] - p_idx = grad_info["p_idx"] - num_executions += len(_get_operation_recipe(op, p_idx, None)) - - info["num_parameter_shift_executions"] = num_executions - - return info diff --git a/pennylane/tape/reversible.py b/pennylane/tape/reversible.py deleted file mode 100644 index df7937b2041..00000000000 --- a/pennylane/tape/reversible.py +++ /dev/null @@ -1,278 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Quantum tape that implements reversible backpropagation. -""" -# pylint: disable=attribute-defined-outside-init,protected-access -import copy -from functools import reduce -from string import ascii_letters as ABC -import warnings - -import numpy as np - -import pennylane as qml - -from .jacobian_tape import JacobianTape -from .tape import QuantumTape - - -ABC_ARRAY = np.array(list(ABC)) - - -class ReversibleTape(JacobianTape): - r"""Quantum tape for computing gradients via reversible analytic differentiation. - - .. warning:: - - The ``ReversibleTape`` is deprecated. - Instead, create a standard ``QuantumTape``, and use devices that support - native adjoint differentiation methods, such as ``default.qubit`` and ``lightning.qubit``. - - .. note:: - - The reversible analytic differentiation method has the following restrictions: - - * As it requires knowledge of the statevector, only statevector simulator devices can be used. - - * Differentiation is only supported for the parametrized quantum operations - :class:`~.RX`, :class:`~.RY`, :class:`~.RZ`, and :class:`~.Rot`. - - This class extends the :class:`~.jacobian` method of the quantum tape to support analytic - gradients of qubit operations using reversible analytic differentiation. This gradient method - returns *exact* gradients, however requires use of a statevector simulator. Simply create - the tape, and then call the Jacobian method: - - >>> tape.jacobian(dev) - - For more details on the quantum tape, please see :class:`~.JacobianTape`. - - **Reversible analytic differentiation** - - Assume a circuit has a gate :math:`G(\theta)` that we want to differentiate. - Without loss of generality, we can write the circuit in the form three unitaries: :math:`UGV`. - Starting from the initial state :math:`\vert 0\rangle`, the quantum state is evolved up to the - "pre-measurement" state :math:`\vert\psi\rangle=UGV\vert 0\rangle`, which is saved - (this can be reused for each variable being differentiated). - - We then apply the unitary :math:`V^{-1}` to evolve this state backwards in time - until just after the gate :math:`G` (hence the name "reversible"). - The generator of :math:`G` is then applied as a gate, and we evolve forward using :math:`V` again. - At this stage, the state of the simulator is proportional to - :math:`\frac{\partial}{\partial\theta}\vert\psi\rangle`. - Some further post-processing of this gives the derivative - :math:`\frac{\partial}{\partial\theta} \langle \hat{O} \rangle` for any observable O. - - The reversible approach is similar to backpropagation, but trades off extra computation for - enhanced memory efficiency. Where backpropagation caches the state tensors at each step during - a forward pass, the reversible method only caches the final pre-measurement state. - - Compared to the parameter-shift rule, the reversible method can - be faster or slower, depending on the density and location of parametrized gates in a circuit - (circuits with higher density of parametrized gates near the end of the circuit will see a - benefit). - """ - - def _grad_method(self, idx, use_graph=True, default_method="A"): - return super()._grad_method(idx, use_graph=use_graph, default_method=default_method) - - @staticmethod - def _matrix_elem(vec1, obs, vec2, dev_wires): - r"""Computes the matrix element of an observable. - - That is, given two basis states :math:`\mathbf{i}`, :math:`\mathbf{j}`, - this method returns :math:`\langle \mathbf{i} \vert \hat{O} \vert \mathbf{j} \rangle`. - Unmeasured wires are contracted, and a scalar is returned. - - Args: - vec1 (array[complex]): a length :math:`2^N` statevector - obs (.Observable): a PennyLane observable - vec2 (array[complex]): a length :math:`2^N` statevector - dev_wires (pennylane.wires.Wires): wires of the device used to prepare the state - """ - # pylint: disable=protected-access - - mat = np.reshape(obs.get_matrix(), [2] * len(obs.wires) * 2) - vec1 = np.reshape(vec1, [2] * len(dev_wires)) - vec2 = np.reshape(vec2, [2] * len(dev_wires)) - - vec1_indices = ABC[: len(dev_wires)] - - # compute the indices of the observable's wires on the device - wire_indices = dev_wires.indices(obs.wires) - obs_in_indices = "".join(ABC_ARRAY[wire_indices].tolist()) - obs_out_indices = ABC[len(dev_wires) : len(dev_wires) + len(obs.wires)] - obs_indices = "".join([obs_in_indices, obs_out_indices]) - - vec2_indices = reduce( - lambda old_string, idx_pair: old_string.replace(idx_pair[0], idx_pair[1]), - zip(obs_in_indices, obs_out_indices), - vec1_indices, - ) - - einsum_str = f"{vec1_indices},{obs_indices},{vec2_indices}->" - - return np.einsum(einsum_str, np.conj(vec1), mat, vec2) - - def reversible_diff(self, idx, params, **options): - """Generate the tapes and postprocessing methods required to compute the gradient of a - parameter using the reversible backpropagation method. - - Args: - idx (int): trainable parameter index to differentiate with respect to - params (list[Any]): the quantum tape operation parameters - - Keyword Args: - dev_wires (.Wires): wires on the device the reversible backpropagation method - is computed on - - Returns: - tuple[list[QuantumTape], function]: A tuple containing the list of generated tapes, - in addition to a post-processing function to be applied to the evaluated - tapes. - """ - - # The reversible tape only support differentiating - # expectation values of observables for now. - for m in self.measurements: - if ( - m.return_type is qml.operation.Variance - or m.return_type is qml.operation.Probability - ): - raise ValueError( - f"{m.return_type} is not supported with the reversible gradient method" - ) - if m.obs.name == "Hamiltonian": - raise qml.QuantumFunctionError( - "Reverse differentiation method does not support Hamiltonian observables." - ) - - t_idx = list(self.trainable_params)[idx] - op = self._par_info[t_idx]["op"] - p_idx = self._par_info[t_idx]["p_idx"] - - # The reversible tape only supports the RX, RY, RZ, and Rot operations for now: - # - # * CRX, CRY, CRZ ops have a non-unitary matrix as generator. - # - # * PauliRot, MultiRZ, U2, and U3 do not have generators specified. - # - # TODO: the controlled rotations can be supported by multiplying ``state`` - # directly by these generators within this function - # (or by allowing non-unitary matrix multiplies in the simulator backends) - - if op.name not in ["RX", "RY", "RZ", "Rot"]: - raise ValueError( - f"The {op.name} gate is not currently supported with the " - f"reversible gradient method." - ) - - # get the stored final state of the original circuit, which we start from here - - final_state = self._final_state - # get the wires on the device used for the differentiation - - dev_wires = options.get("dev_wires") - - self.set_parameters(params) - - # create a new circuit which rewinds the pre-measurement state to just after `op`, - # applies the generator of `op`, and then plays forward back to - # pre-measurement step - op_idx = self.operations.index(op) - between_ops = self.operations[op_idx + 1 :] - - if op.name == "Rot": - decomp = op.decomposition() - generator, multiplier = qml.generator(decomp[p_idx]) - between_ops = decomp[p_idx + 1 :] + between_ops - else: - generator, multiplier = qml.generator(op) - - # construct circuit to compute differentiated state - between_ops_inverse = [copy.copy(op) for op in between_ops[::-1]] - - with QuantumTape() as new_circuit: - # start with final state of original circuit - qml.QubitStateVector(final_state, wires=dev_wires) - - # evolve circuit backwards until gate we want to differentiate - for op in between_ops_inverse: - op.queue().inv() - - # apply generator needed for differentiation - qml.apply(generator) - - # evolve forwards again - for op in between_ops: - op.queue() - - qml.state() - - tapes = [new_circuit] - - def processing_fn(results): - """Computes the gradient of the parameter at index idx via the - reversible backprop method. - - Args: - results (list[real]): evaluated quantum tapes - - Returns: - array[float]: 1-dimensional array of length determined by the tape output - measurement statistics - """ - dstate = results[0][0] - - # compute matrix element for each observable O - # TODO: if all observables act on same number of wires, could do all at once with einsum - matrix_elems = [ - self._matrix_elem(dstate, ob, final_state, dev_wires) for ob in self.observables - ] - matrix_elems = np.array(matrix_elems) - return 2 * multiplier * np.imag(matrix_elems) - - return tapes, processing_fn - - def jacobian(self, device, params=None, **options): - # The reversible_diff method needs to evaluate the circuit - # at the unshifted parameter values; the pre-rotated statevector is then stored - # in the self._state attribute. Here, we set the value of the attribute to None - - # before each Jacobian call, so that the statevector is calculated only once. - self._final_state = None - if device.shots is not None: - warnings.warn( - "Requested reversible differentiation to be computed with finite shots." - " Reversible differentiation always calculated exactly.", - UserWarning, - ) - - return super().jacobian(device, params, **options) - - def analytic_pd(self, idx, params, **options): - device = options["device"] - - # circuits constructed in reversible differentiation always start - # with the final state of the original circuit, which we store here - if self._final_state is None: - self.execute_device(params, device) - # todo: better create a new tape that has state as output method here? - self._final_state = device._pre_rotated_state.flatten() - - # we need the wires to prepare the final state in each run, and to - # be able to compute an expecation value by hand - options["dev_wires"] = device.wires - - return self.reversible_diff(idx, params=params, **options) diff --git a/tests/tape/test_cv_param_shift.py b/tests/tape/test_cv_param_shift.py deleted file mode 100644 index d0febe4c0ee..00000000000 --- a/tests/tape/test_cv_param_shift.py +++ /dev/null @@ -1,899 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the CV parameter-shift CVParamShiftTape""" -import pytest -import numpy as np - -import pennylane as qml -from pennylane.tape import CVParamShiftTape - - -hbar = 2 - - -class TestGradMethod: - """Tests for parameter gradient methods""" - - def test_non_differentiable(self): - """Test that a non-differentiable parameter is - correctly marked""" - - with CVParamShiftTape() as tape: - qml.FockState(1, wires=0) - qml.Displacement(0.543, 0, wires=[1]) - qml.Beamsplitter(0, 0, wires=[0, 1]) - qml.expval(qml.X(wires=[0])) - - assert tape._grad_method(0) is None - assert tape._grad_method(1) == "A" - assert tape._grad_method(2) == "A" - assert tape._grad_method(3) == "A" - assert tape._grad_method(4) == "A" - - tape._update_gradient_info() - - assert tape._par_info[0]["grad_method"] is None - assert tape._par_info[1]["grad_method"] == "A" - assert tape._par_info[2]["grad_method"] == "A" - assert tape._par_info[3]["grad_method"] == "A" - assert tape._par_info[4]["grad_method"] == "A" - - def test_no_graph_exception(self): - """Test that an exception is raised for analytically differentiable - operations if use_graph=False""" - with CVParamShiftTape() as tape: - qml.Rotation(0.543, wires=[0]) - qml.expval(qml.P(0)) - - with pytest.raises(ValueError, match="must always use the graph"): - tape._grad_method(0, use_graph=False) - - def test_independent(self): - """Test that an independent variable is properly marked - as having a zero gradient""" - - with CVParamShiftTape() as tape: - qml.Rotation(0.543, wires=[0]) - qml.Rotation(-0.654, wires=[1]) - qml.expval(qml.P(0)) - - assert tape._grad_method(0) == "A" - assert tape._grad_method(1) == "0" - - tape._update_gradient_info() - - assert tape._par_info[0]["grad_method"] == "A" - assert tape._par_info[1]["grad_method"] == "0" - - def test_finite_diff(self, monkeypatch): - """If an op has grad_method=F, this should be respected - by the CVParamShiftTape""" - monkeypatch.setattr(qml.Rotation, "grad_method", "F") - - with CVParamShiftTape() as tape: - qml.Rotation(0.543, wires=[0]) - qml.Squeezing(0.543, 0, wires=[0]) - qml.expval(qml.P(0)) - - assert tape._grad_method(0) == "F" - assert tape._grad_method(1) == "A" - assert tape._grad_method(2) == "A" - - def test_non_gaussian_operation(self): - """Test that a non-Gaussian operation succeeding - a differentiable Gaussian operation results in - numeric differentiation.""" - - with CVParamShiftTape() as tape: - qml.Rotation(1.0, wires=[0]) - qml.Rotation(1.0, wires=[1]) - # Non-Gaussian - qml.Kerr(1.0, wires=[1]) - qml.expval(qml.P(0)) - qml.expval(qml.X(1)) - - # First rotation gate has no succeeding non-Gaussian operation - assert tape._grad_method(0) == "A" - # Second rotation gate does no succeeding non-Gaussian operation - assert tape._grad_method(1) == "F" - # Kerr gate does not support the parameter-shift rule - assert tape._grad_method(2) == "F" - - with CVParamShiftTape() as tape: - qml.Rotation(1.0, wires=[0]) - qml.Rotation(1.0, wires=[1]) - # entangle the modes - qml.Beamsplitter(1.0, 0.0, wires=[0, 1]) - # Non-Gaussian - qml.Kerr(1.0, wires=[1]) - qml.expval(qml.P(0)) - qml.expval(qml.X(1)) - - # After entangling the modes, the Kerr gate now succeeds - # both initial rotations - assert tape._grad_method(0) == "F" - assert tape._grad_method(1) == "F" - assert tape._grad_method(2) == "F" - - def test_probability(self): - """Probability is the expectation value of a - higher order observable, and thus only supports numerical - differentiation""" - with CVParamShiftTape() as tape: - qml.Rotation(0.543, wires=[0]) - qml.Squeezing(0.543, 0, wires=[0]) - qml.probs(wires=0) - - assert tape._grad_method(0) == "F" - assert tape._grad_method(1) == "F" - assert tape._grad_method(2) == "F" - - def test_variance(self): - """If the variance of the observable is first order, then - parameter-shift is supported. If the observable is second order, - however, only finite-differences is supported.""" - - with CVParamShiftTape() as tape: - qml.Rotation(1.0, wires=[0]) - qml.var(qml.P(0)) # first order - - assert tape._grad_method(0) == "A" - - with CVParamShiftTape() as tape: - qml.Rotation(1.0, wires=[0]) - qml.var(qml.NumberOperator(0)) # second order - - assert tape._grad_method(0) == "F" - - with CVParamShiftTape() as tape: - qml.Rotation(1.0, wires=[0]) - qml.Rotation(1.0, wires=[1]) - qml.Beamsplitter(0.5, 0.0, wires=[0, 1]) - qml.var(qml.NumberOperator(0)) # second order - qml.expval(qml.NumberOperator(1)) - - assert tape._grad_method(0) == "F" - assert tape._grad_method(1) == "F" - assert tape._grad_method(2) == "F" - assert tape._grad_method(3) == "F" - - def test_second_order_expectation(self): - """Test that the expectation of a second-order observable forces - the gradient method to use the second-order parameter-shift rule""" - - with CVParamShiftTape() as tape: - qml.Rotation(1.0, wires=[0]) - qml.expval(qml.NumberOperator(0)) # second order - - assert tape._grad_method(0) == "A2" - - def test_unknown_op_grad_method(self, monkeypatch): - """Test that an exception is raised if an operator has a - grad method defined that the CV parameter-shift tape - doesn't recognize""" - monkeypatch.setattr(qml.Rotation, "grad_method", "B") - - with CVParamShiftTape() as tape: - qml.Rotation(1.0, wires=0) - qml.expval(qml.X(0)) - - with pytest.raises(ValueError, match="unknown gradient method"): - tape._grad_method(0) - - -class TestTransformObservable: - """Tests for the _transform_observable method""" - - def test_incorrect_heisenberg_size(self, monkeypatch): - """The number of dimensions of a CV observable Heisenberg representation does - not match the ev_order attribute.""" - monkeypatch.setattr(qml.P, "ev_order", 2) - - with pytest.raises(ValueError, match="Mismatch between the polynomial order"): - CVParamShiftTape._transform_observable(qml.P(0), np.identity(3), device_wires=[0]) - - def test_higher_order_observable(self, monkeypatch): - """An exception should be raised if the observable is higher than 2nd order.""" - monkeypatch.setattr(qml.P, "ev_order", 3) - - with pytest.raises(NotImplementedError, match="order > 2 not implemented"): - CVParamShiftTape._transform_observable(qml.P(0), np.identity(3), device_wires=[0]) - - def test_first_order_transform(self, tol): - """Test that a first order observable is transformed correctly""" - # create a symmetric transformation - Z = np.arange(3**2).reshape(3, 3) - Z = Z.T + Z - - obs = qml.X(0) - res = CVParamShiftTape._transform_observable(obs, Z, device_wires=[0]) - - # The Heisenberg representation of the X - # operator is simply... X - expected = np.array([0, 1, 0]) @ Z - - assert isinstance(res, qml.PolyXP) - assert res.wires.labels == (0,) - assert np.allclose(res.data[0], expected, atol=tol, rtol=0) - - def test_second_order_transform(self, tol): - """Test that a second order observable is transformed correctly""" - # create a symmetric transformation - Z = np.arange(3**2).reshape(3, 3) - Z = Z.T + Z - - obs = qml.NumberOperator(0) - res = CVParamShiftTape._transform_observable(obs, Z, device_wires=[0]) - - # The Heisenberg representation of the number operator - # is (X^2 + P^2) / (2*hbar) - 1/2 - A = np.array([[-0.5, 0, 0], [0, 0.25, 0], [0, 0, 0.25]]) - expected = A @ Z + Z @ A - - assert isinstance(res, qml.PolyXP) - assert res.wires.labels == (0,) - assert np.allclose(res.data[0], expected, atol=tol, rtol=0) - - def test_device_wire_expansion(self, tol): - """Test that the transformation works correctly - for the case where the transformation applies to more wires - than the observable.""" - - # create a 3-mode symmetric transformation - wires = qml.wires.Wires([0, "a", 2]) - ndim = 1 + 2 * len(wires) - - Z = np.arange(ndim**2).reshape(ndim, ndim) - Z = Z.T + Z - - obs = qml.NumberOperator(0) - res = CVParamShiftTape._transform_observable(obs, Z, device_wires=wires) - - # The Heisenberg representation of the number operator - # is (X^2 + P^2) / (2*hbar) - 1/2. We use the ordering - # I, X0, Xa, X2, P0, Pa, P2. - A = np.diag([-0.5, 0.25, 0.25, 0, 0, 0, 0]) - expected = A @ Z + Z @ A - - assert isinstance(res, qml.PolyXP) - assert res.wires == wires - assert np.allclose(res.data[0], expected, atol=tol, rtol=0) - - -class TestParameterShiftLogic: - """Test for the dispatching logic of the parameter shift method""" - - def test_force_order2(self, mocker): - """Test that if the force_order2 keyword argument is provided, - the second order parameter shift rule is forced""" - dev = qml.device("default.gaussian", wires=1) - - with CVParamShiftTape() as tape: - qml.Displacement(1.0, 0.0, wires=[0]) - qml.Rotation(2.0, wires=[0]) - qml.expval(qml.X(0)) - - tape.trainable_params = {0, 1, 2} - - spy1 = mocker.spy(tape, "parameter_shift_first_order") - spy2 = mocker.spy(tape, "parameter_shift_second_order") - - tape.jacobian(dev, method="analytic", force_order2=False) - spy1.assert_called() - spy2.assert_not_called() - - tape.jacobian(dev, method="analytic", force_order2=True) - spy2.assert_called() - - def test_no_poly_xp_support(self, mocker, monkeypatch, caplog): - """Test that if a device does not support PolyXP - and the second-order parameter-shift rule is required, - we fallback to finite differences.""" - dev = qml.device("default.gaussian", wires=1) - - monkeypatch.delitem(dev._observable_map, "PolyXP") - - with CVParamShiftTape() as tape: - qml.Rotation(1.0, wires=[0]) - qml.expval(qml.NumberOperator(0)) - - tape.trainable_params = {0} - assert tape.analytic_pd == tape.parameter_shift - - spy_analytic = mocker.spy(tape, "analytic_pd") - spy_first_order_shift = mocker.spy(tape, "parameter_shift_first_order") - spy_second_order_shift = mocker.spy(tape, "parameter_shift_second_order") - spy_transform = mocker.spy(qml.operation.CVOperation, "heisenberg_tr") - spy_numeric = mocker.spy(tape, "numeric_pd") - - with pytest.warns(UserWarning, match="does not support the PolyXP observable"): - tape.jacobian(dev, method="analytic") - - spy_analytic.assert_called() - spy_first_order_shift.assert_not_called() - spy_second_order_shift.assert_not_called() - spy_transform.assert_not_called() - spy_numeric.assert_called() - - def test_no_poly_xp_support_variance(self, mocker, monkeypatch, caplog): - """Test that if a device does not support PolyXP - and the variance parameter-shift rule is required, - we fallback to finite differences.""" - dev = qml.device("default.gaussian", wires=1) - - monkeypatch.delitem(dev._observable_map, "PolyXP") - - with CVParamShiftTape() as tape: - qml.Rotation(1.0, wires=[0]) - qml.var(qml.X(0)) - - tape.trainable_params = {0} - assert tape.analytic_pd == tape.parameter_shift_var - - spy1 = mocker.spy(tape, "parameter_shift_first_order") - spy2 = mocker.spy(tape, "parameter_shift_second_order") - spy_numeric = mocker.spy(tape, "numeric_pd") - - with pytest.warns(UserWarning, match="does not support the PolyXP observable"): - tape.jacobian(dev, method="analytic") - - spy1.assert_not_called() - spy2.assert_not_called() - spy_numeric.assert_called() - - -class TestExpectationQuantumGradients: - """Tests for the quantum gradients of various gates - - with expectation value output""" - - def test_rotation_gradient(self, mocker, tol): - """Test the gradient of the rotation gate""" - dev = qml.device("default.gaussian", wires=2, hbar=hbar) - - alpha = 0.5643 - theta = 0.23354 - - with CVParamShiftTape() as tape: - qml.Displacement(alpha, 0.0, wires=[0]) - qml.Rotation(theta, wires=[0]) - qml.expval(qml.X(0)) - - tape._update_gradient_info() - tape.trainable_params = {2} - - spy1 = mocker.spy(CVParamShiftTape, "parameter_shift_first_order") - spy2 = mocker.spy(CVParamShiftTape, "parameter_shift_second_order") - - grad_A = tape.jacobian(dev, method="analytic") - spy1.assert_called() - spy2.assert_not_called() - - grad_A2 = tape.jacobian(dev, method="analytic", force_order2=True) - spy2.assert_called() - - expected = -hbar * alpha * np.sin(theta) - assert np.allclose(grad_A, expected, atol=tol, rtol=0) - assert np.allclose(grad_A2, expected, atol=tol, rtol=0) - - def test_beamsplitter_gradient(self, mocker, tol): - """Test the gradient of the beamsplitter gate""" - dev = qml.device("default.gaussian", wires=2, hbar=hbar) - - alpha = 0.5643 - theta = 0.23354 - - with CVParamShiftTape() as tape: - qml.Displacement(alpha, 0.0, wires=[0]) - qml.Beamsplitter(theta, 0.0, wires=[0, 1]) - qml.expval(qml.X(0)) - - tape._update_gradient_info() - tape.trainable_params = {2} - - spy1 = mocker.spy(CVParamShiftTape, "parameter_shift_first_order") - spy2 = mocker.spy(CVParamShiftTape, "parameter_shift_second_order") - - grad_A = tape.jacobian(dev, method="analytic") - spy1.assert_called() - spy2.assert_not_called() - - grad_A2 = tape.jacobian(dev, method="analytic", force_order2=True) - spy2.assert_called() - - expected = -hbar * alpha * np.sin(theta) - assert np.allclose(grad_A, expected, atol=tol, rtol=0) - assert np.allclose(grad_A2, expected, atol=tol, rtol=0) - - def test_displacement_gradient(self, mocker, tol): - """Test the gradient of the displacement gate""" - dev = qml.device("default.gaussian", wires=2, hbar=hbar) - - r = 0.5643 - phi = 0.23354 - - with CVParamShiftTape() as tape: - qml.Displacement(r, phi, wires=[0]) - qml.expval(qml.X(0)) - - tape._update_gradient_info() - tape.trainable_params = {0, 1} - - spy1 = mocker.spy(CVParamShiftTape, "parameter_shift_first_order") - spy2 = mocker.spy(CVParamShiftTape, "parameter_shift_second_order") - - grad_A = tape.jacobian(dev, method="analytic") - spy1.assert_called() - spy2.assert_not_called() - - grad_A2 = tape.jacobian(dev, method="analytic", force_order2=True) - spy2.assert_called() - - expected = [hbar * np.cos(phi), -hbar * r * np.sin(phi)] - assert np.allclose(grad_A, expected, atol=tol, rtol=0) - assert np.allclose(grad_A2, expected, atol=tol, rtol=0) - - def test_squeezed_gradient(self, mocker, tol): - """Test the gradient of the squeezed gate. We also - ensure that the gradient is correct even when an operation - with no Heisenberg representation is a descendent.""" - dev = qml.device("default.gaussian", wires=2, hbar=hbar) - - class Rotation(qml.operation.CVOperation): - """Dummy operation that does not support - heisenberg representation""" - - num_wires = 1 - num_params = 1 - grad_method = "A" - - alpha = 0.5643 - r = 0.23354 - - with CVParamShiftTape() as tape: - qml.Displacement(alpha, 0.0, wires=[0]) - qml.Squeezing(r, 0.0, wires=[0]) - - # The following two gates have no effect - # on the circuit gradient and expectation value - qml.Beamsplitter(0.0, 0.0, wires=[0, 1]) - Rotation(0.543, wires=[1]) - - qml.expval(qml.X(0)) - - tape._update_gradient_info() - tape.trainable_params = {2} - - spy1 = mocker.spy(CVParamShiftTape, "parameter_shift_first_order") - spy2 = mocker.spy(CVParamShiftTape, "parameter_shift_second_order") - - grad_A = tape.jacobian(dev, method="analytic") - spy1.assert_called() - spy2.assert_not_called() - - grad_A2 = tape.jacobian(dev, method="analytic", force_order2=True) - spy2.assert_called() - - expected = -np.exp(-r) * hbar * alpha - assert np.allclose(grad_A, expected, atol=tol, rtol=0) - assert np.allclose(grad_A2, expected, atol=tol, rtol=0) - - def test_squeezed_number_state_gradient(self, mocker, tol): - """Test the numerical gradient of the squeeze gate with - with number state expectation is correct""" - dev = qml.device("default.gaussian", wires=2, hbar=hbar) - - r = 0.23354 - - with CVParamShiftTape() as tape: - qml.Squeezing(r, 0.0, wires=[0]) - # the fock state projector is a 'non-Gaussian' observable - qml.expval(qml.FockStateProjector(np.array([2, 0]), wires=[0, 1])) - - tape._update_gradient_info() - tape.trainable_params = {0} - assert tape._par_info[0]["grad_method"] == "F" - - spy = mocker.spy(CVParamShiftTape, "parameter_shift") - grad = tape.jacobian(dev) - spy.assert_not_called() - - # (d/dr) |<2|S(r)>|^2 = 0.5 tanh(r)^3 (2 csch(r)^2 - 1) sech(r) - expected = 0.5 * np.tanh(r) ** 3 * (2 / (np.sinh(r) ** 2) - 1) / np.cosh(r) - assert np.allclose(grad, expected, atol=tol, rtol=0) - - def test_multiple_squeezing_gradient(self, mocker, tol): - """Test that the gradient of a circuit with two squeeze - gates is correct.""" - dev = qml.device("default.gaussian", wires=2, hbar=hbar) - - r0, phi0, r1, phi1 = [0.4, -0.3, -0.7, 0.2] - - with CVParamShiftTape() as tape: - qml.Squeezing(r0, phi0, wires=[0]) - qml.Squeezing(r1, phi1, wires=[0]) - qml.expval(qml.NumberOperator(0)) # second order - - tape._update_gradient_info() - - spy2 = mocker.spy(CVParamShiftTape, "parameter_shift_second_order") - grad_A2 = tape.jacobian(dev, method="analytic", force_order2=True) - spy2.assert_called() - - # check against the known analytic formula - expected = np.zeros([4]) - expected[0] = np.cosh(2 * r1) * np.sinh(2 * r0) + np.cos(phi0 - phi1) * np.cosh( - 2 * r0 - ) * np.sinh(2 * r1) - expected[1] = -0.5 * np.sin(phi0 - phi1) * np.sinh(2 * r0) * np.sinh(2 * r1) - expected[2] = np.cos(phi0 - phi1) * np.cosh(2 * r1) * np.sinh(2 * r0) + np.cosh( - 2 * r0 - ) * np.sinh(2 * r1) - expected[3] = 0.5 * np.sin(phi0 - phi1) * np.sinh(2 * r0) * np.sinh(2 * r1) - - assert np.allclose(grad_A2, expected, atol=tol, rtol=0) - - def test_multiple_second_order_observables(self, mocker, tol): - """Test that the gradient of a circuit with multiple - second order observables is correct""" - - dev = qml.device("default.gaussian", wires=2, hbar=hbar) - r = [0.4, -0.7, 0.1, 0.2] - p = [0.1, 0.2, 0.3, 0.4] - - with CVParamShiftTape() as tape: - qml.Squeezing(r[0], p[0], wires=[0]) - qml.Squeezing(r[1], p[1], wires=[0]) - qml.Squeezing(r[2], p[2], wires=[1]) - qml.Squeezing(r[3], p[3], wires=[1]) - qml.expval(qml.NumberOperator(0)) # second order - qml.expval(qml.NumberOperator(1)) # second order - - tape._update_gradient_info() - - spy2 = mocker.spy(CVParamShiftTape, "parameter_shift_second_order") - grad_A2 = tape.jacobian(dev, method="analytic", force_order2=True) - spy2.assert_called() - - # check against the known analytic formula - - def expected_grad(r, p): - return np.array( - [ - np.cosh(2 * r[1]) * np.sinh(2 * r[0]) - + np.cos(p[0] - p[1]) * np.cosh(2 * r[0]) * np.sinh(2 * r[1]), - -0.5 * np.sin(p[0] - p[1]) * np.sinh(2 * r[0]) * np.sinh(2 * r[1]), - np.cos(p[0] - p[1]) * np.cosh(2 * r[1]) * np.sinh(2 * r[0]) - + np.cosh(2 * r[0]) * np.sinh(2 * r[1]), - 0.5 * np.sin(p[0] - p[1]) * np.sinh(2 * r[0]) * np.sinh(2 * r[1]), - ] - ) - - expected = np.zeros([2, 8]) - expected[0, :4] = expected_grad(r[:2], p[:2]) - expected[1, 4:] = expected_grad(r[2:], p[2:]) - - assert np.allclose(grad_A2, expected, atol=tol, rtol=0) - - @pytest.mark.parametrize("obs", [qml.X, qml.Identity]) - @pytest.mark.parametrize( - "op", [qml.Displacement(0.1, 0.2, wires=0), qml.TwoModeSqueezing(0.1, 0.2, wires=[0, 1])] - ) - def test_gradients_gaussian_circuit(self, op, obs, mocker, tol): - """Tests that the gradients of circuits of gaussian gates match between the - finite difference and analytic methods.""" - tol = 1e-2 - - with CVParamShiftTape() as tape: - qml.Displacement(0.5, 0, wires=0) - qml.apply(op) - qml.Beamsplitter(1.3, -2.3, wires=[0, 1]) - qml.Displacement(-0.5, 0.1, wires=0) - qml.Squeezing(0.5, -1.5, wires=0) - qml.Rotation(-1.1, wires=0) - qml.expval(obs(wires=0)) - - dev = qml.device("default.gaussian", wires=2) - res = tape.execute(dev) - - tape._update_gradient_info() - tape.trainable_params = set(range(2, 2 + op.num_params)) - - # check that every parameter is analytic - for i in range(op.num_params): - assert tape._par_info[2 + i]["grad_method"][0] == "A" - - spy = mocker.spy(CVParamShiftTape, "parameter_shift_first_order") - grad_F = tape.jacobian(dev, method="numeric") - grad_A2 = tape.jacobian(dev, method="analytic", force_order2=True) - - spy.assert_not_called() - assert np.allclose(grad_A2, grad_F, atol=tol, rtol=0) - - if obs.ev_order == 1: - grad_A = tape.jacobian(dev, method="analytic") - spy.assert_called() - assert np.allclose(grad_A, grad_F, atol=tol, rtol=0) - - @pytest.mark.parametrize("t", [0, 1]) - def test_interferometer_unitary(self, t, tol): - """An integration test for CV gates that support analytic differentiation - if succeeding the gate to be differentiated, but cannot be differentiated - themselves (for example, they may be Gaussian but accept no parameters, - or may accept a numerical array parameter.). - - This ensures that, assuming their _heisenberg_rep is defined, the quantum - gradient analytic method can still be used, and returns the correct result. - - Currently, the only such operation is qml.InterferometerUnitary. In the future, - we may consider adding a qml.GaussianTransfom operator. - """ - - if t == 1: - pytest.xfail( - "There is a bug in the second order CV parameter-shift rule; " - "phase arguments return the incorrect derivative." - ) - - # Note: this bug currently affects PL core as well: - # - # dev = qml.device("default.gaussian", wires=2) - # - # U = np.array([[ 0.51310276+0.81702166j, 0.13649626+0.22487759j], - # [ 0.26300233+0.00556194j, -0.96414101-0.03508489j]]) - # - # @qml.qnode(dev) - # def circuit(r, phi): - # qml.Displacement(r, phi, wires=0) - # qml.InterferometerUnitary(U, wires=[0, 1]) - # return qml.expval(qml.X(0)) - - # - # r = 0.543 - # phi = 0. - # - # >>> print(circuit.jacobian([r, phi], options={"force_order2":False})) - # [[ 1.02620552 0.14823494]] - # >>> print(circuit.jacobian([r, phi], options={"force_order2":True})) - # [[ 1.02620552 -0.88728552]] - - U = np.array( - [ - [0.51310276 + 0.81702166j, 0.13649626 + 0.22487759j], - [0.26300233 + 0.00556194j, -0.96414101 - 0.03508489j], - ] - ) - - with CVParamShiftTape() as tape: - qml.Displacement(0.543, 0, wires=0) - qml.InterferometerUnitary(U, wires=[0, 1]) - qml.expval(qml.X(0)) - - tape._update_gradient_info() - tape.trainable_params = {t} - assert tape._par_info[0]["grad_method"] == "A" - assert tape._par_info[1]["grad_method"] == "A" - - dev = qml.device("default.gaussian", wires=2) - grad_F = tape.jacobian(dev, method="numeric") - grad_A = tape.jacobian(dev, method="analytic") - grad_A2 = tape.jacobian(dev, method="analytic", force_order2=True) - - # the different methods agree - assert np.allclose(grad_A, grad_F, atol=tol, rtol=0) - assert np.allclose(grad_A2, grad_F, atol=tol, rtol=0) - - -class TestVarianceQuantumGradients: - """Tests for the quantum gradients of various gates - with variance measurements""" - - def test_first_order_observable(self, tol): - """Test variance of a first order CV observable""" - dev = qml.device("default.gaussian", wires=1) - - r = 0.543 - phi = -0.654 - - with CVParamShiftTape() as tape: - qml.Squeezing(r, 0, wires=0) - qml.Rotation(phi, wires=0) - qml.var(qml.X(0)) - - tape.trainable_params = {0, 2} - - res = tape.execute(dev) - expected = np.exp(2 * r) * np.sin(phi) ** 2 + np.exp(-2 * r) * np.cos(phi) ** 2 - assert np.allclose(res, expected, atol=tol, rtol=0) - - # circuit jacobians - grad_F = tape.jacobian(dev, method="numeric") - grad_A = tape.jacobian(dev, method="analytic") - expected = np.array( - [ - [ - 2 * np.exp(2 * r) * np.sin(phi) ** 2 - 2 * np.exp(-2 * r) * np.cos(phi) ** 2, - 2 * np.sinh(2 * r) * np.sin(2 * phi), - ] - ] - ) - assert np.allclose(grad_A, expected, atol=tol, rtol=0) - assert np.allclose(grad_F, expected, atol=tol, rtol=0) - - def test_second_order_cv(self, tol): - """Test variance of a second order CV expectation value""" - dev = qml.device("default.gaussian", wires=1) - - n = 0.12 - a = 0.765 - - with CVParamShiftTape() as tape: - qml.ThermalState(n, wires=0) - qml.Displacement(a, 0, wires=0) - qml.var(qml.NumberOperator(0)) - - tape.trainable_params = {0, 1} - - res = tape.execute(dev) - expected = n**2 + n + np.abs(a) ** 2 * (1 + 2 * n) - assert np.allclose(res, expected, atol=tol, rtol=0) - - # circuit jacobians - grad_F = tape.jacobian(dev, method="numeric") - expected = np.array([[2 * a**2 + 2 * n + 1, 2 * a * (2 * n + 1)]]) - assert np.allclose(grad_F, expected, atol=tol, rtol=0) - - def test_expval_and_variance(self, tol): - """Test that the gradient works for a combination of CV expectation - values and variances""" - dev = qml.device("default.gaussian", wires=3) - - a, b = [0.54, -0.423] - - with CVParamShiftTape() as tape: - qml.Displacement(0.5, 0, wires=0) - qml.Squeezing(a, 0, wires=0) - qml.Squeezing(b, 0, wires=1) - qml.Beamsplitter(0.6, -0.3, wires=[0, 1]) - qml.Squeezing(-0.3, 0, wires=2) - qml.Beamsplitter(1.4, 0.5, wires=[1, 2]) - qml.var(qml.X(0)) - qml.expval(qml.X(1)) - qml.var(qml.X(2)) - - tape.trainable_params = {2, 4} - - # jacobians must match - grad_F = tape.jacobian(dev, method="numeric") - grad_A = tape.jacobian(dev, method="analytic") - assert np.allclose(grad_A, grad_F, atol=tol, rtol=0) - - def test_error_analytic_second_order(self): - """Test exception raised if attempting to use a second - order observable to compute the variance derivative analytically""" - dev = qml.device("default.gaussian", wires=1) - - with CVParamShiftTape() as tape: - qml.Displacement(1.0, 0, wires=0) - qml.var(qml.NumberOperator(0)) - - tape.trainable_params = {0} - - with pytest.raises(ValueError, match=r"cannot be used with the argument\(s\) \{0\}"): - tape.jacobian(dev, method="analytic") - - def test_error_unsupported_grad_recipe(self, monkeypatch): - """Test exception raised if attempting to use the second order rule for - computing the gradient analytically of an expectation value that - contains an operation with more than two terms in the gradient recipe""" - - class DummyOp(qml.operation.CVOperation): - num_wires = 1 - num_params = 1 - grad_method = "A" - grad_recipe = ([[1, 1, 1], [1, 1, 1], [1, 1, 1]],) - - dev = qml.device("default.gaussian", wires=1) - - dev.operations.add(DummyOp) - - with CVParamShiftTape() as tape: - DummyOp(1, wires=[0]) - qml.expval(qml.X(0)) - - with monkeypatch.context() as m: - tape._par_info[0]["grad_method"] = "A" - tape.trainable_params = {0} - - with pytest.raises( - NotImplementedError, match=r"analytic gradient for order-2 operators is unsupported" - ): - tape.jacobian(dev, method="analytic", force_order2=True) - - @pytest.mark.parametrize("obs", [qml.X, qml.P, qml.Identity]) - @pytest.mark.parametrize( - "op", [qml.Squeezing(0.1, 0.2, wires=0), qml.Beamsplitter(0.1, 0.2, wires=[0, 1])] - ) - def test_gradients_gaussian_circuit(self, op, obs, mocker, tol): - """Tests that the gradients of circuits of gaussian gates match between the - finite difference and analytic methods.""" - tol = 1e-2 - - args = np.linspace(0.2, 0.5, op.num_params) - - with CVParamShiftTape() as tape: - qml.Displacement(0.5, 0, wires=0) - qml.apply(op) - qml.Beamsplitter(1.3, -2.3, wires=[0, 1]) - qml.Displacement(-0.5, 0.1, wires=0) - qml.Squeezing(0.5, -1.5, wires=0) - qml.Rotation(-1.1, wires=0) - qml.var(obs(wires=0)) - - dev = qml.device("default.gaussian", wires=2) - res = tape.execute(dev) - - tape._update_gradient_info() - tape.trainable_params = set(range(2, 2 + op.num_params)) - - # check that every parameter is analytic - for i in range(op.num_params): - assert tape._par_info[2 + i]["grad_method"][0] == "A" - - spy = mocker.spy(CVParamShiftTape, "parameter_shift_first_order") - grad_F = tape.jacobian(dev, method="numeric") - grad_A = tape.jacobian(dev, method="analytic") - grad_A2 = tape.jacobian(dev, method="analytic", force_order2=True) - - assert np.allclose(grad_A2, grad_F, atol=tol, rtol=0) - assert np.allclose(grad_A, grad_F, atol=tol, rtol=0) - - def test_squeezed_mean_photon_variance(self, tol): - """Test gradient of the photon variance of a displaced thermal state""" - dev = qml.device("default.gaussian", wires=1) - - r = 0.12 - phi = 0.105 - - with CVParamShiftTape() as tape: - qml.Squeezing(r, 0, wires=0) - qml.Rotation(phi, wires=0) - qml.var(qml.X(wires=[0])) - - tape.trainable_params = {0, 2} - grad = tape.jacobian(dev, method="analytic") - expected = np.array( - [ - 2 * np.exp(2 * r) * np.sin(phi) ** 2 - 2 * np.exp(-2 * r) * np.cos(phi) ** 2, - 2 * np.sinh(2 * r) * np.sin(2 * phi), - ] - ) - assert np.allclose(grad, expected, atol=tol, rtol=0) - - def test_displaced_thermal_mean_photon_variance(self, tol): - """Test gradient of the photon variance of a displaced thermal state""" - dev = qml.device("default.gaussian", wires=1) - - n = 0.12 - a = 0.105 - - with CVParamShiftTape() as tape: - qml.ThermalState(n, wires=0) - qml.Displacement(a, 0, wires=0) - qml.var(qml.TensorN(wires=[0])) - - tape.trainable_params = {0, 1} - grad = tape.jacobian(dev) - expected = np.array([2 * a**2 + 2 * n + 1, 2 * a * (2 * n + 1)]) - assert np.allclose(grad, expected, atol=tol, rtol=0) diff --git a/tests/tape/test_jacobian_tape.py b/tests/tape/test_jacobian_tape.py deleted file mode 100644 index 607e0c1e4f5..00000000000 --- a/tests/tape/test_jacobian_tape.py +++ /dev/null @@ -1,878 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the JacobianTape""" -import pytest -import numpy as np - -import pennylane as qml -from pennylane import numpy as pnp -from pennylane.tape import JacobianTape, QuantumTape -from pennylane.devices import DefaultQubit -from pennylane.operation import Observable -from pennylane.operation import AnyWires - - -class TestConstruction: - """Test for queuing and construction""" - - @pytest.fixture - def make_tape(self): - ops = [] - obs = [] - - with JacobianTape() as tape: - ops += [qml.RX(0.432, wires=0)] - ops += [qml.Rot(0.543, 0, 0.23, wires=0)] - ops += [qml.CNOT(wires=[0, "a"])] - ops += [qml.RX(0.133, wires=4)] - obs += [qml.PauliX(wires="a")] - qml.expval(obs[0]) - obs += [qml.probs(wires=[0, "a"])] - - return tape, ops, obs - - def test_parameter_info(self, make_tape): - """Test that parameter information is correctly extracted""" - tape, ops, obs = make_tape - tape._update_gradient_info() - assert tape._trainable_params == list(range(5)) - assert tape._par_info == { - 0: {"op": ops[0], "p_idx": 0, "grad_method": "F"}, - 1: {"op": ops[1], "p_idx": 0, "grad_method": "F"}, - 2: {"op": ops[1], "p_idx": 1, "grad_method": "F"}, - 3: {"op": ops[1], "p_idx": 2, "grad_method": "F"}, - 4: {"op": ops[3], "p_idx": 0, "grad_method": "0"}, - } - - -class TestTapeCopying: - """Test for tape copying behaviour""" - - def test_jacobian_options_copied(self): - """Tests that the jacobian_options attribute is copied""" - - tape = JacobianTape() - tape.jacobian_options = {"method": "device", "jacobian_method": "adjoint_jacobian"} - - tape_copy = tape.copy() - - assert tape_copy.jacobian_options == { - "method": "device", - "jacobian_method": "adjoint_jacobian", - } - - -class TestGradMethod: - """Tests for parameter gradient methods""" - - def test_non_differentiable(self): - """Test that a non-differentiable parameter is - correctly marked""" - psi = np.array([1, 0, 1, 0]) / np.sqrt(2) - - with JacobianTape() as tape: - qml.QubitStateVector(psi, wires=[0, 1]) - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=[0, 1]) - - assert tape._grad_method(0) is None - assert tape._grad_method(1) == "F" - assert tape._grad_method(2) == "F" - - tape._update_gradient_info() - - assert tape._par_info[0]["grad_method"] is None - assert tape._par_info[1]["grad_method"] == "F" - assert tape._par_info[2]["grad_method"] == "F" - - def test_independent(self): - """Test that an independent variable is properly marked - as having a zero gradient""" - - with JacobianTape() as tape: - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[1]) - qml.expval(qml.PauliY(0)) - - assert tape._grad_method(0) == "F" - assert tape._grad_method(1) == "0" - - tape._update_gradient_info() - - assert tape._par_info[0]["grad_method"] == "F" - assert tape._par_info[1]["grad_method"] == "0" - - # in non-graph mode, it is impossible to determine - # if a parameter is independent or not - tape._graph = None - assert tape._grad_method(1, use_graph=False) == "F" - - -class TestJacobian: - """Unit tests for the jacobian method""" - - def test_unknown_grad_method_error(self): - """Test error raised if gradient method is unknown""" - tape = JacobianTape() - with pytest.raises(ValueError, match="Unknown gradient method"): - tape.jacobian(None, method="unknown method") - - def test_non_differentiable_error(self): - """Test error raised if attempting to differentiate with - respect to a non-differentiable argument""" - psi = np.array([1, 0, 1, 0]) / np.sqrt(2) - - with JacobianTape() as tape: - qml.QubitStateVector(psi, wires=[0, 1]) - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=[0, 1]) - - # by default all parameters are assumed to be trainable - with pytest.raises( - ValueError, match=r"Cannot differentiate with respect to parameter\(s\) {0}" - ): - tape.jacobian(None) - - # setting trainable parameters avoids this - tape.trainable_params = [1, 2] - dev = qml.device("default.qubit", wires=2) - res = tape.jacobian(dev) - assert res.shape == (4, 2) - - def test_non_differentiable_state_error(self): - """Test error raised if attempting to differentiate a circuit - that returns the state.""" - with JacobianTape() as tape: - qml.RX(0.543, wires=[0]) - qml.state() - - # by default all parameters are assumed to be trainable - with pytest.raises(ValueError, match=r"does not support circuits that return the state"): - tape.jacobian(None) - - def test_non_differentiable_sampling_error(self): - """Test error raised if attempting to differentiate a circuit - that returns samples.""" - with JacobianTape() as tape: - qml.RX(0.543, wires=[0]) - qml.sample() - - # by default all parameters are assumed to be trainable - with pytest.raises(qml.QuantumFunctionError, match=r"sampling can not be differentiated"): - tape.jacobian(None) - - def test_analytic_method_with_unsupported_params(self): - """Test that an exception is raised if method="A" but a parameter - only support finite differences""" - with JacobianTape() as tape: - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[0]) - qml.expval(qml.PauliY(0)) - - dev = qml.device("default.qubit", wires=1) - - with pytest.raises(ValueError, match=r"analytic gradient method cannot be used"): - tape.jacobian(dev, method="analytic") - - def test_analytic_method(self, mocker): - """Test that calling the Jacobian with method=analytic correctly - calls the analytic_pd method""" - mock = mocker.patch("pennylane.tape.JacobianTape._grad_method") - mock.return_value = "A" - - with JacobianTape() as tape: - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[0]) - qml.expval(qml.PauliY(0)) - - dev = qml.device("default.qubit", wires=1) - tape.analytic_pd = mocker.Mock() - tape.analytic_pd.return_value = [[QuantumTape()], lambda res: np.array([1.0])] - - tape.jacobian(dev, method="analytic") - assert len(tape.analytic_pd.call_args_list) == 2 - - def test_device_method(self, mocker): - """Test that calling the Jacobian with method=device correctly - calls the device_pd method""" - with JacobianTape() as tape: - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[0]) - qml.expval(qml.PauliY(0)) - - dev = qml.device("default.qubit", wires=1) - - dev.jacobian = mocker.Mock() - tape.device_pd(dev) - dev.jacobian.assert_called_once() - - dev.jacobian = mocker.Mock() - tape.jacobian(dev, method="device") - dev.jacobian.assert_called_once() - - def test_no_output_execute(self): - """Test that tapes with no measurement process return - an empty list.""" - dev = qml.device("default.qubit", wires=2) - params = [0.1, 0.2] - - with JacobianTape() as tape: - qml.RX(params[0], wires=[0]) - qml.RY(params[1], wires=[1]) - - res = tape.jacobian(dev) - assert res.size == 0 - - def test_incorrect_inferred_output_dim(self): - """Test that a quantum tape with an incorrect inferred output dimension - corrects itself when computing the Jacobian.""" - dev = qml.device("default.qubit", wires=3) - params = [1.0, 1.0, 1.0] - - with JacobianTape() as tape: - qml.RX(params[0], wires=[0]) - qml.RY(params[1], wires=[1]) - qml.RZ(params[2], wires=[2]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=0) - qml.probs(wires=[1]) - - # inferred output dim should be correct - assert tape.output_dim == sum([2, 2]) - - # modify the output dim - tape._output_dim = 2 - - res = tape.jacobian(dev, order=2, method="numeric") - - # output dim should be correct - assert tape.output_dim == sum([2, 2]) - assert res.shape == (4, 3) - - def test_incorrect_ragged_output_dim(self, mocker): - """Test that a quantum tape with an incorrect inferred *ragged* output dimension - corrects itself after evaluation.""" - dev = qml.device("default.qubit", wires=3) - params = [1.0, 1.0, 1.0] - - with JacobianTape() as tape: - qml.RX(params[0], wires=[0]) - qml.RY(params[1], wires=[1]) - qml.RZ(params[2], wires=[2]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=0) - qml.probs(wires=[1, 2]) - - # inferred output dim should be correct - assert tape.output_dim == sum([2, 4]) - - # modify the output dim - tape._output_dim = 2 - - res = tape.jacobian(dev, order=2, method="numeric") - - # output dim should be correct - assert tape.output_dim == sum([2, 4]) - assert res.shape == (6, 3) - - def test_independent_parameter(self, mocker): - """Test that an independent parameter is skipped - during the Jacobian computation.""" - numeric_spy = mocker.spy(JacobianTape, "numeric_pd") - analytic_spy = mocker.spy(JacobianTape, "analytic_pd") - - with JacobianTape() as tape: - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[1]) - qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=2) - res = tape.jacobian(dev) - assert res.shape == (1, 2) - - # the numeric pd method is only called once - assert len(numeric_spy.call_args_list) == 1 - - # analytic pd should not be called at all - assert len(analytic_spy.call_args_list) == 0 - - # the numeric pd method is only called for parameter 0 - assert numeric_spy.call_args[0] == (tape, 0) - - def test_no_trainable_parameters(self, mocker): - """Test that if the tape has no trainable parameters, no - subroutines are called and the returned Jacobian is empty""" - numeric_spy = mocker.spy(JacobianTape, "numeric_pd") - analytic_spy = mocker.spy(JacobianTape, "analytic_pd") - - with JacobianTape() as tape: - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[1]) - qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=2) - tape.trainable_params = [] - - res = tape.jacobian(dev) - assert res.size == 0 - assert np.all(res == np.array([[]])) - - numeric_spy.assert_not_called() - analytic_spy.assert_not_called() - - def test_y0(self, mocker): - """Test that if first order finite differences is used, then - the tape is executed only once using the current parameter - values.""" - dev = qml.device("default.qubit", wires=2) - execute_spy = mocker.spy(dev, "execute") - numeric_spy = mocker.spy(JacobianTape, "numeric_pd") - - with JacobianTape() as tape: - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[0]) - qml.expval(qml.PauliZ(0)) - - res = tape.jacobian(dev, order=1) - - # the execute device method is called once per parameter, - # plus one global call - assert len(execute_spy.call_args_list) == tape.num_params + 1 - assert "y0" in numeric_spy.call_args_list[0][1] - assert "y0" in numeric_spy.call_args_list[1][1] - - def test_parameters(self, tol): - """Test Jacobian computation works when parameters are both passed and not passed.""" - dev = qml.device("default.qubit", wires=2) - params = [0.1, 0.2] - - with JacobianTape() as tape: - qml.RX(params[0], wires=[0]) - qml.RY(params[1], wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - # test Jacobian with no parameters - res1 = tape.jacobian(dev) - assert tape.get_parameters() == params - - # test Jacobian with parameters - res2 = tape.jacobian(dev, params=[0.5, 0.6]) - assert tape.get_parameters() == params - - # test setting parameters - tape.set_parameters(params=[0.5, 0.6]) - res3 = tape.jacobian(dev) - assert np.allclose(res2, res3, atol=tol, rtol=0) - assert not np.allclose(res1, res2, atol=tol, rtol=0) - assert tape.get_parameters() == [0.5, 0.6] - - def test_numeric_pd_no_y0(self, tol): - """Test that, if y0 is not passed when calling the numeric_pd method, - y0 is calculated.""" - dev = qml.device("default.qubit", wires=2) - - params = [0.1, 0.2] - - with JacobianTape() as tape: - qml.RX(params[0], wires=[0]) - qml.RY(params[1], wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - # compute numeric gradient of parameter 0, without passing y0 - tapes, fn = tape.numeric_pd(0) - assert len(tapes) == 2 - - res1 = fn([tape.execute(dev) for tape in tapes]) - - # compute y0 in advance - y0 = tape.execute(dev) - tapes, fn = tape.numeric_pd(0, y0=y0) - assert len(tapes) == 1 - - res2 = fn([tape.execute(dev) for tape in tapes]) - - assert np.allclose(res1, res2, atol=tol, rtol=0) - - def test_numeric_unknown_order(self): - """Test that an exception is raised if the finite-difference - order is not supported""" - dev = qml.device("default.qubit", wires=2) - params = [0.1, 0.2] - - with JacobianTape() as tape: - qml.RX(1, wires=[0]) - qml.RY(1, wires=[1]) - qml.RZ(1, wires=[2]) - qml.CNOT(wires=[0, 1]) - - qml.expval(qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliZ(2)) - - with pytest.raises(ValueError, match="Order must be 1 or 2"): - tape.jacobian(dev, order=3) - - def test_independent_parameters(self): - """Test the case where expectation values are independent of some parameters. For those - parameters, the gradient should be evaluated to zero without executing the device.""" - dev = qml.device("default.qubit", wires=2) - - with JacobianTape() as tape1: - qml.RX(1, wires=[0]) - qml.RX(1, wires=[1]) - qml.expval(qml.PauliZ(0)) - - with JacobianTape() as tape2: - qml.RX(1, wires=[0]) - qml.RX(1, wires=[1]) - qml.expval(qml.PauliZ(1)) - - j1 = tape1.jacobian(dev) - - # We should only be executing the device to differentiate 1 parameter (2 executions) - assert dev.num_executions == 2 - - j2 = tape2.jacobian(dev) - - exp = -np.sin(1) - - assert np.allclose(j1, [exp, 0]) - assert np.allclose(j2, [0, exp]) - - @pytest.mark.parametrize( - "diff_methods", [["A", "0", "F"], ["A", "A", "A"], ["F", "A", "A", "0", "0"]] - ) - @pytest.mark.parametrize("argnum", [None, 0, [0, 1], [0, 1, 2], [2, 0], [1, 0], [0, 0, 0]]) - def test_choose_params_and_methods(self, diff_methods, argnum): - """Test that the _choose_params_and_methods helper method returns - expected results""" - tape = JacobianTape() - tape._trainable_params = list(range(len(diff_methods))) - res = list(tape._choose_params_with_methods(diff_methods, argnum)) - - num_all_params = len(diff_methods) - - assert all(k in range(num_all_params) for k, _ in res) - assert all(v in diff_methods for _, v in res) - - if argnum is None: - num_params = num_all_params - elif isinstance(argnum, int): - num_params = 1 - else: - num_params = len(argnum) - - assert len(res) == num_params - - def test_choose_params_and_methods_warns_no_params(self): - """Test that the _choose_params_and_methods helper method warns if an - empty list was passed as argnum.""" - tape = JacobianTape() - tape.trainable_params = [0] - diff_methods = ["F"] - argnum = [] - with pytest.warns( - UserWarning, - match="No trainable parameters", - ): - res = tape._choose_params_with_methods(diff_methods, argnum) - - def test_hamiltonian_grad(self): - """Test that the gradient of Hamiltonians works as expected.""" - dev = qml.device("default.qubit", wires=2) - - with qml.tape.JacobianTape() as tape: - qml.RY(0.3, wires=0) - qml.RX(0.5, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.Hamiltonian([-1.5, 2.0], [qml.PauliZ(0), qml.PauliZ(1)])) - - tape.trainable_params = {2, 3} - res = qml.math.stack(tape.jacobian(dev)[0]) - - with qml.tape.JacobianTape() as tape1: - qml.RY(0.3, wires=0) - qml.RX(0.5, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - with qml.tape.JacobianTape() as tape2: - qml.RY(0.3, wires=0) - qml.RX(0.5, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(1)) - - expected = qml.math.stack(qml.execute([tape1, tape2], dev, None)) - assert np.allclose(expected, res) - - -class TestJacobianIntegration: - """Integration tests for the Jacobian method""" - - def test_ragged_output(self): - """Test that the Jacobian is correctly returned for a tape - with ragged output""" - dev = qml.device("default.qubit", wires=3) - params = [1.0, 1.0, 1.0] - - with JacobianTape() as tape: - qml.RX(params[0], wires=[0]) - qml.RY(params[1], wires=[1]) - qml.RZ(params[2], wires=[2]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=0) - qml.probs(wires=[1, 2]) - - res = tape.jacobian(dev) - assert res.shape == (6, 3) - - def test_single_expectation_value(self, tol): - """Tests correct output shape and evaluation for a tape - with a single expval output""" - dev = qml.device("default.qubit", wires=2) - x = 0.543 - y = -0.654 - - with JacobianTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - res = tape.jacobian(dev) - assert res.shape == (1, 2) - - expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]]) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_single_expectation_value_with_argnum_all(self, tol): - """Tests correct output shape and evaluation for a tape - with a single expval output where all parameters are chose to compute - the jacobian""" - dev = qml.device("default.qubit", wires=2) - x = 0.543 - y = -0.654 - - with JacobianTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - res = tape.jacobian(dev, argnum=[0, 1]) # <--- we choose both trainable parameters - assert res.shape == (1, 2) - - expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]]) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_single_expectation_value_with_argnum_one(self, tol): - """Tests correct output shape and evaluation for a tape - with a single expval output where only one parameter is chosen to - estimate the jacobian. - - This test relies on the fact that exactly one term of the estimated - jacobian will match the expected analytical value. - """ - dev = qml.device("default.qubit", wires=2) - x = 0.543 - y = -0.654 - - with JacobianTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - res = tape.jacobian(dev, argnum=1) # <--- we only choose one trainable parameter - assert res.shape == (1, 2) - - expected = np.array([[0, np.cos(y) * np.cos(x)]]) - res = res.flatten() - expected = expected.flatten() - - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_multiple_expectation_values(self, tol): - """Tests correct output shape and evaluation for a tape - with multiple expval outputs""" - dev = qml.device("default.qubit", wires=2) - x = 0.543 - y = -0.654 - - with JacobianTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliX(1)) - - res = tape.jacobian(dev) - assert res.shape == (2, 2) - - expected = np.array([[-np.sin(x), 0], [0, np.cos(y)]]) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_var_expectation_values(self, tol): - """Tests correct output shape and evaluation for a tape - with expval and var outputs""" - dev = qml.device("default.qubit", wires=2) - x = 0.543 - y = -0.654 - - with JacobianTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.var(qml.PauliX(1)) - - res = tape.jacobian(dev) - assert res.shape == (2, 2) - - expected = np.array([[-np.sin(x), 0], [0, -2 * np.cos(y) * np.sin(y)]]) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_prob_expectation_values(self, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - dev = qml.device("default.qubit", wires=2) - x = 0.543 - y = -0.654 - - with JacobianTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.probs(wires=[0, 1]) - - res = tape.jacobian(dev) - assert res.shape == (5, 2) - - expected = ( - np.array( - [ - [-2 * np.sin(x), 0], - [ - -(np.cos(y / 2) ** 2 * np.sin(x)), - -(np.cos(x / 2) ** 2 * np.sin(y)), - ], - [ - -(np.sin(x) * np.sin(y / 2) ** 2), - (np.cos(x / 2) ** 2 * np.sin(y)), - ], - [ - (np.sin(x) * np.sin(y / 2) ** 2), - (np.sin(x / 2) ** 2 * np.sin(y)), - ], - [ - (np.cos(y / 2) ** 2 * np.sin(x)), - -(np.sin(x / 2) ** 2 * np.sin(y)), - ], - ] - ) - / 2 - ) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - -class TestJacobianCVIntegration: - """Intgration tests for the Jacobian method and CV circuits""" - - def test_single_output_value(self, tol): - """Tests correct Jacobian and output shape for a CV tape - with a single output""" - dev = qml.device("default.gaussian", wires=2) - n = 0.543 - a = -0.654 - - with JacobianTape() as tape: - qml.ThermalState(n, wires=0) - qml.Displacement(a, 0, wires=0) - qml.var(qml.NumberOperator(0)) - - tape.trainable_params = [0, 1] - res = tape.jacobian(dev) - assert res.shape == (1, 2) - - expected = np.array([2 * a**2 + 2 * n + 1, 2 * a * (2 * n + 1)]) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_multiple_output_values(self, tol): - """Tests correct output shape and evaluation for a tape - with multiple outputs""" - dev = qml.device("default.gaussian", wires=2) - n = 0.543 - a = -0.654 - - with JacobianTape() as tape: - qml.ThermalState(n, wires=0) - qml.Displacement(a, 0, wires=0) - qml.expval(qml.NumberOperator(1)) - qml.var(qml.NumberOperator(0)) - - tape.trainable_params = [0, 1] - res = tape.jacobian(dev) - assert res.shape == (2, 2) - - expected = np.array([[0, 0], [2 * a**2 + 2 * n + 1, 2 * a * (2 * n + 1)]]) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_trainable_measurement(self, tol): - """Test that a trainable measurement can be differentiated""" - dev = qml.device("default.gaussian", wires=2) - a = 0.32 - phi = 0.54 - - with JacobianTape() as tape: - qml.Displacement(a, 0, wires=0) - qml.expval(qml.QuadOperator(phi, wires=0)) - - tape.trainable_params = [2] - res = tape.jacobian(dev) - expected = np.array([[-2 * a * np.sin(phi)]]) - assert np.allclose(res, expected, atol=tol, rtol=0) - - -class TestHessian: - """Unit tests for the hessian method""" - - def test_non_differentiable_error(self): - """Test error raised if attempting to differentiate with respect to a - non-differentiable argument""" - psi = np.array([1, 0, 1, 0]) / np.sqrt(2) - - with JacobianTape() as tape: - qml.QubitStateVector(psi, wires=[0, 1]) - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=[0, 1]) - - # by default all parameters are assumed to be trainable - with pytest.raises( - ValueError, match=r"Cannot differentiate with respect to parameter\(s\) {0}" - ): - tape.hessian(None) - - def test_unknown_hessian_method_error(self): - """Test error raised if gradient method is unknown.""" - tape = JacobianTape() - with pytest.raises(ValueError, match="Unknown Hessian method"): - tape.hessian(None, method="unknown method") - - def test_return_state_hessian_error(self): - """Test error raised if circuit returns the state.""" - psi = np.array([1, 0, 1, 0]) / np.sqrt(2) - - with JacobianTape() as tape: - qml.QubitStateVector(psi, wires=[0, 1]) - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.state() - - with pytest.raises( - ValueError, - match=r"The Hessian method does not support circuits that return the state", - ): - tape.hessian(None) - - -@pytest.mark.parametrize("qn", [qml.qnode, qml.qnode_old.qnode]) -class TestObservableWithObjectReturnType: - """Unit tests for differentiation of observables returning an object""" - - def test_special_observable_qnode_differentiation(self, qn): - """Test differentiation of a QNode on a device supporting a - special observable that returns an object rathern than a nummber.""" - - class SpecialObject: - """SpecialObject - - A special object that conveniently encapsulates the return value of - a special observable supported by a special device and which supports - multiplication with scalars and addition. - """ - - def __init__(self, val): - self.val = val - - def __mul__(self, other): - new = SpecialObject(self.val) - new *= other - return new - - def __imul__(self, other): - self.val *= other - return self - - def __rmul__(self, other): - return self * other - - def __iadd__(self, other): - self.val += other.val if isinstance(other, self.__class__) else other - return self - - def __add__(self, other): - new = SpecialObject(self.val) - new += other.val if isinstance(other, self.__class__) else other - return new - - def __radd__(self, other): - return self + other - - class SpecialObservable(Observable): - """SpecialObservable""" - - num_wires = AnyWires - - def diagonalizing_gates(self): - """Diagonalizing gates""" - return [] - - class DeviceSupporingSpecialObservable(DefaultQubit): - name = "Device supporing SpecialObservable" - short_name = "default.qibit.specialobservable" - observables = DefaultQubit.observables.union({"SpecialObservable"}) - - def expval(self, observable, **kwargs): - if self.analytic and isinstance(observable, SpecialObservable): - val = super().expval(qml.PauliZ(wires=0), **kwargs) - return SpecialObject(val) - - return super().expval(observable, **kwargs) - - dev = DeviceSupporingSpecialObservable(wires=1, shots=None) - - # force diff_method='parameter-shift' because otherwise - # PennyLane swaps out dev for default.qubit.autograd - @qn(dev, diff_method="parameter-shift") - def qnode(x): - qml.RY(x, wires=0) - return qml.expval(SpecialObservable(wires=0)) - - @qn(dev, diff_method="parameter-shift") - def reference_qnode(x): - qml.RY(x, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - par = pnp.array(0.2, requires_grad=True) - assert np.isclose(qnode(par).item().val, reference_qnode(par)) - assert np.isclose(qml.jacobian(qnode)(par).item().val, qml.jacobian(reference_qnode)(par)) diff --git a/tests/tape/test_qnode_old.py b/tests/tape/test_qnode_old.py deleted file mode 100644 index 668ae05fc08..00000000000 --- a/tests/tape/test_qnode_old.py +++ /dev/null @@ -1,1541 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the QNode""" -import pytest -import numpy as np -from collections import defaultdict - -import pennylane as qml -from pennylane import numpy as pnp -from pennylane import QNodeCollection -from pennylane.qnode_old import qnode, QNode -from pennylane.transforms import draw -from pennylane.tape import JacobianTape, QubitParamShiftTape, CVParamShiftTape - - -def dummyfunc(): - return None - - -class TestValidation: - """Tests for QNode creation and validation""" - - def test_invalid_interface(self): - """Test that an exception is raised for an invalid interface""" - dev = qml.device("default.qubit", wires=1) - test_interface = "something" - expected_error = ( - rf"Unknown interface {test_interface}\. Interface must be " - r"one of \['autograd', 'torch', 'tf', 'jax'\]\." - ) - - with pytest.raises(qml.QuantumFunctionError, match=expected_error): - QNode(dummyfunc, dev, interface="something") - - def test_invalid_device(self): - """Test that an exception is raised for an invalid device""" - with pytest.raises(qml.QuantumFunctionError, match="Invalid device"): - QNode(dummyfunc, None) - - def test_validate_device_method(self, monkeypatch): - """Test that the method for validating the device diff method - tape works as expected""" - dev = qml.device("default.qubit", wires=1) - - with pytest.raises( - qml.QuantumFunctionError, - match="does not provide a native method for computing the jacobian", - ): - QNode._validate_device_method(dev, None) - - monkeypatch.setitem(dev._capabilities, "provides_jacobian", True) - tape_class, interface, device, diff_options = QNode._validate_device_method( - dev, "interface" - ) - method = diff_options["method"] - - assert tape_class is JacobianTape - assert method == "device" - assert interface == "interface" - assert device is dev - - @pytest.mark.parametrize("interface", ("autograd", "torch", "tensorflow", "jax")) - def test_validate_backprop_method_finite_shots(self, interface): - """Tests that an error is raised for backpropagation with finite shots.""" - - dev = qml.device("default.qubit", wires=1, shots=3) - - with pytest.raises(qml.QuantumFunctionError, match="Devices with finite shots"): - QNode._validate_backprop_method(dev, interface) - - def test_validate_backprop_method_invalid_device(self): - """Test that the method for validating the backprop diff method - tape raises an exception if the device does not support backprop.""" - dev = qml.device("default.gaussian", wires=1) - - with pytest.raises(qml.QuantumFunctionError, match="does not support native computations"): - QNode._validate_backprop_method(dev, None) - - def test_validate_backprop_method_invalid_interface(self, monkeypatch): - """Test that the method for validating the backprop diff method - tape raises an exception if the wrong interface is provided""" - dev = qml.device("default.qubit", wires=1) - test_interface = "something" - - monkeypatch.setitem(dev._capabilities, "passthru_interface", test_interface) - - with pytest.raises(qml.QuantumFunctionError, match=f"when using the {test_interface}"): - QNode._validate_backprop_method(dev, None) - - def test_validate_backprop_method(self, monkeypatch): - """Test that the method for validating the backprop diff method - tape works as expected""" - dev = qml.device("default.qubit", wires=1) - test_interface = "something" - monkeypatch.setitem(dev._capabilities, "passthru_interface", test_interface) - - tape_class, interface, device, diff_options = QNode._validate_backprop_method( - dev, test_interface - ) - method = diff_options["method"] - - assert tape_class is JacobianTape - assert method == "backprop" - assert interface == "something" - assert device is dev - - def test_validate_backprop_child_method(self, monkeypatch): - """Test that the method for validating the backprop diff method - tape works as expected if a child device supports backprop""" - dev = qml.device("default.qubit", wires=1) - test_interface = "something" - - orig_capabilities = dev.capabilities().copy() - orig_capabilities["passthru_devices"] = {test_interface: "default.gaussian"} - monkeypatch.setattr(dev, "capabilities", lambda: orig_capabilities) - - tape_class, interface, device, diff_options = QNode._validate_backprop_method( - dev, test_interface - ) - method = diff_options["method"] - - assert tape_class is JacobianTape - assert method == "backprop" - assert interface == "something" - assert isinstance(device, qml.devices.DefaultGaussian) - - def test_validate_backprop_child_method_wrong_interface(self, monkeypatch): - """Test that the method for validating the backprop diff method - tape raises an error if a child device supports backprop but using a different interface""" - dev = qml.device("default.qubit", wires=1) - test_interface = "something" - - orig_capabilities = dev.capabilities().copy() - orig_capabilities["passthru_devices"] = {test_interface: "default.gaussian"} - monkeypatch.setattr(dev, "capabilities", lambda: orig_capabilities) - - with pytest.raises( - qml.QuantumFunctionError, match=r"when using the \['something'\] interface" - ): - QNode._validate_backprop_method(dev, "another_interface") - - def test_parameter_shift_tape_qubit_device(self): - """Test that the get_parameter_shift_tape method correctly and - returns the correct tape for qubit devices.""" - dev = qml.device("default.qubit", wires=1) - tape_class = QNode._get_parameter_shift_tape(dev) - assert tape_class is QubitParamShiftTape - - def test_parameter_shift_tape_cv_device(self): - """Test that the get_parameter_shift_tape method correctly and - returns the correct tape for qubit devices.""" - dev = qml.device("default.gaussian", wires=1) - tape_class = QNode._get_parameter_shift_tape(dev) - assert tape_class is CVParamShiftTape - - def test_parameter_shift_tape_unknown_model(self, monkeypatch): - """test that an unknown model raises an exception""" - - def capabilities(cls): - capabilities = cls._capabilities - capabilities.update(model="None") - return capabilities - - monkeypatch.setattr(qml.devices.DefaultQubit, "capabilities", capabilities) - dev = qml.device("default.qubit", wires=1) - - with pytest.raises( - qml.QuantumFunctionError, match="does not support the parameter-shift rule" - ): - QNode._get_parameter_shift_tape(dev) - - def test_best_method(self, monkeypatch): - """Test that the method for determining the best diff method - for a given device and interface works correctly""" - dev = qml.device("default.qubit", wires=1) - monkeypatch.setitem(dev._capabilities, "passthru_interface", "some_interface") - monkeypatch.setitem(dev._capabilities, "provides_jacobian", True) - - # device is top priority - res = QNode.get_best_method(dev, "another_interface") - assert res == (JacobianTape, "another_interface", dev, {"method": "device"}) - - # backprop is next priority - monkeypatch.setitem(dev._capabilities, "provides_jacobian", False) - res = QNode.get_best_method(dev, "some_interface") - assert res == (JacobianTape, "some_interface", dev, {"method": "backprop"}) - - # The next fallback is parameter-shift. - res = QNode.get_best_method(dev, "another_interface") - assert res == (QubitParamShiftTape, "another_interface", dev, {"method": "best"}) - - # finally, if both fail, finite differences is the fallback - def capabilities(cls): - capabilities = cls._capabilities - capabilities.update(model="None") - return capabilities - - monkeypatch.setattr(qml.devices.DefaultQubit, "capabilities", capabilities) - res = QNode.get_best_method(dev, "another_interface") - assert res == (JacobianTape, "another_interface", dev, {"method": "numeric"}) - - def test_diff_method(self, mocker): - """Test that a user-supplied diff-method correctly returns the right - quantum tape, interface, and diff method.""" - dev = qml.device("default.qubit", wires=1) - - mock_best = mocker.patch("pennylane.qnode_old.QNode.get_best_method") - mock_best.return_value = 1, 2, 3, {"method": "best"} - - mock_backprop = mocker.patch("pennylane.qnode_old.QNode._validate_backprop_method") - mock_backprop.return_value = 4, 5, 6, {"method": "backprop"} - - mock_device = mocker.patch("pennylane.qnode_old.QNode._validate_device_method") - mock_device.return_value = 7, 8, 9, {"method": "device"} - - qn = QNode(dummyfunc, dev, diff_method="best") - assert qn._tape == mock_best.return_value[0] - assert qn.interface == mock_best.return_value[1] - assert qn.diff_options["method"] == mock_best.return_value[3]["method"] - - qn = QNode(dummyfunc, dev, diff_method="backprop") - assert qn._tape == mock_backprop.return_value[0] - assert qn.interface == mock_backprop.return_value[1] - assert qn.diff_options["method"] == mock_backprop.return_value[3]["method"] - mock_backprop.assert_called_once() - - qn = QNode(dummyfunc, dev, diff_method="device") - assert qn._tape == mock_device.return_value[0] - assert qn.interface == mock_device.return_value[1] - assert qn.diff_options["method"] == mock_device.return_value[3]["method"] - mock_device.assert_called_once() - - qn = QNode(dummyfunc, dev, diff_method="finite-diff") - assert qn._tape == JacobianTape - assert qn.diff_options["method"] == "numeric" - - qn = QNode(dummyfunc, dev, diff_method="parameter-shift") - assert qn._tape == QubitParamShiftTape - assert qn.diff_options["method"] == "analytic" - - # check that get_best_method was only ever called once - mock_best.assert_called_once() - - def test_unknown_diff_method(self): - """Test that an exception is raised for an unknown differentiation method""" - dev = qml.device("default.qubit", wires=1) - - with pytest.raises( - qml.QuantumFunctionError, match="Differentiation method hello not recognized" - ): - QNode(dummyfunc, dev, diff_method="hello") - - def test_validate_adjoint_invalid_device(self): - """Test if a ValueError is raised when an invalid device is provided to - _validate_adjoint_method""" - - dev = qml.device("default.gaussian", wires=1) - - with pytest.raises(ValueError, match="The default.gaussian device does not"): - QNode._validate_adjoint_method(dev, "tf") - - def test_validate_adjoint_finite_shots(self): - """Test that a UserWarning is raised when device has finite shots""" - - dev = qml.device("default.qubit", wires=1, shots=1) - - with pytest.warns( - UserWarning, match="Requested adjoint differentiation to be computed with finite shots." - ): - QNode._validate_adjoint_method(dev, "autograd") - - def test_adjoint_finite_shots(self): - """Tests that UserWarning is raised with the adjoint differentiation method - on QNode construction when the device has finite shots - """ - - dev = qml.device("default.qubit", wires=1, shots=1) - - with pytest.warns( - UserWarning, match="Requested adjoint differentiation to be computed with finite shots." - ): - - @qml.qnode_old.qnode(dev, diff_method="adjoint") - def circ(): - return qml.expval(qml.PauliZ(0)) - - def test_validate_reversible_finite_shots(self): - """Test that a UserWarning is raised when validating the reversible differentiation method - and using a device that has finite shots - """ - - dev = qml.device("default.qubit", wires=1, shots=1) - - with pytest.warns( - UserWarning, - match="Requested reversible differentiation to be computed with finite shots.", - ): - QNode._validate_reversible_method(dev, "autograd") - - def test_reversible_finite_shots(self): - """Tests that UserWarning is raised with the reversible differentiation method - on QNode construction when the device has finite shots - """ - - dev = qml.device("default.qubit", wires=1, shots=1) - - with pytest.warns( - UserWarning, - match="Requested reversible differentiation to be computed with finite shots.", - ): - - @qml.qnode_old.qnode(dev, diff_method="reversible") - def circ(): - return qml.expval(qml.PauliZ(0)) - - def test_qnode_print(self): - """Test that printing a QNode object yields the right information.""" - dev = qml.device("default.qubit", wires=1) - - def func(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - qn = qml.qnode_old.QNode(func, dev, diff_method="finite-diff") - - assert ( - qn.__repr__() - == "" - ) - assert qn.diff_method_change == False - - def test_qnode_best_diff_method_backprop(self): - """Test that selected "best" diff_method is correctly set to 'backprop'.""" - dev = qml.device("default.qubit", wires=1) - - def func(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - qn = qml.qnode_old.QNode(func, dev) - - assert qn.diff_method == "backprop" - assert qn.diff_method_change - - def test_qnode_best_diff_method_parameter_shift(self): - """Test that selected "best" diff_method is correctly set to 'parameter-shift'.""" - dev = qml.device("default.mixed", wires=1) - - def func(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - qn = qml.qnode_old.QNode(func, dev) - - assert qn.diff_method == "parameter-shift" - assert qn.diff_method_change - - def test_qnode_best_diff_method_device(self, monkeypatch): - """Test that selected "best" diff_method is correctly set to 'device'.""" - dev = qml.device("default.qubit", wires=1) - - def func(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - # Force the "best" method to be "device" - monkeypatch.setitem(dev._capabilities, "passthru_interface", "some_interface") - monkeypatch.setitem(dev._capabilities, "provides_jacobian", True) - qn = qml.qnode_old.QNode(func, dev) - assert qn.diff_method == "device" - assert qn.diff_method_change - - def test_qnode_best_diff_method_finite_diff(self, monkeypatch): - """Test that selected "best" diff_method is correctly set to 'finite-diff'.""" - dev = qml.device("default.qubit", wires=1) - - def func(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - def capabilities(cls): - capabilities = cls._capabilities - capabilities.update(model="None") - return capabilities - - # Force the "best" method to be "finite-diff" - monkeypatch.setitem(dev._capabilities, "provides_jacobian", False) - monkeypatch.setattr(qml.devices.DefaultQubit, "capabilities", capabilities) - qn = qml.qnode_old.QNode(func, dev) - assert qn.diff_method == "finite-diff" - assert qn.diff_method_change - - def test_qnode_best_diff_method_finite_fallback(self): - """Test that selected "best" diff_method is correctly set to 'finite-diff' - in cases where other methods are not available.""" - - # Custom operation which has grad_method="finite_diff" - class MyRX(qml.operation.Operation): - num_wires = 1 - is_composable_rotation = True - basis = "X" - grad_method = "F" - - @staticmethod - def compute_matrix(*params): - return qml.RX.compute_matrix(*params) - - dev = qml.device("default.mixed", wires=3, shots=None) - dev.operations.add("MyRX") - - def circuit(x): - MyRX(x, wires=1) - return qml.expval(qml.PauliZ(1)) - - qnode = qml.qnode_old.QNode(circuit, dev, diff_method="best") - - # Before execution correctly show 'parameter-shift' - assert qnode.diff_method == "parameter-shift" - - par = qml.numpy.array(0.3) - qml.grad(qnode)(par) - - # After execution correctly show 'finite-diff' - assert qnode.diff_method == "finite-diff" - - @pytest.mark.parametrize( - "method", - [ - "best", - "parameter-shift", - "finite-diff", - "reversible", - "adjoint", - "backprop", - ], - ) - def test_to_tf(self, method, mocker): - """Test if interface change is working""" - tf = pytest.importorskip("tensorflow") - dev = qml.device("default.qubit", wires=1) - - def func(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - # Test if interface change works with different diff_methods - qn = qml.qnode_old.QNode(func, dev, interface="autograd", diff_method=method) - spy = mocker.spy(qn, "_get_best_diff_method") - qn.to_tf() - if method == "best": - spy.assert_called_once() - - @pytest.mark.parametrize( - "method", - [ - "best", - "parameter-shift", - "finite-diff", - "reversible", - "adjoint", - "backprop", - ], - ) - def test_to_autograd(self, method, mocker): - """Test if interface change is working""" - dev = qml.device("default.qubit", wires=1) - tf = pytest.importorskip("tensorflow") - - def func(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - # Test if interface change works with different diff_methods - qn = qml.qnode_old.QNode(func, dev, interface="tf", diff_method=method) - spy = mocker.spy(qn, "_get_best_diff_method") - qn.to_autograd() - if method == "best": - spy.assert_called_once() - - @pytest.mark.parametrize( - "method", - [ - "best", - "parameter-shift", - "finite-diff", - "reversible", - "adjoint", - "backprop", - ], - ) - def test_to_torch(self, method, mocker): - """Test if interface change is working""" - dev = qml.device("default.qubit", wires=1) - torch = pytest.importorskip("torch") - - def func(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - # Test if interface change works with different diff_methods - qn = qml.qnode_old.QNode(func, dev, interface="autograd", diff_method=method) - spy = mocker.spy(qn, "_get_best_diff_method") - qn.to_torch() - if method == "best": - spy.assert_called_once() - - @pytest.mark.parametrize( - "method", - [ - "best", - "parameter-shift", - "finite-diff", - "reversible", - "adjoint", - "backprop", - ], - ) - def test_to_jax(self, method, mocker): - """Test if interface change is working""" - dev = qml.device("default.qubit", wires=1) - jax = pytest.importorskip("jax") - - def func(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - # Test if interface change works with different diff_methods - qn = qml.qnode_old.QNode(func, dev, interface="autograd", diff_method=method) - spy = mocker.spy(qn, "_get_best_diff_method") - qn.to_jax() - if method == "best": - spy.assert_called_once() - - @pytest.mark.parametrize( - "par", [None, 1, 1.1, np.array(1.2), pnp.array(1.3, requires_grad=True)] - ) - def test_diff_method_none(self, par): - """Test if diff_method=None works as intended.""" - dev = qml.device("default.qubit", wires=1) - - def func(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - qn = qml.qnode_old.QNode(func, dev, diff_method=None) - assert qn.interface is None - - grad = qml.grad(qn) - - # Raise error in cases 1 and 5, as non-trainable parameters do not trigger differentiation. - # Raise warning in cases 1-4 as there a no trainable parameters. - # Case 1: No input - # Case 2: int input - # Case 3: float input - # Case 4: numpy input - # Case 5: differentiable tensor input - if par is None: - with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"): - with pytest.raises(TypeError) as exp: - grad() - elif hasattr(par, "requires_grad"): - with pytest.raises(TypeError) as exp: - grad(par) - else: - with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"): - grad(par) - - def test_diff_method_none_no_qnode_param(self): - """Test if diff_method=None works as intended.""" - dev = qml.device("default.qubit", wires=1) - - def func(): - qml.PauliX(wires=0) - return qml.expval(qml.PauliZ(0)) - - qn = qml.qnode_old.QNode(func, dev, diff_method=None) - assert qn.interface is None - - grad = qml.grad(qn) - - # No differentiation required. No error raised. - with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"): - grad() - - def test_unrecognized_keyword_arguments_validation(self): - """Tests that a UserWarning is raised when unrecognized keyword arguments are provided.""" - - # use two unrecognized methods, to confirm that multiple warnings are raised - unrecognized_one = "test_method_one" - unrecognized_two = "test_method_two" - warning_text = ( - " is unrecognized, and will not be included in your computation. " - "Please review the QNode class or qnode decorator for the list of available " - "keyword variables." - ) - - expected_warnings = { - ( - UserWarning, - f"qml.qnode_old.QNode is deprecated, and will be removed in an " - "upcoming release. Please use qml.QNode instead.", - ), - (UserWarning, f"'{unrecognized_one}'{warning_text}"), - (UserWarning, f"'{unrecognized_two}'{warning_text}"), - } - - dev = qml.device("default.qubit", wires=1, shots=1) - - with pytest.warns(UserWarning) as warning_list: - - QNode(dummyfunc, dev, test_method_one=1, test_method_two=2) - - warnings = {(warning.category, warning.message.args[0]) for warning in warning_list} - assert warnings == expected_warnings - - def test_unrecognized_keyword_arguments_validation_decorator(self): - """Tests that a UserWarning is raised when unrecognized keyword arguments are provided.""" - - # use two unrecognized methods, to confirm that multiple warnings are raised - unrecognized_one = "test_method_one" - unrecognized_two = "test_method_two" - warning_text = ( - " is unrecognized, and will not be included in your computation. " - "Please review the QNode class or qnode decorator for the list of available " - "keyword variables." - ) - - expected_warnings = { - ( - UserWarning, - f"qml.qnode_old.QNode is deprecated, and will be removed in an " - "upcoming release. Please use qml.QNode instead.", - ), - (UserWarning, f"'{unrecognized_one}'{warning_text}"), - (UserWarning, f"'{unrecognized_two}'{warning_text}"), - } - - dev = qml.device("default.qubit", wires=1, shots=1) - - with pytest.warns(UserWarning) as warning_list: - - @qml.qnode_old.qnode(dev, test_method_one=1, test_method_two=2) - def circ(): - return qml.expval(qml.PauliZ(0)) - - warnings = {(warning.category, warning.message.args[0]) for warning in warning_list} - assert warnings == expected_warnings - - -class TestTapeConstruction: - """Tests for the tape construction""" - - def test_basic_tape_construction(self, tol): - """Test that a quantum tape is properly constructed""" - dev = qml.device("default.qubit", wires=2) - - def func(x, y): - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - qn = QNode(func, dev) - - x = 0.12 - y = 0.54 - - res = qn(x, y) - - assert isinstance(qn.qtape, JacobianTape) - assert len(qn.qtape.operations) == 3 - assert len(qn.qtape.observables) == 1 - assert qn.qtape.num_params == 2 - - expected = qn.qtape.execute(dev) - assert np.allclose(res, expected, atol=tol, rtol=0) - - # when called, a new quantum tape is constructed - old_tape = qn.qtape - res2 = qn(x, y) - - assert np.allclose(res, res2, atol=tol, rtol=0) - assert qn.qtape is not old_tape - - def test_jacobian(self, tol): - """Test the jacobian computation""" - dev = qml.device("default.qubit", wires=2) - - def func(x, y): - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.probs(wires=0), qml.probs(wires=1) - - qn = QNode(func, dev, h=1e-8, order=2) - assert qn.diff_options["h"] == 1e-8 - assert qn.diff_options["order"] == 2 - - x = 0.12 - y = 0.54 - - res = qn(x, y) - jac = qn.qtape.jacobian(dev, params=[0.45, 0.1]) - - assert jac.shape == (4, 2) - - def test_diff_method_expansion(self, monkeypatch, mocker): - """Test that a QNode with tape expansion during construction - preserves the differentiation method.""" - - class MyDev(qml.devices.DefaultQubit): - """Dummy device that supports device Jacobians""" - - @classmethod - def capabilities(cls): - capabilities = super().capabilities().copy() - capabilities.update( - provides_jacobian=True, - ) - return capabilities - - def jacobian(self, *args, **kwargs): - return np.zeros((2, 4)) - - dev = MyDev(wires=2) - - def func(x, y): - # the U2 operation is not supported on default.qubit - # and is decomposed. - qml.U2(x, y, wires=0) - qml.CNOT(wires=[0, 1]) - return qml.probs(wires=0) - - qn = QNode(func, dev, diff_method="device", h=1e-8, order=2) - - assert qn.diff_options["method"] == "device" - assert qn.diff_options["h"] == 1e-8 - assert qn.diff_options["order"] == 2 - - x = pnp.array(0.12, requires_grad=True) - y = pnp.array(0.54, requires_grad=True) - - spy = mocker.spy(JacobianTape, "expand") - res = qn(x, y) - - spy.assert_called_once() - assert qn.qtape.jacobian_options["method"] == "device" - assert qn.qtape.jacobian_options["h"] == 1e-8 - assert qn.qtape.jacobian_options["order"] == 2 - - spy = mocker.spy(JacobianTape, "jacobian") - jac = qml.jacobian(qn)(x, y) - - assert spy.call_args_list[0][1]["method"] == "device" - - def test_returning_non_measurements(self): - """Test that an exception is raised if a non-measurement - is returned from the QNode.""" - dev = qml.device("default.qubit", wires=2) - - def func(x, y): - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.CNOT(wires=[0, 1]) - return 5 - - qn = QNode(func, dev) - - with pytest.raises( - qml.QuantumFunctionError, match="must return either a single measurement" - ): - qn(5, 1) - - def func(x, y): - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), 5 - - qn = QNode(func, dev) - - with pytest.raises( - qml.QuantumFunctionError, match="must return either a single measurement" - ): - qn(5, 1) - - def test_inconsistent_measurement_order(self): - """Test that an exception is raised if measurements are returned in an - order different to how they were queued on the tape""" - dev = qml.device("default.qubit", wires=2) - - def func(x, y): - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.CNOT(wires=[0, 1]) - m = qml.expval(qml.PauliZ(0)) - return qml.expval(qml.PauliX(1)), m - - qn = QNode(func, dev) - - with pytest.raises( - qml.QuantumFunctionError, - match="measurements must be returned in the order they are measured", - ): - qn(5, 1) - - def test_consistent_measurement_order(self): - """Test evaluation exceeds as expected if measurements are returned in the - same order to how they were queued on the tape""" - dev = qml.device("default.qubit", wires=2) - - def func(x, y): - global op1, op2, op3, m1, m2 - op1 = qml.RX(x, wires=0) - op2 = qml.RY(y, wires=1) - op3 = qml.CNOT(wires=[0, 1]) - m1 = qml.expval(qml.PauliZ(0)) - m2 = qml.expval(qml.PauliX(1)) - return [m1, m2] - - qn = QNode(func, dev) - qn(5, 1) # evaluate the QNode - assert qn.qtape.operations == [op1, op2, op3] - assert qn.qtape.measurements == [m1, m2] - - def test_draw_transform(self): - """Test circuit drawing""" - - x = pnp.array(0.1, requires_grad=True) - y = pnp.array([0.2, 0.3], requires_grad=True) - z = pnp.array(0.4, requires_grad=True) - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, interface="autograd") - def circuit(p1, p2=y, **kwargs): - qml.RX(p1, wires=0) - qml.RY(p2[0] * p2[1], wires=1) - qml.RX(kwargs["p3"], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - result = draw(circuit)(p1=x, p3=z) - expected = "0: ──RX(0.10)──RX(0.40)─╭C─┤ ╭\n" "1: ──RY(0.06)───────────╰X─┤ ╰" - - assert result == expected - - def test_drawing(self): - """Test circuit drawing""" - - x = pnp.array(0.1, requires_grad=True) - y = pnp.array([0.2, 0.3], requires_grad=True) - z = pnp.array(0.4, requires_grad=True) - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, interface="autograd") - def circuit(p1, p2=y, **kwargs): - qml.RX(p1, wires=0) - qml.RY(p2[0] * p2[1], wires=1) - qml.RX(kwargs["p3"], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - circuit(p1=x, p3=z) - - with pytest.warns(UserWarning, match="The QNode.draw method has been deprecated."): - result = circuit.draw() - expected = """\ - 0: ──RX(0.1)───RX(0.4)──╭C──╭┤ ⟨Z ⊗ X⟩ - 1: ──RY(0.06)───────────╰X──╰┤ ⟨Z ⊗ X⟩ -""" - - assert result == expected - - def test_drawing_ascii(self): - """Test circuit drawing when using ASCII characters""" - from pennylane import numpy as anp - - x = anp.array(0.1, requires_grad=True) - y = anp.array([0.2, 0.3], requires_grad=True) - z = anp.array(0.4, requires_grad=True) - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, interface="autograd") - def circuit(p1, p2=y, **kwargs): - qml.RX(p1, wires=0) - qml.RY(p2[0] * p2[1], wires=1) - qml.RX(kwargs["p3"], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - circuit(p1=x, p3=z) - - with pytest.warns(UserWarning, match="The QNode.draw method has been deprecated."): - result = circuit.draw(charset="ascii") - expected = """\ - 0: --RX(0.1)---RX(0.4)--+C--+| - 1: --RY(0.06)-----------+X--+| -""" - - assert result == expected - - def test_drawing_exception(self): - """Test that an error is raised if a QNode is drawn prior to - construction.""" - from pennylane import numpy as anp - - x = anp.array(0.1, requires_grad=True) - y = anp.array([0.2, 0.3], requires_grad=True) - z = anp.array(0.4, requires_grad=True) - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, interface="autograd") - def circuit(p1, p2=y, **kwargs): - qml.RX(p1, wires=0) - qml.RY(p2[0] * p2[1], wires=1) - qml.RX(kwargs["p3"], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - with pytest.raises(qml.QuantumFunctionError, match="can only be drawn after"): - circuit.draw() - - def test_multiple_observables_same_wire_expval(self, mocker): - """Test that the QNode supports returning expectation values of observables that are on the - same wire (provided that they are Pauli words and qubit-wise commuting)""" - dev = qml.device("default.qubit", wires=3) - - w = np.random.random((2, 3, 3)) - - @qnode(dev) - def f(w): - qml.templates.StronglyEntanglingLayers(w, wires=range(3)) - return ( - qml.expval(qml.PauliX(0)), - qml.expval(qml.PauliX(0) @ qml.PauliZ(1)), - qml.expval(qml.PauliX(2)), - ) - - spy = mocker.spy(qml.devices.DefaultQubit, "apply") - res = f(w) - spy.assert_called_once() - - obs = [qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(1), qml.PauliX(2)] - qnodes = qml.map(qml.templates.StronglyEntanglingLayers, obs, dev) - res_2 = qnodes(w) - - assert np.allclose(res, res_2) - - def test_multiple_observables_same_wire_mixed(self, mocker): - """Test that the QNode supports returning observables that are on the - same wire but with different return types (provided that the observables are Pauli words and - qubit-wise commuting)""" - dev = qml.device("default.qubit", wires=3) - - w = np.random.random((2, 3, 3)) - - @qnode(dev) - def f(w): - qml.templates.StronglyEntanglingLayers(w, wires=range(3)) - return qml.expval(qml.PauliX(0)), qml.var(qml.PauliX(0) @ qml.PauliZ(1)) - - spy = mocker.spy(qml.devices.DefaultQubit, "apply") - res = f(w) - spy.assert_called_once() - - q1 = qml.map(qml.templates.StronglyEntanglingLayers, [qml.PauliX(0)], dev, measure="expval") - q2 = qml.map( - qml.templates.StronglyEntanglingLayers, - [qml.PauliX(0) @ qml.PauliZ(1)], - dev, - measure="var", - ) - - res_2 = np.array([q1(w), q2(w)]).squeeze() - - assert np.allclose(res, res_2) - - -class TestDecorator: - """Unittests for the decorator""" - - def test_decorator(self, tol): - """Test that the decorator correctly creates a QNode.""" - dev = qml.device("default.qubit", wires=2) - - @qnode(dev) - def func(x, y): - """My function docstring""" - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - assert isinstance(func, QNode) - assert func.__doc__ == "My function docstring" - - x = 0.12 - y = 0.54 - - res = func(x, y) - - assert isinstance(func.qtape, JacobianTape) - assert len(func.qtape.operations) == 3 - assert len(func.qtape.observables) == 1 - assert func.qtape.num_params == 2 - - expected = func.qtape.execute(dev) - assert np.allclose(res, expected, atol=tol, rtol=0) - - # when called, a new quantum tape is constructed - old_tape = func.qtape - res2 = func(x, y) - - assert np.allclose(res, res2, atol=tol, rtol=0) - assert func.qtape is not old_tape - - -@pytest.mark.usefixtures("skip_if_no_dask_support") -class TestQNodeCollection: - """Unittests for the QNodeCollection""" - - def test_multi_thread(self): - """Test that multi-threaded queuing works correctly""" - n_qubits = 4 - n_batches = 5 - dev = qml.device("default.qubit", wires=n_qubits) - - def circuit(inputs, weights): - for index, input in enumerate(inputs): - qml.RY(input, wires=index) - for index in range(n_qubits - 1): - qml.CNOT(wires=(index, index + 1)) - for index, weight in enumerate(weights): - qml.RX(weight, wires=index) - return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_qubits)] - - weight_shapes = {"weights": (n_qubits)} - - try: - qnode = QNodeCollection([QNode(circuit, dev) for _ in range(n_batches)]) - except Exception as e: - pytest.fail("QNodeCollection cannot be instantiated") - x = np.random.rand(n_qubits).astype(np.float64) - p = np.random.rand(weight_shapes["weights"]).astype(np.float64) - try: - for _ in range(10): - qnode(x, p, parallel=True) - except: - pytest.fail("Multi-threading on QuantumTape failed") - - -class TestIntegration: - """Integration tests.""" - - def test_correct_number_of_executions_autograd(self): - """Test that number of executions are tracked in the autograd interface.""" - - def func(): - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=2) - qn = QNode(func, dev, interface="autograd") - - for i in range(2): - qn() - - assert dev.num_executions == 2 - - qn2 = QNode(func, dev, interface="autograd") - for i in range(3): - qn2() - - assert dev.num_executions == 5 - - def test_correct_number_of_executions_tf(self): - """Test that number of executions are tracked in the tf interface.""" - tf = pytest.importorskip("tf") - - def func(): - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=2) - qn = QNode(func, dev, interface="tf") - for i in range(2): - qn() - - assert dev.num_executions == 2 - - qn2 = QNode(func, dev, interface="tf") - for i in range(3): - qn2() - - assert dev.num_executions == 5 - - # qubit of different interface - qn3 = QNode(func, dev, interface="autograd") - qn3() - - assert dev.num_executions == 6 - - def test_correct_number_of_executions_torch(self): - """Test that number of executions are tracked in the torch interface.""" - torch = pytest.importorskip("torch") - - def func(): - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=2) - qn = QNode(func, dev, interface="torch") - for i in range(2): - qn() - - assert dev.num_executions == 2 - - qn2 = QNode(func, dev, interface="torch") - for i in range(3): - qn2() - - assert dev.num_executions == 5 - - # qubit of different interface - qn3 = QNode(func, dev, interface="autograd") - qn3() - - assert dev.num_executions == 6 - - @pytest.mark.parametrize("diff_method", ["parameter-shift", "finite-diff", "reversible"]) - def test_single_expectation_value_with_argnum_one(self, diff_method, tol): - """Tests correct output shape and evaluation for a QNode - with a single expval output where only one parameter is chosen to - estimate the jacobian. - - This test relies on the fact that exactly one term of the estimated - jacobian will match the expected analytical value. - """ - from pennylane import numpy as anp - - dev = qml.device("default.qubit", wires=2) - - x = anp.array(0.543, requires_grad=True) - y = anp.array(-0.654, requires_grad=True) - - @qml.qnode_old.qnode( - dev, diff_method=diff_method, argnum=[1] - ) # <--- we only choose one trainable parameter - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - res = qml.grad(circuit)(x, y) - assert len(res) == 2 - - expected = (0, np.cos(y) * np.cos(x)) - res = res - expected = expected - - assert np.allclose(res, expected, atol=tol, rtol=0) - - -class TestMutability: - """Test for QNode immutability""" - - def test_mutable(self, mocker, tol): - """Test that a QNode which has structure dependent - on trainable arguments is reconstructed with - every call, and remains differentiable""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode_old.qnode(dev, mutable=True) - def circuit(x): - if x < 0: - qml.RY(x, wires=0) - else: - qml.RZ(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - x = 0.5 - spy = mocker.spy(circuit, "construct") - res = circuit(x) - spy.assert_called_once_with((x,), {}) - assert len(spy.call_args_list) == 1 - assert circuit.qtape.operations[0].name == "RZ" - assert circuit.qtape.operations[0].data == [x] - np.testing.assert_allclose(res, 1, atol=tol, rtol=0) - - # calling the qnode with new arguments reconstructs the tape - x = -0.5 - res = circuit(x) - spy.assert_called_with((x,), {}) - assert len(spy.call_args_list) == 2 - assert circuit.qtape.operations[0].name == "RY" - assert circuit.qtape.operations[0].data == [x] - np.testing.assert_allclose(res, np.cos(x), atol=tol, rtol=0) - - # test differentiability - grad = qml.grad(circuit, argnum=0)(0.5) - np.testing.assert_allclose(grad, 0, atol=tol, rtol=0) - - grad = qml.grad(circuit, argnum=0)(-0.5) - np.testing.assert_allclose(grad, -np.sin(-0.5), atol=tol, rtol=0) - - def test_immutable(self, mocker, tol): - """Test that a QNode which has structure dependent - on trainable arguments is *not* reconstructed with - every call when mutable=False""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode_old.qnode(dev, mutable=False) - def circuit(x): - if x < 0: - qml.RY(x, wires=0) - else: - qml.RZ(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - x = 0.5 - spy = mocker.spy(circuit, "construct") - res = circuit(x) - spy.assert_called_once_with((x,), {}) - assert len(spy.call_args_list) == 1 - assert circuit.qtape.operations[0].name == "RZ" - assert circuit.qtape.operations[0].data == [x] - np.testing.assert_allclose(res, 1, atol=tol, rtol=0) - - # calling the qnode with new arguments does not reconstruct the tape - x = -0.5 - res = circuit(x) - spy.assert_called_once_with((0.5,), {}) - assert len(spy.call_args_list) == 1 - assert circuit.qtape.operations[0].name == "RZ" - assert circuit.qtape.operations[0].data == [0.5] - np.testing.assert_allclose(res, 1, atol=tol, rtol=0) - - # test differentiability. The circuit will assume an RZ gate - with pytest.warns(UserWarning, match="Output seems independent of input"): - grad = qml.grad(circuit, argnum=0)(-0.5) - np.testing.assert_allclose(grad, 0, atol=tol, rtol=0) - - -class TestShots: - """Unittests for specifying shots per call.""" - - def test_specify_shots_per_call_sample(self): - """Tests that shots can be set per call for a sample return type.""" - dev = qml.device("default.qubit", wires=1, shots=10) - - @qml.qnode_old.qnode(dev) - def circuit(a): - qml.RX(a, wires=0) - return qml.sample(qml.PauliZ(wires=0)) - - assert len(circuit(0.8)) == 10 - assert len(circuit(0.8, shots=2)) == 2 - assert len(circuit(0.8, shots=3178)) == 3178 - assert len(circuit(0.8)) == 10 - - def test_specify_shots_per_call_expval(self): - """Tests that shots can be set per call for an expectation value. - Note: this test has a vanishingly small probability to fail.""" - dev = qml.device("default.qubit", wires=1, shots=None) - - @qml.qnode_old.qnode(dev) - def circuit(): - qml.Hadamard(wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - # check that the circuit is analytic - res1 = [circuit() for _ in range(100)] - assert np.std(res1) == 0.0 - assert circuit.device._shots is None - - # check that the circuit is temporary non-analytic - res1 = [circuit(shots=1) for _ in range(100)] - assert np.std(res1) != 0.0 - - # check that the circuit is analytic again - res1 = [circuit() for _ in range(100)] - assert np.std(res1) == 0.0 - assert circuit.device._shots is None - - def test_no_shots_per_call_if_user_has_shots_qfunc_kwarg(self): - """Tests that the per-call shots overwriting is suspended if user - has a shots keyword argument, but a warning is raised.""" - - dev = qml.device("default.qubit", wires=2, shots=10) - - def circuit(a, shots=0): - qml.RX(a, wires=shots) - return qml.sample(qml.PauliZ(wires=0)) - - with pytest.warns( - UserWarning, match="The 'shots' argument name is reserved for overriding" - ): - circuit = qml.qnode_old.QNode(circuit, dev) - - assert len(circuit(0.8)) == 10 - assert circuit.qtape.operations[0].wires.labels == (0,) - - assert len(circuit(0.8, shots=1)) == 10 - assert circuit.qtape.operations[0].wires.labels == (1,) - - assert len(circuit(0.8, shots=0)) == 10 - assert circuit.qtape.operations[0].wires.labels == (0,) - - def test_no_shots_per_call_if_user_has_shots_qfunc_arg(self): - """Tests that the per-call shots overwriting is suspended - if user has a shots argument, but a warning is raised.""" - - # Todo: use standard creation of qnode below for both asserts once we do not parse args to tensors any more - dev = qml.device("default.qubit", wires=[qml.numpy.array(0), qml.numpy.array(1)], shots=10) - - def circuit(a, shots): - qml.RX(a, wires=shots) - return qml.sample(qml.PauliZ(wires=qml.numpy.array(0))) - - # assert that warning is still raised - with pytest.warns( - UserWarning, match="The 'shots' argument name is reserved for overriding" - ): - circuit = qml.qnode_old.QNode(circuit, dev) - - assert len(circuit(0.8, 1)) == 10 - assert circuit.qtape.operations[0].wires.labels == (1,) - - dev = qml.device("default.qubit", wires=2, shots=10) - - @qml.qnode_old.qnode(dev) - def circuit(a, shots): - qml.RX(a, wires=shots) - return qml.sample(qml.PauliZ(wires=0)) - - assert len(circuit(0.8, shots=0)) == 10 - assert circuit.qtape.operations[0].wires.labels == (0,) - - @pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift"]) - def test_shots_setting_does_not_mutate_device(self, diff_method): - """Tests that per-call shots setting does not change the number of shots in the device.""" - - dev = qml.device("default.qubit", wires=1, shots=3) - - @qml.qnode_old.qnode(dev) - def circuit(a): - qml.RX(a, wires=0) - return qml.sample(qml.PauliZ(wires=0)) - - assert dev.shots == 3 - res = circuit(0.8, shots=2) - assert len(res) == 2 - assert dev.shots == 3 - - -class TestSpecs: - """Tests for the qnode property specs""" - - def test_specs_error(self): - """Tests an error is raised if the tape is not constructed.""" - - dev = qml.device("default.qubit", wires=4) - - @qml.qnode_old.qnode(dev) - def circuit(): - return qml.expval(qml.PauliZ(0)) - - with pytest.raises(qml.QuantumFunctionError, match=r"The QNode specifications"): - circuit.specs - - @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 10), ("parameter-shift", 12), ("adjoint", 11)] - ) - def test_specs(self, diff_method, len_info): - """Tests the specs property with backprop""" - - dev = qml.device("default.qubit", wires=4) - - @qml.qnode_old.qnode(dev, diff_method=diff_method) - def circuit(x, y): - qml.RX(x[0], wires=0) - qml.Toffoli(wires=(0, 1, 2)) - qml.CRY(x[1], wires=(0, 1)) - qml.Rot(x[2], x[3], y, wires=2) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1)) - - x = pnp.array([0.05, 0.1, 0.2, 0.3], requires_grad=True) - y = pnp.array(0.1, requires_grad=False) - - res = circuit(x, y) - - info = circuit.specs - - assert len(info) == len_info - - assert info["gate_sizes"] == defaultdict(int, {1: 2, 3: 1, 2: 1}) - assert info["gate_types"] == defaultdict(int, {"RX": 1, "Toffoli": 1, "CRY": 1, "Rot": 1}) - assert info["num_operations"] == 4 - assert info["num_observables"] == 2 - assert info["num_diagonalizing_gates"] == 1 - assert info["num_used_wires"] == 3 - assert info["depth"] == 3 - assert info["num_device_wires"] == 4 - - assert info["diff_method"] == diff_method - - if diff_method == "parameter-shift": - assert info["num_parameter_shift_executions"] == 7 - - if diff_method != "backprop": - assert info["device_name"] == "default.qubit" - assert info["num_trainable_params"] == 4 - else: - assert info["device_name"] == "default.qubit.autograd" - - -def test_finitediff_float32(tol): - """Tests that float32 parameters do not effect order 1 finite-diff results. - - Checks bugfix. Problem occured with StronglyEntanglingLayers, but not simpler circuits. - """ - - n_wires = 2 - n_layers = 2 - - shape = qml.templates.StronglyEntanglingLayers.shape(n_wires=n_wires, n_layers=n_layers) - - rng = pnp.random.default_rng(seed=42) - params = rng.random(shape, requires_grad=True) - params_f32 = pnp.array(params, dtype=np.float32, requires_grad=True) - - dev = qml.device("default.qubit", n_wires) - - @qml.qnode_old.qnode(dev, diff_method="finite-diff", order=1) - def circuit(params): - qml.templates.StronglyEntanglingLayers(params, wires=range(n_wires)) - return qml.expval(qml.PauliZ(0)) - - grad64 = qml.grad(circuit)(params) - grad32 = qml.grad(circuit)(params_f32) - - assert np.allclose(grad64, grad32, atol=tol, rtol=0) - - -class TestDrawMethod: - """Tests for the deprecated qnode.draw() method""" - - def test_method_deprecation(self): - """Test that the qnode.draw() method raises a deprecation warning""" - - x = np.array(0.1) - y = np.array([0.2, 0.3]) - z = np.array(0.4) - - dev = qml.device("default.qubit", wires=2) - - @qml.qnode_old.qnode(dev, interface="autograd") - def circuit(p1, p2=y, **kwargs): - qml.RX(p1, wires=0) - qml.RY(p2[0] * p2[1], wires=1) - qml.RX(kwargs["p3"], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - circuit(p1=x, p3=z) - - with pytest.warns(UserWarning, match=r"The QNode\.draw method has been deprecated"): - result = circuit.draw() - - expected = """\ - 0: ──RX(0.1)───RX(0.4)──╭C──╭┤ ⟨Z ⊗ X⟩ - 1: ──RY(0.06)───────────╰X──╰┤ ⟨Z ⊗ X⟩ -""" - - assert result == expected - - def test_invalid_wires(self): - """Test that an exception is raised if a wire in the wire - ordering does not exist on the device""" - dev = qml.device("default.qubit", wires=["a", -1, "q2"]) - - @qml.qnode_old.qnode(dev) - def circuit(): - qml.Hadamard(wires=-1) - qml.CNOT(wires=["a", "q2"]) - qml.RX(0.2, wires="a") - return qml.expval(qml.PauliX(wires="q2")) - - circuit() - - with pytest.raises(ValueError, match="contains wires not contained on the device"): - res = circuit.draw(wire_order=["q2", 5]) - - def test_tape_not_constructed(self): - """Test that an exception is raised if the tape has not been constructed""" - dev = qml.device("default.qubit", wires=1) - - @qml.qnode_old.qnode(dev) - def circuit(): - return qml.expval(qml.PauliX(wires=0)) - - with pytest.raises( - qml.QuantumFunctionError, match="after its quantum tape has been constructed" - ): - res = circuit.draw() - - def test_show_all_wires_error(self): - """Test that show_all_wires will raise an error if the provided wire - order does not contain all wires on the device""" - - dev = qml.device("default.qubit", wires=[-1, "a", "q2", 0]) - - @qml.qnode_old.qnode(dev) - def circuit(): - qml.Hadamard(wires=-1) - qml.CNOT(wires=[-1, "q2"]) - return qml.expval(qml.PauliX(wires="q2")) - - circuit() - - with pytest.raises(ValueError, match="must contain all wires"): - circuit.draw(show_all_wires=True, wire_order=[-1, "a"]) diff --git a/tests/tape/test_qubit_param_shift.py b/tests/tape/test_qubit_param_shift.py deleted file mode 100644 index 1a5a27b5cb7..00000000000 --- a/tests/tape/test_qubit_param_shift.py +++ /dev/null @@ -1,776 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the qubit parameter-shift QubitParamShiftTape""" -import pytest -import numpy as np - -import pennylane as qml -from pennylane import numpy as pnp -from pennylane.tape import QubitParamShiftTape -from pennylane.tape.qubit_param_shift import _get_operation_recipe -from pennylane.operation import ( - Operation, - OperatorPropertyUndefined, - ParameterFrequenciesUndefinedError, -) - - -class TestGetOperationRecipe: - """Tests special cases for the _get_operation_recipe - copy in qubit_param_shift.py, the original is in - gradients/parameter_shift.py - """ - - class DummyOp0(Operation): - num_params = 1 - num_wires = 1 - grad_recipe = (None,) - - class DummyOp1(Operation): - num_params = 1 - num_wires = 1 - grad_recipe = (None,) - - @property - def parameter_frequencies(self): - raise ParameterFrequenciesUndefinedError - - @pytest.mark.parametrize("Op", [DummyOp0, DummyOp1]) - def test_error_no_grad_info(self, Op): - op = Op(0.1, wires=0) - with pytest.raises( - OperatorPropertyUndefined, - match=f"The operation {op.name} does not have a grad_recipe,", - ): - _get_operation_recipe(op, 0, None) - - -class TestGradMethod: - """Tests for parameter gradient methods""" - - def test_non_differentiable(self): - """Test that a non-differentiable parameter is - correctly marked""" - psi = np.array([1, 0, 1, 0]) / np.sqrt(2) - - with QubitParamShiftTape() as tape: - qml.QubitStateVector(psi, wires=[0, 1]) - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=[0, 1]) - - assert tape._grad_method(0) is None - assert tape._grad_method(1) == "A" - assert tape._grad_method(2) == "A" - - tape._update_gradient_info() - - assert tape._par_info[0]["grad_method"] is None - assert tape._par_info[1]["grad_method"] == "A" - assert tape._par_info[2]["grad_method"] == "A" - - def test_independent(self): - """Test that an independent variable is properly marked - as having a zero gradient""" - - with QubitParamShiftTape() as tape: - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[1]) - qml.expval(qml.PauliY(0)) - - assert tape._grad_method(0) == "A" - assert tape._grad_method(1) == "0" - - tape._update_gradient_info() - - assert tape._par_info[0]["grad_method"] == "A" - assert tape._par_info[1]["grad_method"] == "0" - - # in non-graph mode, it is impossible to determine - # if a parameter is independent or not - tape._graph = None - assert tape._grad_method(1, use_graph=False) == "A" - - def test_finite_diff(self, monkeypatch): - """If an op has grad_method=F, this should be respected - by the QubitParamShiftTape""" - monkeypatch.setattr(qml.RX, "grad_method", "F") - - psi = np.array([1, 0, 1, 0]) / np.sqrt(2) - - with QubitParamShiftTape() as tape: - qml.QubitStateVector(psi, wires=[0, 1]) - qml.RX(0.543, wires=[0]) - qml.RY(-0.654, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=[0, 1]) - - assert tape._grad_method(0) is None - assert tape._grad_method(1) == "F" - assert tape._grad_method(2) == "A" - - -def test_specs_num_parameter_shift_executions(): - """Tests specs has the correct number of parameter-shift executions""" - - dev = qml.device("default.qubit", wires=3) - x = 0.543 - y = -0.654 - - with qml.tape.QubitParamShiftTape() as tape: - qml.CRX(x, wires=[0, 1]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.RY(0.12345, wires=2) - qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - num_exec = tape.specs["num_parameter_shift_executions"] - assert num_exec == 7 - - jac = tape.jacobian(dev) - - assert num_exec == (dev.num_executions + 1) - - -class TestParameterShiftRule: - """Tests for the parameter shift implementation""" - - @pytest.mark.parametrize("theta", np.linspace(-2 * np.pi, 2 * np.pi, 7)) - @pytest.mark.parametrize("shift", [np.pi / 2, 0.3, np.sqrt(2)]) - @pytest.mark.parametrize("G", [qml.RX, qml.RY, qml.RZ, qml.PhaseShift]) - def test_pauli_rotation_gradient(self, mocker, G, theta, shift, tol): - """Tests that the automatic gradients of Pauli rotations are correct.""" - spy = mocker.spy(QubitParamShiftTape, "parameter_shift") - dev = qml.device("default.qubit", wires=1) - - with QubitParamShiftTape() as tape: - qml.QubitStateVector(np.array([1.0, -1.0]) / np.sqrt(2), wires=0) - G(theta, wires=[0]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {1} - - autograd_val = tape.jacobian(dev, shift=shift, method="analytic") - manualgrad_val = ( - tape.execute(dev, params=[theta + np.pi / 2]) - - tape.execute(dev, params=[theta - np.pi / 2]) - ) / 2 - assert np.allclose(autograd_val, manualgrad_val, atol=tol, rtol=0) - - assert spy.call_args[1]["shift"] == shift - - # compare to finite differences - numeric_val = tape.jacobian(dev, shift=shift, method="numeric") - assert np.allclose(autograd_val, numeric_val, atol=tol, rtol=0) - - @pytest.mark.parametrize("theta", np.linspace(-2 * np.pi, 2 * np.pi, 7)) - @pytest.mark.parametrize("shift", [np.pi / 2, 0.3, np.sqrt(2)]) - def test_Rot_gradient(self, mocker, theta, shift, tol): - """Tests that the automatic gradient of a arbitrary Euler-angle-parameterized gate is correct.""" - spy = mocker.spy(QubitParamShiftTape, "parameter_shift") - dev = qml.device("default.qubit", wires=1) - params = np.array([theta, theta**3, np.sqrt(2) * theta]) - - with QubitParamShiftTape() as tape: - qml.QubitStateVector(np.array([1.0, -1.0]) / np.sqrt(2), wires=0) - qml.Rot(*params, wires=[0]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {1, 2, 3} - - autograd_val = tape.jacobian(dev, shift=shift, method="analytic") - manualgrad_val = np.zeros_like(autograd_val) - - for idx in list(np.ndindex(*params.shape)): - s = np.zeros_like(params) - s[idx] += np.pi / 2 - - forward = tape.execute(dev, params=params + s) - backward = tape.execute(dev, params=params - s) - - manualgrad_val[0, idx] = (forward - backward) / 2 - - assert np.allclose(autograd_val, manualgrad_val, atol=tol, rtol=0) - assert spy.call_args[1]["shift"] == shift - - # compare to finite differences - numeric_val = tape.jacobian(dev, shift=shift, method="numeric") - assert np.allclose(autograd_val, numeric_val, atol=tol, rtol=0) - - @pytest.mark.parametrize("G", [qml.CRX, qml.CRY, qml.CRZ]) - def test_controlled_rotation_gradient(self, G, tol): - """Test gradient of controlled rotation gates""" - dev = qml.device("default.qubit", wires=2) - b = 0.123 - - with QubitParamShiftTape() as tape: - qml.QubitStateVector(np.array([1.0, -1.0]) / np.sqrt(2), wires=0) - G(b, wires=[0, 1]) - qml.expval(qml.PauliX(0)) - - tape.trainable_params = {1} - - res = tape.execute(dev) - assert np.allclose(res, -np.cos(b / 2), atol=tol, rtol=0) - - grad = tape.jacobian(dev, method="analytic") - expected = np.sin(b / 2) / 2 - assert np.allclose(grad, expected, atol=tol, rtol=0) - - # compare to finite differences - numeric_val = tape.jacobian(dev, method="numeric") - assert np.allclose(grad, numeric_val, atol=tol, rtol=0) - - @pytest.mark.parametrize("theta", np.linspace(-2 * np.pi, np.pi, 7)) - def test_CRot_gradient(self, mocker, theta, tol): - """Tests that the automatic gradient of an arbitrary controlled Euler-angle-parameterized - gate is correct.""" - spy = mocker.spy(QubitParamShiftTape, "parameter_shift") - dev = qml.device("default.qubit", wires=2) - a, b, c = np.array([theta, theta**3, np.sqrt(2) * theta]) - - with QubitParamShiftTape() as tape: - qml.QubitStateVector(np.array([1.0, -1.0]) / np.sqrt(2), wires=0) - qml.CRot(a, b, c, wires=[0, 1]) - qml.expval(qml.PauliX(0)) - - tape.trainable_params = {1, 2, 3} - - res = tape.execute(dev) - expected = -np.cos(b / 2) * np.cos(0.5 * (a + c)) - assert np.allclose(res, expected, atol=tol, rtol=0) - - grad = tape.jacobian(dev, method="analytic") - expected = np.array( - [ - [ - 0.5 * np.cos(b / 2) * np.sin(0.5 * (a + c)), - 0.5 * np.sin(b / 2) * np.cos(0.5 * (a + c)), - 0.5 * np.cos(b / 2) * np.sin(0.5 * (a + c)), - ] - ] - ) - assert np.allclose(grad, expected, atol=tol, rtol=0) - - # compare to finite differences - numeric_val = tape.jacobian(dev, method="numeric") - assert np.allclose(grad, numeric_val, atol=tol, rtol=0) - - def test_gradients_agree_finite_differences(self, mocker, tol): - """Tests that the parameter-shift rule agrees with the first and second - order finite differences""" - params = np.array([0.1, -1.6, np.pi / 5]) - - with QubitParamShiftTape() as tape: - qml.RX(params[0], wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.RY(-1.6, wires=[0]) - qml.RY(params[1], wires=[1]) - qml.CNOT(wires=[1, 0]) - qml.RX(params[2], wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {0, 2, 3} - dev = qml.device("default.qubit", wires=2) - - spy_numeric = mocker.spy(tape, "numeric_pd") - spy_analytic = mocker.spy(tape, "analytic_pd") - - grad_F1 = tape.jacobian(dev, method="numeric", order=1) - grad_F2 = tape.jacobian(dev, method="numeric", order=2) - - spy_numeric.assert_called() - spy_analytic.assert_not_called() - - grad_A = tape.jacobian(dev, method="analytic") - spy_analytic.assert_called() - - # gradients computed with different methods must agree - assert np.allclose(grad_A, grad_F1, atol=tol, rtol=0) - assert np.allclose(grad_A, grad_F2, atol=tol, rtol=0) - - def test_variance_gradients_agree_finite_differences(self, mocker, tol): - """Tests that the variance parameter-shift rule agrees with the first and second - order finite differences""" - params = np.array([0.1, -1.6, np.pi / 5]) - - with QubitParamShiftTape() as tape: - qml.RX(params[0], wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.RY(-1.6, wires=[0]) - qml.RY(params[1], wires=[1]) - qml.CNOT(wires=[1, 0]) - qml.RX(params[2], wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)), qml.var(qml.PauliX(1)) - - tape.trainable_params = {0, 2, 3} - dev = qml.device("default.qubit", wires=2) - - spy_numeric = mocker.spy(tape, "numeric_pd") - spy_analytic = mocker.spy(tape, "analytic_pd") - - grad_F1 = tape.jacobian(dev, method="numeric", order=1) - grad_F2 = tape.jacobian(dev, method="numeric", order=2) - - spy_numeric.assert_called() - spy_analytic.assert_not_called() - - grad_A = tape.jacobian(dev, method="analytic") - spy_analytic.assert_called() - - # gradients computed with different methods must agree - assert np.allclose(grad_A, grad_F1, atol=tol, rtol=0) - assert np.allclose(grad_A, grad_F2, atol=tol, rtol=0) - - def test_processing_function_torch(self, mocker, tol): - """Tests the processing function that is created when using the - parameter_shift method returns a numpy array. - - This is a unit test specifically aimed at checking an edge case - discovered related to default.qubit.torch. - """ - torch = pytest.importorskip("torch") - - results = [ - torch.tensor([0.4342], dtype=torch.float64), - torch.tensor([-0.4342], dtype=torch.float64), - ] - theta = torch.tensor(0.543, dtype=torch.float64) - phi = torch.tensor(-0.234, dtype=torch.float64) - - pars = torch.tensor([theta, phi], dtype=torch.float64) - - with qml.tape.QubitParamShiftTape() as tape: - qml.RY(pars[0], wires=[0]) - qml.RX(pars[1], wires=[0]) - qml.expval(qml.PauliZ(0)) - - tapes, func = tape.parameter_shift(0, pars) - - assert type(func(results)) == np.ndarray - - -class TestJacobianIntegration: - """Tests for general Jacobian integration""" - - def test_single_expectation_value(self, tol): - """Tests correct output shape and evaluation for a tape - with a single expval output""" - dev = qml.device("default.qubit", wires=2) - x = 0.543 - y = -0.654 - - with QubitParamShiftTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - res = tape.jacobian(dev, method="analytic") - assert res.shape == (1, 2) - - expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]]) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_multiple_expectation_values(self, tol): - """Tests correct output shape and evaluation for a tape - with multiple expval outputs""" - dev = qml.device("default.qubit", wires=2) - x = 0.543 - y = -0.654 - - with QubitParamShiftTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliX(1)) - - res = tape.jacobian(dev, method="analytic") - assert res.shape == (2, 2) - - expected = np.array([[-np.sin(x), 0], [0, np.cos(y)]]) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_var_expectation_values(self, tol): - """Tests correct output shape and evaluation for a tape - with expval and var outputs""" - dev = qml.device("default.qubit", wires=2) - x = 0.543 - y = -0.654 - - with QubitParamShiftTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.var(qml.PauliX(1)) - - res = tape.jacobian(dev, method="analytic") - assert res.shape == (2, 2) - - expected = np.array([[-np.sin(x), 0], [0, -2 * np.cos(y) * np.sin(y)]]) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_prob_expectation_values(self, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - dev = qml.device("default.qubit", wires=2) - x = 0.543 - y = -0.654 - - with QubitParamShiftTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.probs(wires=[0, 1]) - - res = tape.jacobian(dev, method="analytic") - assert res.shape == (5, 2) - - expected = ( - np.array( - [ - [-2 * np.sin(x), 0], - [ - -(np.cos(y / 2) ** 2 * np.sin(x)), - -(np.cos(x / 2) ** 2 * np.sin(y)), - ], - [ - -(np.sin(x) * np.sin(y / 2) ** 2), - (np.cos(x / 2) ** 2 * np.sin(y)), - ], - [ - (np.sin(x) * np.sin(y / 2) ** 2), - (np.sin(x / 2) ** 2 * np.sin(y)), - ], - [ - (np.cos(y / 2) ** 2 * np.sin(x)), - -(np.sin(x / 2) ** 2 * np.sin(y)), - ], - ] - ) - / 2 - ) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_involutory_variance(self, mocker, tol): - """Tests qubit observable that are involutory""" - dev = qml.device("default.qubit", wires=1) - a = 0.54 - - spy_analytic_var = mocker.spy(QubitParamShiftTape, "parameter_shift_var") - spy_numeric = mocker.spy(QubitParamShiftTape, "numeric_pd") - spy_execute = mocker.spy(dev, "execute") - - with QubitParamShiftTape() as tape: - qml.RX(a, wires=0) - qml.var(qml.PauliZ(0)) - - res = tape.execute(dev) - expected = 1 - np.cos(a) ** 2 - assert np.allclose(res, expected, atol=tol, rtol=0) - - spy_execute.call_args_list = [] - - # circuit jacobians - gradA = tape.jacobian(dev, method="analytic") - spy_analytic_var.assert_called() - spy_numeric.assert_not_called() - assert len(spy_execute.call_args_list) == 1 + 2 * 1 - - spy_execute.call_args_list = [] - - gradF = tape.jacobian(dev, method="numeric") - spy_numeric.assert_called() - assert len(spy_execute.call_args_list) == 2 - - expected = 2 * np.sin(a) * np.cos(a) - - assert gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def test_non_involutory_variance(self, mocker, tol): - """Tests a qubit Hermitian observable that is not involutory""" - dev = qml.device("default.qubit", wires=1) - A = np.array([[4, -1 + 6j], [-1 - 6j, 2]]) - a = 0.54 - - spy_analytic_var = mocker.spy(QubitParamShiftTape, "parameter_shift_var") - spy_numeric = mocker.spy(QubitParamShiftTape, "numeric_pd") - spy_execute = mocker.spy(dev, "execute") - - with QubitParamShiftTape() as tape: - qml.RX(a, wires=0) - qml.var(qml.Hermitian(A, 0)) - - tape.trainable_params = {0} - - res = tape.execute(dev) - expected = (39 / 2) - 6 * np.sin(2 * a) + (35 / 2) * np.cos(2 * a) - assert np.allclose(res, expected, atol=tol, rtol=0) - - spy_execute.call_args_list = [] - - # circuit jacobians - gradA = tape.jacobian(dev, method="analytic") - spy_analytic_var.assert_called() - spy_numeric.assert_not_called() - assert len(spy_execute.call_args_list) == 1 + 4 * 1 - - spy_execute.call_args_list = [] - - gradF = tape.jacobian(dev, method="numeric") - spy_numeric.assert_called() - assert len(spy_execute.call_args_list) == 2 - - expected = -35 * np.sin(2 * a) - 12 * np.cos(2 * a) - assert gradA == pytest.approx(expected, abs=tol) - assert gradF == pytest.approx(expected, abs=tol) - - def test_involutory_and_noninvolutory_variance(self, mocker, tol): - """Tests a qubit Hermitian observable that is not involutory alongside - a involutory observable.""" - dev = qml.device("default.qubit", wires=2) - A = np.array([[4, -1 + 6j], [-1 - 6j, 2]]) - a = 0.54 - - spy_analytic_var = mocker.spy(QubitParamShiftTape, "parameter_shift_var") - spy_numeric = mocker.spy(QubitParamShiftTape, "numeric_pd") - spy_execute = mocker.spy(dev, "execute") - - with QubitParamShiftTape() as tape: - qml.RX(a, wires=0) - qml.RX(a, wires=1) - qml.var(qml.PauliZ(0)) - qml.var(qml.Hermitian(A, 1)) - - tape.trainable_params = {0, 1} - - res = tape.execute(dev) - expected = [1 - np.cos(a) ** 2, (39 / 2) - 6 * np.sin(2 * a) + (35 / 2) * np.cos(2 * a)] - assert np.allclose(res, expected, atol=tol, rtol=0) - - spy_execute.call_args_list = [] - - # circuit jacobians - gradA = tape.jacobian(dev, method="analytic") - spy_analytic_var.assert_called() - spy_numeric.assert_not_called() - assert len(spy_execute.call_args_list) == 1 + 2 * 4 - - spy_execute.call_args_list = [] - - gradF = tape.jacobian(dev, method="numeric") - spy_numeric.assert_called() - assert len(spy_execute.call_args_list) == 1 + 2 - - expected = [2 * np.sin(a) * np.cos(a), -35 * np.sin(2 * a) - 12 * np.cos(2 * a)] - assert np.diag(gradA) == pytest.approx(expected, abs=tol) - assert np.diag(gradF) == pytest.approx(expected, abs=tol) - - def test_expval_and_variance(self, tol): - """Test that the qnode works for a combination of expectation - values and variances""" - dev = qml.device("default.qubit", wires=3) - - a = 0.54 - b = -0.423 - c = 0.123 - - with QubitParamShiftTape() as tape: - qml.RX(a, wires=0) - qml.RY(b, wires=1) - qml.CNOT(wires=[1, 2]) - qml.RX(c, wires=2) - qml.CNOT(wires=[0, 1]) - qml.var(qml.PauliZ(0)) - qml.expval(qml.PauliZ(1)) - qml.var(qml.PauliZ(2)) - - res = tape.execute(dev) - expected = np.array( - [ - np.sin(a) ** 2, - np.cos(a) * np.cos(b), - 0.25 * (3 - 2 * np.cos(b) ** 2 * np.cos(2 * c) - np.cos(2 * b)), - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - # # circuit jacobians - gradA = tape.jacobian(dev, method="analytic") - gradF = tape.jacobian(dev, method="numeric") - expected = np.array( - [ - [2 * np.cos(a) * np.sin(a), -np.cos(b) * np.sin(a), 0], - [ - 0, - -np.cos(a) * np.sin(b), - 0.5 * (2 * np.cos(b) * np.cos(2 * c) * np.sin(b) + np.sin(2 * b)), - ], - [0, 0, np.cos(b) ** 2 * np.sin(2 * c)], - ] - ).T - assert gradA == pytest.approx(expected, abs=tol) - assert gradF == pytest.approx(expected, abs=tol) - - -class TestHessian: - """Tests for parameter Hessian method""" - - @pytest.mark.parametrize("s1", [np.pi / 2, np.pi / 4, 2]) - @pytest.mark.parametrize("s2", [np.pi / 2, np.pi / 4, 3]) - @pytest.mark.parametrize("G", [qml.RX, qml.RY, qml.RZ, qml.PhaseShift]) - def test_pauli_rotation_hessian(self, s1, s2, G, tol): - """Tests that the automatic Hessian of Pauli rotations are correct.""" - theta = np.array([0.234, 2.443]) - dev = qml.device("default.qubit", wires=2) - - with QubitParamShiftTape() as tape: - qml.QubitStateVector(np.array([1.0, -1.0, 1.0, -1.0]) / np.sqrt(4), wires=[0, 1]) - G(theta[0], wires=[0]) - G(theta[1], wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {1, 2} - - autograd_val = tape.hessian(dev, s1=s1, s2=s2) - - assert autograd_val.shape == (len(theta), len(theta)) - - shift = np.eye(len(theta)) - manualgrad_val = np.zeros((len(theta), len(theta))) - for i in range(len(theta)): - for j in range(len(theta)): - manualgrad_val[i, j] = ( - tape.execute(dev, params=theta + s1 * shift[i] + s2 * shift[j]) - - tape.execute(dev, params=theta - s1 * shift[i] + s2 * shift[j]) - - tape.execute(dev, params=theta + s1 * shift[i] - s2 * shift[j]) - + tape.execute(dev, params=theta - s1 * shift[i] - s2 * shift[j]) - ) / (4 * np.sin(s1) * np.sin(s2)) - - assert np.allclose(autograd_val, manualgrad_val, atol=tol, rtol=0) - - def test_vector_output(self, tol): - """Tests that a vector valued output tape has a hessian with the proper result.""" - - dev = qml.device("default.qubit", wires=1) - - x = np.array([1.0, 2.0]) - - with QubitParamShiftTape() as tape: - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - qml.probs(wires=[0]) - - hess = tape.hessian(dev) - - expected_hess = expected_hess = np.array( - [ - [ - [-0.5 * np.cos(x[0]) * np.cos(x[1]), 0.5 * np.cos(x[0]) * np.cos(x[1])], - [0.5 * np.sin(x[0]) * np.sin(x[1]), -0.5 * np.sin(x[0]) * np.sin(x[1])], - ], - [ - [0.5 * np.sin(x[0]) * np.sin(x[1]), -0.5 * np.sin(x[0]) * np.sin(x[1])], - [-0.5 * np.cos(x[0]) * np.cos(x[1]), 0.5 * np.cos(x[0]) * np.cos(x[1])], - ], - ] - ) - - assert np.allclose(hess, expected_hess, atol=tol, rtol=0) - - def test_no_trainable_params_hessian(self): - """Test that an empty Hessian is returned when there are no trainable - parameters.""" - dev = qml.device("default.qubit", wires=1) - - with QubitParamShiftTape() as tape: - qml.RX(0.224, wires=[0]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {} - - hessian = tape.hessian(dev) - - assert hessian.shape == (0, 0) - - @pytest.mark.parametrize("G", [qml.CRX, qml.CRY, qml.CRZ]) - def test_controlled_rotation_error(self, G, tol): - """Test that attempting to perform the parameter-shift rule on the controlled rotation gates - results in an error.""" - dev = qml.device("default.qubit", wires=2) - b = 0.123 - - with QubitParamShiftTape() as tape: - qml.QubitStateVector(np.array([1.0, -1.0]) / np.sqrt(2), wires=0) - G(b, wires=[0, 1]) - qml.expval(qml.PauliX(0)) - - tape.trainable_params = {1} - - res = tape.execute(dev) - assert np.allclose(res, -np.cos(b / 2), atol=tol, rtol=0) - - with pytest.raises(ValueError, match="not supported for the parameter-shift Hessian"): - tape.hessian(dev, method="analytic") - - @pytest.mark.parametrize("G", [qml.CRX, qml.CRY, qml.CRZ]) - def test_controlled_rotation_second_derivative(self, G, tol): - """Test that the controlled rotation gates return the correct - second derivative if first decomposed.""" - dev = qml.device("default.qubit", wires=2) - init_state = qml.numpy.array([1.0, -1.0], requires_grad=False) / np.sqrt(2) - - @qml.qnode(dev) - def circuit(b): - qml.QubitStateVector(init_state, wires=0) - G(b, wires=[0, 1]) - return qml.expval(qml.PauliX(0)) - - b = pnp.array(0.123, requires_grad=True) - - res = circuit(b) - assert np.allclose(res, -np.cos(b / 2), atol=tol, rtol=0) - - grad = qml.grad(qml.grad(circuit))(b) - expected = np.cos(b / 2) / 4 - assert np.allclose(grad, expected, atol=tol, rtol=0) - - def test_non_differentiable_controlled_rotation(self, tol): - """Tests that a non-differentiable controlled operation does not affect - the Hessian computation.""" - - dev = qml.device("default.qubit", wires=2) - - x = 0.6 - - with QubitParamShiftTape() as tape: - qml.RY(x, wires=0) - qml.CRY(np.pi / 2, wires=[0, 1]) - qml.expval(qml.PauliX(0)) - - tape.trainable_params = {0} - hess = tape.hessian(dev) - - expected_hess = np.array([-np.sin(x) / np.sqrt(2)]) - - assert np.allclose(hess, expected_hess, atol=tol, rtol=0) diff --git a/tests/tape/test_reversible.py b/tests/tape/test_reversible.py deleted file mode 100644 index b083ba76364..00000000000 --- a/tests/tape/test_reversible.py +++ /dev/null @@ -1,545 +0,0 @@ -# pylint: disable=no-member -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the ReversibleTape""" -import pytest -from pennylane import numpy as np - -import pennylane as qml -from pennylane import numpy as pnp -from pennylane.tape import JacobianTape, ReversibleTape -from pennylane.qnode_old import QNode, qnode - - -thetas = np.linspace(-2 * np.pi, 2 * np.pi, 8) - - -class TestReversibleTape: - """Unit tests for the reversible tape""" - - def test_diff_circuit_construction(self, mocker): - """Test that the diff circuit is correctly constructed""" - dev = qml.device("default.qubit", wires=2) - - with ReversibleTape() as tape: - qml.PauliX(wires=0) - qml.RX(0.542, wires=0) - qml.RY(0.542, wires=0) - qml.expval(qml.PauliZ(0)) - - spy = mocker.spy(dev, "execute") - tape.jacobian(dev) - - tape0 = spy.call_args_list[0][0][0] - tape1 = spy.call_args_list[1][0][0] - tape2 = spy.call_args_list[2][0][0] - - assert tape0 is tape - - assert len(tape1.operations) == 4 - assert len(tape1.measurements) == 1 - assert tape1.operations[0].name == "QubitStateVector" - assert tape1.operations[1].name == "RY.inv" - assert tape1.operations[2].name == "PauliX" - assert tape1.operations[3].name == "RY" - - assert len(tape2.operations) == 2 - assert len(tape1.measurements) == 1 - assert tape1.operations[0].name == "QubitStateVector" - assert tape2.operations[1].name == "PauliY" - - def test_rot_diff_circuit_construction(self, mocker): - """Test that the diff circuit is correctly constructed for the Rot gate""" - dev = qml.device("default.qubit", wires=2) - - with ReversibleTape() as tape: - qml.PauliX(wires=0) - qml.Rot(0.1, 0.2, 0.3, wires=0) - qml.expval(qml.PauliZ(0)) - - spy = mocker.spy(dev, "execute") - tape.jacobian(dev) - - tape0 = spy.call_args_list[0][0][0] - tape1 = spy.call_args_list[1][0][0] - tape2 = spy.call_args_list[2][0][0] - tape3 = spy.call_args_list[3][0][0] - - assert tape0 is tape - - assert len(tape1.operations) == 6 - assert len(tape1.measurements) == 1 - assert tape1.operations[0].name == "QubitStateVector" - assert tape1.operations[1].name == "RZ.inv" - assert tape1.operations[2].name == "RY.inv" - assert tape1.operations[3].name == "PauliZ" - assert tape1.operations[4].name == "RY" - assert tape1.operations[5].name == "RZ" - - assert len(tape2.operations) == 4 - assert len(tape1.measurements) == 1 - assert tape1.operations[0].name == "QubitStateVector" - assert tape2.operations[1].name == "RZ.inv" - assert tape2.operations[2].name == "PauliY" - assert tape2.operations[3].name == "RZ" - - assert len(tape3.operations) == 2 - assert len(tape1.measurements) == 1 - assert tape1.operations[0].name == "QubitStateVector" - assert tape3.operations[1].name == "PauliZ" - - @pytest.mark.parametrize("op, name", [(qml.CRX, "CRX"), (qml.CRY, "CRY"), (qml.CRZ, "CRZ")]) - def test_controlled_rotation_gates_exception(self, op, name): - """Tests that an exception is raised when a controlled - rotation gate is used with the ReversibleTape.""" - # TODO: remove this test when this support is added - dev = qml.device("default.qubit", wires=2) - - with ReversibleTape() as tape: - qml.PauliX(wires=0) - op(0.542, wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - with pytest.raises(ValueError, match=f"The {name} gate is not currently supported"): - tape.jacobian(dev) - - def test_var_exception(self): - """Tests that an exception is raised when variance - is used with the ReversibleTape.""" - # TODO: remove this test when this support is added - dev = qml.device("default.qubit", wires=2) - - with ReversibleTape() as tape: - qml.PauliX(wires=0) - qml.RX(0.542, wires=0) - qml.var(qml.PauliZ(0)) - - with pytest.raises(ValueError, match="Variance is not supported"): - tape.jacobian(dev) - - def test_probs_exception(self): - """Tests that an exception is raised when probability - is used with the ReversibleTape.""" - # TODO: remove this test when this support is added - dev = qml.device("default.qubit", wires=2) - - with ReversibleTape() as tape: - qml.PauliX(wires=0) - qml.RX(0.542, wires=0) - qml.probs(wires=[0, 1]) - - with pytest.raises(ValueError, match="Probability is not supported"): - tape.jacobian(dev) - - def test_hamiltonian_error(self): - """Tests that an exception is raised when a Hamiltonian - is used with the ReversibleTape.""" - with ReversibleTape() as tape: - qml.RX(0.542, wires=0) - qml.expval(1.0 * qml.PauliZ(0) @ qml.PauliX(1)) - - dev = qml.device("default.qubit", wires=2) - - with pytest.raises(qml.QuantumFunctionError, match="does not support Hamiltonian"): - tape.jacobian(dev, method="analytic") - - def test_phaseshift_exception(self): - """Tests that an exception is raised when a PhaseShift gate - is used with the ReversibleTape.""" - # TODO: remove this test when this support is added - dev = qml.device("default.qubit", wires=1) - - with ReversibleTape() as tape: - qml.PauliX(wires=0) - qml.PhaseShift(0.542, wires=0) - qml.expval(qml.PauliZ(0)) - - with pytest.raises(ValueError, match="The PhaseShift gate is not currently supported"): - tape.jacobian(dev) - - -class TestGradients: - """Jacobian integration tests for qubit expectations.""" - - def test_finite_shots_warning(self): - """Test that a warning is raised when calling the jacobian with a device with finite shots""" - - with ReversibleTape() as tape: - qml.RX(0.1, wires=0) - qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=1, shots=1) - - with pytest.warns( - UserWarning, - match="Requested reversible differentiation to be computed with finite shots.", - ): - tape.jacobian(dev) - - @pytest.mark.parametrize("theta", np.linspace(-2 * np.pi, 2 * np.pi, 7)) - @pytest.mark.parametrize("G", [qml.RX, qml.RY, qml.RZ]) - def test_pauli_rotation_gradient(self, G, theta, tol): - """Tests that the automatic gradients of Pauli rotations are correct.""" - dev = qml.device("default.qubit", wires=1) - - with ReversibleTape() as tape: - qml.QubitStateVector(np.array([1.0, -1.0]) / np.sqrt(2), wires=0) - G(theta, wires=[0]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {1} - - autograd_val = tape.jacobian(dev, method="analytic") - - # compare to finite differences - numeric_val = tape.jacobian(dev, method="numeric") - assert np.allclose(autograd_val, numeric_val, atol=tol, rtol=0) - - @pytest.mark.parametrize("theta", np.linspace(-2 * np.pi, 2 * np.pi, 7)) - def test_Rot_gradient(self, theta, tol): - """Tests that the automatic gradient of a arbitrary Euler-angle-parameterized gate is correct.""" - dev = qml.device("default.qubit", wires=1) - params = np.array([theta, theta**3, np.sqrt(2) * theta]) - - with ReversibleTape() as tape: - qml.QubitStateVector(np.array([1.0, -1.0]) / np.sqrt(2), wires=0) - qml.Rot(*params, wires=[0]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {1, 2, 3} - - autograd_val = tape.jacobian(dev, method="analytic") - - # compare to finite differences - numeric_val = tape.jacobian(dev, method="numeric") - assert np.allclose(autograd_val, numeric_val, atol=tol, rtol=0) - - @pytest.mark.parametrize("par", [1, -2, 1.623, -0.051, 0]) # intergers, floats, zero - def test_ry_gradient(self, par, mocker, tol): - """Test that the gradient of the RY gate matches the exact analytic - formula. Further, make sure the correct gradient methods - are being called.""" - - with ReversibleTape() as tape: - qml.RY(par, wires=[0]) - qml.expval(qml.PauliX(0)) - - tape.trainable_params = {0} - - dev = qml.device("default.qubit", wires=1) - - spy_numeric = mocker.spy(tape, "numeric_pd") - spy_analytic = mocker.spy(tape, "analytic_pd") - - # gradients - exact = np.cos(par) - grad_F = tape.jacobian(dev, method="numeric") - - spy_numeric.assert_called() - spy_analytic.assert_not_called() - - spy_device = mocker.spy(tape, "execute_device") - grad_A = tape.jacobian(dev, method="analytic") - - spy_analytic.assert_called() - spy_device.assert_called_once() # check that the state was only pre-computed once - - # different methods must agree - assert np.allclose(grad_F, exact, atol=tol, rtol=0) - assert np.allclose(grad_A, exact, atol=tol, rtol=0) - - def test_rx_gradient(self, tol): - """Test that the gradient of the RX gate matches the known formula.""" - dev = qml.device("default.qubit", wires=2) - a = 0.7418 - - with ReversibleTape() as tape: - qml.RX(a, wires=0) - qml.expval(qml.PauliZ(0)) - - circuit_output = tape.execute(dev) - expected_output = np.cos(a) - assert np.allclose(circuit_output, expected_output, atol=tol, rtol=0) - - # circuit jacobians - circuit_jacobian = tape.jacobian(dev, method="analytic") - expected_jacobian = -np.sin(a) - assert np.allclose(circuit_jacobian, expected_jacobian, atol=tol, rtol=0) - - def test_multiple_rx_gradient(self, tol): - """Tests that the gradient of multiple RX gates in a circuit - yeilds the correct result.""" - dev = qml.device("default.qubit", wires=3) - params = np.array([np.pi, np.pi / 2, np.pi / 3]) - - with ReversibleTape() as tape: - qml.RX(params[0], wires=0) - qml.RX(params[1], wires=1) - qml.RX(params[2], wires=2) - - for idx in range(3): - qml.expval(qml.PauliZ(idx)) - - circuit_output = tape.execute(dev) - expected_output = np.cos(params) - assert np.allclose(circuit_output, expected_output, atol=tol, rtol=0) - - # circuit jacobians - circuit_jacobian = tape.jacobian(dev, method="analytic") - expected_jacobian = -np.diag(np.sin(params)) - assert np.allclose(circuit_jacobian, expected_jacobian, atol=tol, rtol=0) - - @pytest.mark.parametrize( - "op", - [ - qml.RY(0.3, wires=0), - qml.Rot(1.0, 2.0, 3.0, wires=[0]), - ], - ) - def test_compare_analytic_and_numeric_gradients(self, op, mocker, tol): - """Test for selected gates that the gradients of circuits match between the - finite difference and analytic methods.""" - - with ReversibleTape() as tape: - qml.Hadamard(wires=0) - qml.RX(0.543, wires=0) - qml.CNOT(wires=[0, 1]) - - qml.apply(op) - - qml.Rot(1.3, -2.3, 0.5, wires=[0]) - qml.RZ(-0.5, wires=0) - qml.RY(0.5, wires=1) - qml.CNOT(wires=[0, 1]) - - qml.expval(qml.PauliX(wires=0)) - qml.expval(qml.PauliZ(wires=1)) - - dev = qml.device("default.qubit", wires=2) - res = tape.execute(dev) - - tape._update_gradient_info() - tape.trainable_params = set(range(1, 1 + op.num_params)) - - # check that every parameter is analytic - for i in range(op.num_params): - assert tape._par_info[1 + i]["grad_method"][0] == "A" - - grad_F = tape.jacobian(dev, method="numeric") - - spy = mocker.spy(ReversibleTape, "analytic_pd") - spy_execute = mocker.spy(tape, "execute_device") - grad_A = tape.jacobian(dev, method="analytic") - spy.assert_called() - - # check that the execute device method has only been called - # once, for all parameters. - spy_execute.assert_called_once() - - assert np.allclose(grad_A, grad_F, atol=tol, rtol=0) - - def test_gradient_gate_with_multiple_parameters(self, tol): - """Tests that gates with multiple free parameters yield correct gradients.""" - x, y, z = [0.5, 0.3, -0.7] - - with ReversibleTape() as tape: - qml.RX(0.4, wires=[0]) - qml.Rot(x, y, z, wires=[0]) - qml.RY(-0.2, wires=[0]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {1, 2, 3} - - dev = qml.device("default.qubit", wires=1) - grad_A = tape.jacobian(dev, method="analytic") - grad_F = tape.jacobian(dev, method="numeric") - - # gradient has the correct shape and every element is nonzero - assert grad_A.shape == (1, 3) - assert np.count_nonzero(grad_A) == 3 - # the different methods agree - assert np.allclose(grad_A, grad_F, atol=tol, rtol=0) - - -class TestQNodeIntegration: - """Test QNode integration with the reversible method""" - - def test_finite_shots_warning(self): - """Test that a warning is raised when calling the jacobian with a device with finite shots""" - - dev = qml.device("default.qubit", wires=1, shots=1) - - with pytest.warns( - UserWarning, - match="Requested reversible differentiation to be computed with finite shots.", - ): - - @qml.qnode_old.qnode(dev, diff_method="reversible") - def circ(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - with pytest.warns( - UserWarning, - match="Requested reversible differentiation to be computed with finite shots.", - ): - qml.grad(circ)(pnp.array(0.1, requires_grad=True)) - - def test_qnode(self, mocker, tol): - """Test that specifying diff_method allows the reversible - method to be selected""" - args = np.array([0.54, 0.1, 0.5], requires_grad=True) - dev = qml.device("default.qubit", wires=2) - - def circuit(x, y, z): - qml.Hadamard(wires=0) - qml.RX(0.543, wires=0) - qml.CNOT(wires=[0, 1]) - - qml.Rot(x, y, z, wires=0) - - qml.Rot(1.3, -2.3, 0.5, wires=[0]) - qml.RZ(-0.5, wires=0) - qml.RY(0.5, wires=1) - qml.CNOT(wires=[0, 1]) - - return qml.expval(qml.PauliX(0) @ qml.PauliZ(1)) - - qnode1 = QNode(circuit, dev, diff_method="reversible") - spy = mocker.spy(ReversibleTape, "analytic_pd") - - grad_fn = qml.grad(qnode1) - grad_A = grad_fn(*args) - - spy.assert_called() - assert isinstance(qnode1.qtape, ReversibleTape) - - qnode2 = QNode(circuit, dev, diff_method="finite-diff") - grad_fn = qml.grad(qnode2) - grad_F = grad_fn(*args) - - assert not isinstance(qnode2.qtape, ReversibleTape) - assert np.allclose(grad_A, grad_F, atol=tol, rtol=0) - - @pytest.mark.parametrize("reused_p", thetas**3 / 19) - @pytest.mark.parametrize("other_p", thetas**2 / 1) - def test_fanout_multiple_params(self, reused_p, other_p, tol): - """Tests that the correct gradient is computed for qnodes which - use the same parameter in multiple gates.""" - - from gate_data import Rotx as Rx, Roty as Ry, Rotz as Rz - - def expZ(state): - return np.abs(state[0]) ** 2 - np.abs(state[1]) ** 2 - - dev = qml.device("default.qubit", wires=1) - extra_param = np.array(0.31, requires_grad=False) - - @qnode(dev) - def cost(p1, p2): - qml.RX(extra_param, wires=[0]) - qml.RY(p1, wires=[0]) - qml.RZ(p2, wires=[0]) - qml.RX(p1, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - zero_state = np.array([1.0, 0.0]) - - # analytic gradient - grad_fn = qml.grad(cost) - grad_A = grad_fn(reused_p, other_p) - - # manual gradient - grad_true0 = ( - expZ( - Rx(reused_p) @ Rz(other_p) @ Ry(reused_p + np.pi / 2) @ Rx(extra_param) @ zero_state - ) - - expZ( - Rx(reused_p) @ Rz(other_p) @ Ry(reused_p - np.pi / 2) @ Rx(extra_param) @ zero_state - ) - ) / 2 - grad_true1 = ( - expZ( - Rx(reused_p + np.pi / 2) @ Rz(other_p) @ Ry(reused_p) @ Rx(extra_param) @ zero_state - ) - - expZ( - Rx(reused_p - np.pi / 2) @ Rz(other_p) @ Ry(reused_p) @ Rx(extra_param) @ zero_state - ) - ) / 2 - expected = grad_true0 + grad_true1 # product rule - - assert np.allclose(grad_A[0], expected, atol=tol, rtol=0) - - def test_gradient_repeated_gate_parameters(self, mocker, tol): - """Tests that repeated use of a free parameter in a - multi-parameter gate yield correct gradients.""" - dev = qml.device("default.qubit", wires=1) - params = np.array([0.8, 1.3], requires_grad=True) - - def circuit(params): - qml.RX(np.array(np.pi / 4, requires_grad=False), wires=[0]) - qml.Rot(params[1], params[0], 2 * params[0], wires=[0]) - return qml.expval(qml.PauliX(0)) - - spy_numeric = mocker.spy(JacobianTape, "numeric_pd") - spy_analytic = mocker.spy(ReversibleTape, "analytic_pd") - - cost = QNode(circuit, dev, diff_method="finite-diff") - grad_fn = qml.grad(cost) - grad_F = grad_fn(params) - - spy_numeric.assert_called() - spy_analytic.assert_not_called() - - cost = QNode(circuit, dev, diff_method="reversible") - grad_fn = qml.grad(cost) - grad_A = grad_fn(params) - - spy_analytic.assert_called() - - # the different methods agree - assert np.allclose(grad_A, grad_F, atol=tol, rtol=0) - - -class TestHelperFunctions: - """Tests for additional helper functions.""" - - one_qubit_vec1 = np.array([1, 1]) - one_qubit_vec2 = np.array([1, 1j]) - two_qubit_vec = np.array([1, 1, 1, -1]) - single_qubit_obs1 = qml.PauliZ(0) - single_qubit_obs2 = qml.PauliY(0) - two_qubit_obs = qml.Hermitian(np.eye(4), wires=[0, 1]) - - @pytest.mark.parametrize( - "wires, vec1, obs, vec2, expected", - [ - (1, one_qubit_vec1, single_qubit_obs1, one_qubit_vec1, 0), - (1, one_qubit_vec2, single_qubit_obs1, one_qubit_vec2, 0), - (1, one_qubit_vec1, single_qubit_obs1, one_qubit_vec2, 1 - 1j), - (1, one_qubit_vec2, single_qubit_obs1, one_qubit_vec1, 1 + 1j), - (1, one_qubit_vec1, single_qubit_obs2, one_qubit_vec1, 0), - (1, one_qubit_vec2, single_qubit_obs2, one_qubit_vec2, 2), - (1, one_qubit_vec1, single_qubit_obs2, one_qubit_vec2, 1 + 1j), - (1, one_qubit_vec2, single_qubit_obs2, one_qubit_vec1, 1 - 1j), - (2, two_qubit_vec, single_qubit_obs1, two_qubit_vec, 0), - (2, two_qubit_vec, single_qubit_obs2, two_qubit_vec, 0), - (2, two_qubit_vec, two_qubit_obs, two_qubit_vec, 4), - ], - ) - def test_matrix_elem(self, wires, vec1, obs, vec2, expected): - """Tests for the helper function _matrix_elem""" - tape = ReversibleTape() - res = tape._matrix_elem(vec1, obs, vec2, qml.wires.Wires(range(wires))) - assert res == expected From 0d22feee362ba1bafdf739ff8fa76c8e13fccf63 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 13:30:03 +0800 Subject: [PATCH 02/13] remove more files --- pennylane/qnn/keras.py | 8 +- pennylane/qnn/torch.py | 7 +- pennylane/transforms/batch_input.py | 4 +- pennylane/transforms/batch_transform.py | 10 +- pennylane/transforms/specs.py | 4 - tests/interfaces/test_qnode_autograd.py | 1254 ----------------------- tests/interfaces/test_qnode_jax.py | 227 ---- tests/interfaces/test_qnode_tf.py | 1015 ------------------ tests/interfaces/test_qnode_torch.py | 1013 ------------------ tests/interfaces/test_tape_autograd.py | 752 -------------- tests/interfaces/test_tape_jax.py | 123 --- tests/interfaces/test_tape_tf.py | 784 -------------- tests/interfaces/test_tape_torch.py | 496 --------- 13 files changed, 5 insertions(+), 5692 deletions(-) delete mode 100644 tests/interfaces/test_qnode_autograd.py delete mode 100644 tests/interfaces/test_qnode_jax.py delete mode 100644 tests/interfaces/test_qnode_tf.py delete mode 100644 tests/interfaces/test_qnode_torch.py delete mode 100644 tests/interfaces/test_tape_autograd.py delete mode 100644 tests/interfaces/test_tape_jax.py delete mode 100644 tests/interfaces/test_tape_tf.py delete mode 100644 tests/interfaces/test_tape_torch.py diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 26e5565cf74..03f7e17a2cf 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -233,13 +233,7 @@ def __init__( self.qnode = batch_input(qnode, argnum=batch_idx) dtype = tf.float32 if tf.keras.backend.floatx() == tf.float32 else tf.float64 - - try: - # TODO: remove when the old QNode is removed - if self.qnode.diff_method != "backprop" or self.qnode.diff_method_change: - self.qnode.to_tf(dtype=dtype) - except AttributeError: - self.qnode.interface = "tf" + self.qnode.interface = "tf" # Allows output_dim to be specified as an int or as a tuple, e.g, 5, (5,), (5, 2), [5, 2] # Note: Single digit values will be considered an int and multiple as a tuple, e.g [5,] or (5,) diff --git a/pennylane/qnn/torch.py b/pennylane/qnn/torch.py index cc51dc82e03..0874099ea10 100644 --- a/pennylane/qnn/torch.py +++ b/pennylane/qnn/torch.py @@ -215,12 +215,7 @@ def __init__(self, qnode, weight_shapes: dict, init_method: Optional[Callable] = # TODO: update the docstring regarding changes to restrictions when tape mode is default. self._signature_validation(qnode, weight_shapes) self.qnode = qnode - - try: - # TODO: remove when the old QNode is removed - self.qnode.to_torch() - except AttributeError: - self.qnode.interface = "torch" + self.qnode.interface = "torch" if not init_method: init_method = functools.partial(torch.nn.init.uniform_, b=2 * math.pi) diff --git a/pennylane/transforms/batch_input.py b/pennylane/transforms/batch_input.py index b2fd48ad854..08c1da65e45 100644 --- a/pennylane/transforms/batch_input.py +++ b/pennylane/transforms/batch_input.py @@ -22,9 +22,9 @@ @batch_transform def batch_input( - tape: Union[qml.tape.JacobianTape, qml.QNode], + tape: Union[qml.tape.QuantumTape, qml.QNode], argnum: Union[Sequence[int], int] = 0, -) -> Tuple[Sequence[qml.tape.JacobianTape], Callable]: +) -> Tuple[Sequence[qml.tape.QuantumTape], Callable]: """ Transform a QNode to support an initial batch dimension for gate inputs. diff --git a/pennylane/transforms/batch_transform.py b/pennylane/transforms/batch_transform.py index ba46a70faca..531ac003afe 100644 --- a/pennylane/transforms/batch_transform.py +++ b/pennylane/transforms/batch_transform.py @@ -298,14 +298,6 @@ def _wrapper(*args, **kwargs): if interface is None or not self.differentiable: gradient_fn = None - elif gradient_fn in ("best", "parameter-shift"): - # TODO: remove when the old QNode is removed - gradient_fn = qml.gradients.param_shift # pragma: no cover - - elif gradient_fn == "finite-diff": - # TODO: remove when the old QNode is removed - gradient_fn = qml.gradients.finite_diff # pragma: no cover - res = qml.execute( tapes, device=qnode.device, @@ -337,7 +329,7 @@ def __call__(self, *targs, **tkwargs): # tapes, fn = some_transform(tape, *transform_args) return self._tape_wrapper(*targs, **tkwargs)(qnode) - if isinstance(qnode, (qml.QNode, qml.qnode_old.QNode, qml.ExpvalCost)): + if isinstance(qnode, (qml.QNode, qml.ExpvalCost)): # Input is a QNode: # result = some_transform(qnode, *transform_args)(*qnode_args) wrapper = self.qnode_wrapper(qnode, targs, tkwargs) diff --git a/pennylane/transforms/specs.py b/pennylane/transforms/specs.py index 4f317bf32bb..664e9ce2e82 100644 --- a/pennylane/transforms/specs.py +++ b/pennylane/transforms/specs.py @@ -104,10 +104,6 @@ def specs_qnode(*args, **kwargs): finally: qnode.max_expansion = initial_max_expansion - if isinstance(qnode, qml.qnode_old.QNode): - # TODO: remove when the old QNode is removed - return qnode.specs - info = qnode.qtape.specs.copy() info["num_device_wires"] = qnode.device.num_wires diff --git a/tests/interfaces/test_qnode_autograd.py b/tests/interfaces/test_qnode_autograd.py deleted file mode 100644 index 9e0b8062c94..00000000000 --- a/tests/interfaces/test_qnode_autograd.py +++ /dev/null @@ -1,1254 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the autograd interface""" -import pytest -from pennylane import numpy as np - -import pennylane as qml -from pennylane.qnode_old import qnode, QNode -from pennylane.tape import JacobianTape, QubitParamShiftTape - - -@pytest.mark.parametrize( - "dev_name,diff_method", - [ - ["default.qubit", "finite-diff"], - ["default.qubit", "parameter-shift"], - ["default.qubit", "backprop"], - ["default.qubit", "adjoint"], - ], -) -class TestQNode: - """Same tests as above, but this time via the QNode interface!""" - - def test_nondiff_param_unwrapping(self, dev_name, diff_method, mocker): - """Test that non-differentiable parameters are correctly unwrapped - to NumPy ndarrays or floats (if 0-dimensional)""" - if diff_method != "parameter-shift": - pytest.skip("Test only supports parameter-shift") - - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, interface="autograd", diff_method="parameter-shift") - def circuit(x, y): - qml.RX(x[0], wires=0) - qml.Rot(*x[1:], wires=0) - qml.RY(y[0], wires=0) - return qml.expval(qml.PauliZ(0)) - - x = np.array([0.1, 0.2, 0.3, 0.4], requires_grad=False) - y = np.array([0.5], requires_grad=True) - - param_data = [] - - def mock_apply(*args, **kwargs): - for op in args[0]: - param_data.extend(op.data.copy()) - - mocker.patch.object(dev, "apply", side_effect=mock_apply) - circuit(x, y) - assert param_data == [0.1, 0.2, 0.3, 0.4, 0.5] - assert not any(isinstance(p, np.tensor) for p in param_data) - - # test the jacobian works correctly - param_data = [] - qml.grad(circuit)(x, y) - assert param_data == [ - 0.1, - 0.2, - 0.3, - 0.4, - 0.5, - 0.1, - 0.2, - 0.3, - 0.4, - 0.5 + np.pi / 2, - 0.1, - 0.2, - 0.3, - 0.4, - 0.5 - np.pi / 2, - ] - assert not any(isinstance(p, np.tensor) for p in param_data) - - def test_execution_no_interface(self, dev_name, diff_method): - """Test execution works without an interface""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, interface=None) - def circuit(a): - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - return qml.expval(qml.PauliZ(0)) - - a = np.array(0.1, requires_grad=True) - - res = circuit(a) - - assert circuit.qtape.interface == None - - # without the interface, the QNode simply returns a scalar array - assert isinstance(res, np.ndarray) - assert res.shape == tuple() - - # gradients should cause an error - with pytest.raises(TypeError, match="must be real number, not ArrayBox"): - qml.grad(circuit)(a) - - def test_execution_with_interface(self, dev_name, diff_method): - """Test execution works with the interface""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, interface="autograd", diff_method=diff_method) - def circuit(a): - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - return qml.expval(qml.PauliZ(0)) - - a = np.array(0.1, requires_grad=True) - circuit(a) - - assert circuit.qtape.interface == "autograd" - - # the tape is able to deduce trainable parameters - assert circuit.qtape.trainable_params == [0] - - # gradients should work - grad = qml.grad(circuit)(a) - assert isinstance(grad, float) - assert grad.shape == tuple() - - def test_interface_swap(self, dev_name, diff_method, tol): - """Test that the autograd interface can be applied to a QNode - with a pre-existing interface""" - tf = pytest.importorskip("tensorflow", minversion="2.1") - - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, interface="tf", diff_method=diff_method) - def circuit(a): - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - return qml.expval(qml.PauliZ(0)) - - a = tf.Variable(0.1, dtype=tf.float64) - - with tf.GradientTape() as tape: - res_tf = circuit(a) - - grad_tf = tape.gradient(res_tf, a) - - # switch to autograd interface - circuit.to_autograd() - - a = np.array(0.1, requires_grad=True) - - res = circuit(a) - grad = qml.grad(circuit)(a) - - assert np.allclose(res, res_tf, atol=tol, rtol=0) - assert np.allclose(grad, grad_tf, atol=tol, rtol=0) - - def test_jacobian(self, dev_name, diff_method, mocker, tol): - """Test jacobian calculation""" - spy = mocker.spy(JacobianTape, "jacobian") - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=True) - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1))] - - res = circuit(a, b) - - assert circuit.qtape.trainable_params == [0, 1] - assert res.shape == (2,) - - expected = [np.cos(a), -np.cos(a) * np.sin(b)] - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = qml.jacobian(circuit)(a, b) - assert isinstance(res, tuple) and len(res) == 2 - expected = ([-np.sin(a), np.sin(a) * np.sin(b)], [0, -np.cos(a) * np.cos(b)]) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - - if diff_method == "finite-diff": - spy.assert_called() - elif diff_method == "backprop": - spy.assert_not_called() - - def test_jacobian_no_evaluate(self, dev_name, diff_method, mocker, tol): - """Test jacobian calculation when no prior circuit evaluation has been performed""" - spy = mocker.spy(JacobianTape, "jacobian") - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=True) - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1))] - - jac_fn = qml.jacobian(circuit) - res = jac_fn(a, b) - assert isinstance(res, tuple) and len(res) == 2 - expected = ([-np.sin(a), np.sin(a) * np.sin(b)], [0, -np.cos(a) * np.cos(b)]) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - - if diff_method == "finite-diff": - spy.assert_called() - elif diff_method == "backprop": - spy.assert_not_called() - - # call the Jacobian with new parameters - a = np.array(0.6, requires_grad=True) - b = np.array(0.832, requires_grad=True) - - res = jac_fn(a, b) - assert isinstance(res, tuple) and len(res) == 2 - expected = ([-np.sin(a), np.sin(a) * np.sin(b)], [0, -np.cos(a) * np.cos(b)]) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - - def test_jacobian_options(self, dev_name, diff_method, mocker, tol): - """Test setting jacobian options""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - spy = mocker.spy(JacobianTape, "numeric_pd") - - a = np.array([0.1, 0.2], requires_grad=True) - - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, interface="autograd", h=1e-8, order=2) - def circuit(a): - qml.RY(a[0], wires=0) - qml.RX(a[1], wires=0) - return qml.expval(qml.PauliZ(0)) - - qml.jacobian(circuit)(a) - - for args in spy.call_args_list: - assert args[1]["order"] == 2 - assert args[1]["h"] == 1e-8 - - def test_changing_trainability(self, dev_name, diff_method, mocker, tol): - """Test changing the trainability of parameters changes the - number of differentiation requests made""" - if diff_method != "parameter-shift": - pytest.skip("Test only supports parameter-shift") - - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=True) - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, interface="autograd", diff_method="parameter-shift") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1)) - - def loss(a, b): - return np.sum(circuit(a, b)) - - grad_fn = qml.grad(loss) - spy = mocker.spy(QubitParamShiftTape, "parameter_shift") - - res = grad_fn(a, b) - - # the tape has reported both arguments as trainable - assert circuit.qtape.trainable_params == [0, 1] - - expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] - assert np.allclose(res, expected, atol=tol, rtol=0) - - # The parameter-shift rule has been called for each argument - assert len(spy.call_args_list) == 2 - - # make the second QNode argument a constant - a = np.array(0.54, requires_grad=True) - b = np.array(0.8, requires_grad=False) - - spy.call_args_list = [] - res = grad_fn(a, b) - - # the tape has reported only the first argument as trainable - assert circuit.qtape.trainable_params == [0] - - expected = [-np.sin(a) + np.sin(a) * np.sin(b)] - assert np.allclose(res, expected, atol=tol, rtol=0) - - # JacobianTape.numeric_pd has been called only once - assert len(spy.call_args_list) == 1 - - # trainability also updates on evaluation - a = np.array(0.54, requires_grad=False) - b = np.array(0.8, requires_grad=True) - circuit(a, b) - assert circuit.qtape.trainable_params == [1] - - def test_classical_processing(self, dev_name, diff_method, tol): - """Test classical processing within the quantum tape""" - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=False) - c = np.array(0.3, requires_grad=True) - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(a, b, c): - qml.RY(a * c, wires=0) - qml.RZ(b, wires=0) - qml.RX(c + c**2 + np.sin(a), wires=0) - return qml.expval(qml.PauliZ(0)) - - res = qml.jacobian(circuit)(a, b, c) - - if diff_method == "finite-diff": - assert circuit.qtape.trainable_params == [0, 2] - tape_params = np.array(circuit.qtape.get_parameters()) - assert np.all(tape_params == [a * c, c + c**2 + np.sin(a)]) - - assert isinstance(res, tuple) and len(res) == 2 - - def test_no_trainable_parameters(self, dev_name, diff_method, tol): - """Test evaluation and Jacobian if there are no trainable parameters""" - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - a = np.array(0.1, requires_grad=False) - b = np.array(0.2, requires_grad=False) - - res = circuit(a, b) - - if diff_method == "finite-diff": - assert circuit.qtape.trainable_params == [] - - assert res.shape == (2,) - assert isinstance(res, np.ndarray) - - with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"): - assert not qml.jacobian(circuit)(a, b) - - def cost(a, b): - return np.sum(circuit(a, b)) - - with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"): - grad = qml.grad(cost)(a, b) - - assert grad == tuple() - - def test_matrix_parameter(self, dev_name, diff_method, tol): - """Test that the autograd interface works correctly - with a matrix parameter""" - U = np.array([[0, 1], [1, 0]], requires_grad=False) - a = np.array(0.1, requires_grad=True) - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(U, a): - qml.QubitUnitary(U, wires=0) - qml.RY(a, wires=0) - return qml.expval(qml.PauliZ(0)) - - res = circuit(U, a) - - if diff_method == "finite-diff": - assert circuit.qtape.trainable_params == [1] - - res = qml.grad(circuit)(U, a) - assert np.allclose(res, np.sin(a), atol=tol, rtol=0) - - def test_differentiable_expand(self, dev_name, diff_method, tol): - """Test that operation and nested tapes expansion - is differentiable""" - - class U3(qml.U3): - def expand(self): - theta, phi, lam = self.data - wires = self.wires - - with JacobianTape() as tape: - qml.Rot(lam, theta, -lam, wires=wires) - qml.PhaseShift(phi + lam, wires=wires) - - return tape - - dev = qml.device(dev_name, wires=1) - a = np.array(0.1, requires_grad=False) - p = np.array([0.1, 0.2, 0.3], requires_grad=True) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(a, p): - qml.RX(a, wires=0) - U3(p[0], p[1], p[2], wires=0) - return qml.expval(qml.PauliX(0)) - - res = circuit(a, p) - assert circuit.qtape.trainable_params == [1, 2, 3, 4] - - assert [i.name for i in circuit.qtape.operations] == ["RX", "Rot", "PhaseShift"] - assert np.all(circuit.qtape.get_parameters() == [p[2], p[0], -p[2], p[1] + p[2]]) - - expected = np.cos(a) * np.cos(p[1]) * np.sin(p[0]) + np.sin(a) * ( - np.cos(p[2]) * np.sin(p[1]) + np.cos(p[0]) * np.cos(p[1]) * np.sin(p[2]) - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = qml.grad(circuit)(a, p) - expected = np.array( - [ - np.cos(p[1]) * (np.cos(a) * np.cos(p[0]) - np.sin(a) * np.sin(p[0]) * np.sin(p[2])), - np.cos(p[1]) * np.cos(p[2]) * np.sin(a) - - np.sin(p[1]) - * (np.cos(a) * np.sin(p[0]) + np.cos(p[0]) * np.sin(a) * np.sin(p[2])), - np.sin(a) - * (np.cos(p[0]) * np.cos(p[1]) * np.cos(p[2]) - np.sin(p[1]) * np.sin(p[2])), - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_probability_differentiation(self, dev_name, diff_method, tol): - """Tests correct output shape and evaluation for a tape - with a single prob output""" - - if diff_method == "adjoint": - pytest.skip("The adjoint method does not currently support returning probabilities") - - dev = qml.device(dev_name, wires=2) - x = np.array(0.543, requires_grad=True) - y = np.array(-0.654, requires_grad=True) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.probs(wires=[1]) - - res = qml.jacobian(circuit)(x, y) - assert isinstance(res, tuple) and len(res) == 2 - - expected = ( - [-np.sin(x) * np.cos(y) / 2, np.cos(y) * np.sin(x) / 2], - [-np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], - ) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - - def test_multiple_probability_differentiation(self, dev_name, diff_method, tol): - """Tests correct output shape and evaluation for a tape - with multiple prob outputs""" - - if diff_method == "adjoint": - pytest.skip("The adjoint method does not currently support returning probabilities") - - dev = qml.device(dev_name, wires=2) - x = np.array(0.543, requires_grad=True) - y = np.array(-0.654, requires_grad=True) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.probs(wires=[0]), qml.probs(wires=[1]) - - res = circuit(x, y) - - expected = np.array( - [ - [np.cos(x / 2) ** 2, np.sin(x / 2) ** 2], - [(1 + np.cos(x) * np.cos(y)) / 2, (1 - np.cos(x) * np.cos(y)) / 2], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = qml.jacobian(circuit)(x, y) - assert isinstance(res, tuple) and len(res) == 2 - - expected = ( - [ - [-np.sin(x) / 2, np.sin(x) / 2], - [-np.sin(x) * np.cos(y) / 2, np.sin(x) * np.cos(y) / 2], - ], - [ - [0, 0], - [-np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], - ], - ) - - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - - def test_ragged_differentiation(self, dev_name, diff_method, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - if diff_method == "adjoint": - pytest.skip("The adjoint method does not currently support returning probabilities") - - dev = qml.device(dev_name, wires=2) - x = np.array(0.543, requires_grad=True) - y = np.array(-0.654, requires_grad=True) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return [qml.expval(qml.PauliZ(0)), qml.probs(wires=[1])] - - res = circuit(x, y) - - expected = np.array( - [np.cos(x), (1 + np.cos(x) * np.cos(y)) / 2, (1 - np.cos(x) * np.cos(y)) / 2] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = qml.jacobian(circuit)(x, y) - assert isinstance(res, tuple) and len(res) == 2 - - expected = ( - [-np.sin(x), -np.sin(x) * np.cos(y) / 2, np.cos(y) * np.sin(x) / 2], - [0, -np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], - ) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - - def test_ragged_differentiation_variance(self, dev_name, diff_method, tol): - """Tests correct output shape and evaluation for a tape - with prob and variance outputs""" - if diff_method == "adjoint": - pytest.skip("The adjoint method does not currently support returning probabilities") - - dev = qml.device(dev_name, wires=2) - x = np.array(0.543, requires_grad=True) - y = np.array(-0.654, requires_grad=True) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return [qml.var(qml.PauliZ(0)), qml.probs(wires=[1])] - - res = circuit(x, y) - - expected = np.array( - [np.sin(x) ** 2, (1 + np.cos(x) * np.cos(y)) / 2, (1 - np.cos(x) * np.cos(y)) / 2] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = qml.jacobian(circuit)(x, y) - assert isinstance(res, tuple) and len(res) == 2 - - expected = ( - [np.sin(2 * x), -np.sin(x) * np.cos(y) / 2, np.sin(x) * np.cos(y) / 2], - [0, -np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], - ) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - - def test_sampling(self, dev_name, diff_method): - """Test sampling works as expected""" - - if diff_method == "backprop": - pytest.skip("Sampling not possible with backprop differentiation.") - - dev = qml.device(dev_name, wires=2, shots=10) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(): - qml.Hadamard(wires=[0]) - qml.CNOT(wires=[0, 1]) - return [qml.sample(qml.PauliZ(0)), qml.sample(qml.PauliX(1))] - - res = circuit() - - assert res.shape == (2, 10) - assert isinstance(res, np.ndarray) - - def test_gradient_non_differentiable_exception(self, dev_name, diff_method): - """Test that an exception is raised if non-differentiable data is - differentiated""" - dev = qml.device(dev_name, wires=2) - - @qml.qnode_old.qnode(dev, interface="autograd", diff_method=diff_method) - def circuit(data1): - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - grad_fn = qml.grad(circuit, argnum=0) - data1 = np.array([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - - with pytest.raises(qml.numpy.NonDifferentiableError, match="is non-differentiable"): - grad_fn(data1) - - def test_chained_qnodes(self, dev_name, diff_method): - """Test that the gradient of chained QNodes works without error""" - dev = qml.device(dev_name, wires=2) - - @qml.qnode_old.qnode(dev, interface="autograd", diff_method=diff_method) - def circuit1(weights): - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - @qml.qnode_old.qnode(dev, interface="autograd", diff_method=diff_method) - def circuit2(data, weights): - qml.templates.AngleEmbedding(data, wires=[0, 1]) - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - return qml.expval(qml.PauliX(0)) - - def cost(w1, w2): - c1 = circuit1(w1) - c2 = circuit2(c1, w2) - return np.sum(c2) ** 2 - - w1 = qml.templates.StronglyEntanglingLayers.shape(n_layers=3, n_wires=2) - w2 = qml.templates.StronglyEntanglingLayers.shape(n_layers=4, n_wires=2) - - weights = [ - np.random.random(w1, requires_grad=True), - np.random.random(w2, requires_grad=True), - ] - - grad_fn = qml.grad(cost) - res = grad_fn(*weights) - - assert len(res) == 2 - - def test_chained_gradient_value(self, dev_name, diff_method, tol): - """Test that the returned gradient value for two chained qubit QNodes - is correct.""" - dev1 = qml.device(dev_name, wires=3) - - @qml.qnode_old.qnode(dev1, diff_method=diff_method) - def circuit1(a, b, c): - qml.RX(a, wires=0) - qml.RX(b, wires=1) - qml.RX(c, wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(2)) - - dev2 = qml.device("default.qubit", wires=2) - - @qml.qnode_old.qnode(dev2, diff_method=diff_method) - def circuit2(data, weights): - qml.RX(data[0], wires=0) - qml.RX(data[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.RZ(weights[0], wires=0) - qml.RZ(weights[1], wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliX(0) @ qml.PauliY(1)) - - def cost(a, b, c, weights): - return circuit2(circuit1(a, b, c), weights) - - grad_fn = qml.grad(cost) - - # Set the first parameter of circuit1 as non-differentiable. - a = np.array(0.4, requires_grad=False) - - # The remaining free parameters are all differentiable. - b = np.array(0.5, requires_grad=True) - c = np.array(0.1, requires_grad=True) - weights = np.array([0.2, 0.3], requires_grad=True) - - res = grad_fn(a, b, c, weights) - - # Output should have shape [dcost/db, dcost/dc, dcost/dw], - # where b,c are scalars, and w is a vector of length 2. - assert len(res) == 3 - assert res[0].shape == tuple() # scalar - assert res[1].shape == tuple() # scalar - assert res[2].shape == (2,) # vector - - cacbsc = np.cos(a) * np.cos(b) * np.sin(c) - - expected = np.array( - [ - # analytic expression for dcost/db - -np.cos(a) - * np.sin(b) - * np.sin(c) - * np.cos(cacbsc) - * np.sin(weights[0]) - * np.sin(np.cos(a)), - # analytic expression for dcost/dc - np.cos(a) - * np.cos(b) - * np.cos(c) - * np.cos(cacbsc) - * np.sin(weights[0]) - * np.sin(np.cos(a)), - # analytic expression for dcost/dw[0] - np.sin(cacbsc) * np.cos(weights[0]) * np.sin(np.cos(a)), - # analytic expression for dcost/dw[1] - 0, - ] - ) - - # np.hstack 'flattens' the ragged gradient array allowing it - # to be compared with the expected result - assert np.allclose(np.hstack(res), expected, atol=tol, rtol=0) - - if diff_method != "backprop": - # Check that the gradient was computed - # for all parameters in circuit2 - assert circuit2.qtape.trainable_params == [0, 1, 2, 3] - - # Check that the parameter-shift rule was not applied - # to the first parameter of circuit1. - assert circuit1.qtape.trainable_params == [1, 2] - - def test_second_derivative(self, dev_name, diff_method, mocker, tol): - """Test second derivative calculation of a scalar valued QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - return qml.expval(qml.PauliZ(0)) - - x = np.array([1.0, 2.0], requires_grad=True) - res = circuit(x) - g = qml.grad(circuit)(x) - - spy = mocker.spy(JacobianTape, "hessian") - g2 = qml.grad(lambda x: np.sum(qml.grad(circuit)(x)))(x) - - if diff_method == "parameter-shift": - spy.assert_called_once() - elif diff_method == "backprop": - spy.assert_not_called() - - a, b = x - - expected_res = np.cos(a) * np.cos(b) - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - expected_g = [-np.sin(a) * np.cos(b), -np.cos(a) * np.sin(b)] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - expected_g2 = [ - -np.cos(a) * np.cos(b) + np.sin(a) * np.sin(b), - np.sin(a) * np.sin(b) - np.cos(a) * np.cos(b), - ] - assert np.allclose(g2, expected_g2, atol=tol, rtol=0) - - def test_hessian(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a scalar valued QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - return qml.expval(qml.PauliZ(0)) - - x = np.array([1.0, 2.0], requires_grad=True) - res = circuit(x) - - a, b = x - - expected_res = np.cos(a) * np.cos(b) - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - grad_fn = qml.grad(circuit) - g = grad_fn(x) - - expected_g = [-np.sin(a) * np.cos(b), -np.cos(a) * np.sin(b)] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - spy = mocker.spy(JacobianTape, "hessian") - hess = qml.jacobian(grad_fn)(x) - - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called_once() - - expected_hess = [ - [-np.cos(a) * np.cos(b), np.sin(a) * np.sin(b)], - [np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)], - ] - assert np.allclose(hess, expected_hess, atol=tol, rtol=0) - - def test_hessian_unused_parameter(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a scalar valued QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(x): - qml.RY(x[0], wires=0) - return qml.expval(qml.PauliZ(0)) - - x = np.array([1.0, 2.0], requires_grad=True) - res = circuit(x) - - a, b = x - - expected_res = np.cos(a) - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - grad_fn = qml.grad(circuit) - g = grad_fn(x) - - expected_g = [-np.sin(a), 0] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - spy = mocker.spy(JacobianTape, "hessian") - hess = qml.jacobian(grad_fn)(x) - - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called_once() - - expected_hess = [ - [-np.cos(a), 0], - [0, 0], - ] - assert np.allclose(hess, expected_hess, atol=tol, rtol=0) - - def test_hessian_vector_valued(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a vector valued QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - return qml.probs(wires=0) - - x = np.array([1.0, 2.0], requires_grad=True) - res = circuit(x) - - a, b = x - - expected_res = [0.5 + 0.5 * np.cos(a) * np.cos(b), 0.5 - 0.5 * np.cos(a) * np.cos(b)] - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - jac_fn = qml.jacobian(circuit) - g = jac_fn(x) - - expected_g = [ - [-0.5 * np.sin(a) * np.cos(b), -0.5 * np.cos(a) * np.sin(b)], - [0.5 * np.sin(a) * np.cos(b), 0.5 * np.cos(a) * np.sin(b)], - ] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - spy = mocker.spy(JacobianTape, "hessian") - hess = qml.jacobian(jac_fn)(x) - - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called_once() - - expected_hess = [ - [ - [-0.5 * np.cos(a) * np.cos(b), 0.5 * np.sin(a) * np.sin(b)], - [0.5 * np.sin(a) * np.sin(b), -0.5 * np.cos(a) * np.cos(b)], - ], - [ - [0.5 * np.cos(a) * np.cos(b), -0.5 * np.sin(a) * np.sin(b)], - [-0.5 * np.sin(a) * np.sin(b), 0.5 * np.cos(a) * np.cos(b)], - ], - ] - assert np.allclose(hess, expected_hess, atol=tol, rtol=0) - - def test_hessian_vector_valued_postprocessing(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a vector valued QNode with post-processing""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=0) - return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(0))] - - def cost_fn(x): - return x @ circuit(x) - - x = np.array([0.76, -0.87], requires_grad=True) - res = cost_fn(x) - - a, b = x - - expected_res = x @ [np.cos(a) * np.cos(b), np.cos(a) * np.cos(b)] - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - grad_fn = qml.grad(cost_fn) - g = grad_fn(x) - - expected_g = [ - np.cos(b) * (np.cos(a) - (a + b) * np.sin(a)), - np.cos(a) * (np.cos(b) - (a + b) * np.sin(b)), - ] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - spy = mocker.spy(JacobianTape, "hessian") - hess = qml.jacobian(grad_fn)(x) - - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called_once() - - expected_hess = [ - [ - -(np.cos(b) * ((a + b) * np.cos(a) + 2 * np.sin(a))), - -(np.cos(b) * np.sin(a)) + (-np.cos(a) + (a + b) * np.sin(a)) * np.sin(b), - ], - [ - -(np.cos(b) * np.sin(a)) + (-np.cos(a) + (a + b) * np.sin(a)) * np.sin(b), - -(np.cos(a) * ((a + b) * np.cos(b) + 2 * np.sin(b))), - ], - ] - - assert np.allclose(hess, expected_hess, atol=tol, rtol=0) - - def test_hessian_vector_valued_separate_args(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a vector valued QNode that has separate input arguments""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=0) - return qml.probs(wires=0) - - a = np.array(1.0, requires_grad=True) - b = np.array(2.0, requires_grad=True) - res = circuit(a, b) - - expected_res = [0.5 + 0.5 * np.cos(a) * np.cos(b), 0.5 - 0.5 * np.cos(a) * np.cos(b)] - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - jac_fn = qml.jacobian(circuit) - g = jac_fn(a, b) - assert isinstance(g, tuple) and len(g) == 2 - - expected_g = ( - [-0.5 * np.sin(a) * np.cos(b), 0.5 * np.sin(a) * np.cos(b)], - [-0.5 * np.cos(a) * np.sin(b), 0.5 * np.cos(a) * np.sin(b)], - ) - assert np.allclose(g[0], expected_g[0], atol=tol, rtol=0) - assert np.allclose(g[1], expected_g[1], atol=tol, rtol=0) - - spy = mocker.spy(JacobianTape, "hessian") - jac_fn_a = lambda *args: jac_fn(*args)[0] - jac_fn_b = lambda *args: jac_fn(*args)[1] - hess_a = qml.jacobian(jac_fn_a)(a, b) - hess_b = qml.jacobian(jac_fn_b)(a, b) - assert isinstance(hess_a, tuple) and len(hess_a) == 2 - assert isinstance(hess_b, tuple) and len(hess_b) == 2 - - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - # Hessian will have been called 4 times, for every permutation of a, b - # since autograd will treat them as separate arguments. - assert spy.call_count == 4 - - exp_hess_a = ( - [-0.5 * np.cos(a) * np.cos(b), 0.5 * np.cos(a) * np.cos(b)], - [0.5 * np.sin(a) * np.sin(b), -0.5 * np.sin(a) * np.sin(b)], - ) - exp_hess_b = ( - [0.5 * np.sin(a) * np.sin(b), -0.5 * np.sin(a) * np.sin(b)], - [-0.5 * np.cos(a) * np.cos(b), 0.5 * np.cos(a) * np.cos(b)], - ) - for hess, exp_hess in zip([hess_a, hess_b], [exp_hess_a, exp_hess_b]): - assert np.allclose(hess[0], exp_hess[0], atol=tol, rtol=0) - assert np.allclose(hess[1], exp_hess[1], atol=tol, rtol=0) - - def test_hessian_ragged(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a ragged QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - qml.RY(x[0], wires=1) - qml.RX(x[1], wires=1) - return qml.expval(qml.PauliZ(0)), qml.probs(wires=1) - - x = np.array([1.0, 2.0], requires_grad=True) - res = circuit(x) - - a, b = x - - expected_res = [ - np.cos(a) * np.cos(b), - 0.5 + 0.5 * np.cos(a) * np.cos(b), - 0.5 - 0.5 * np.cos(a) * np.cos(b), - ] - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - jac_fn = qml.jacobian(circuit) - g = jac_fn(x) - - expected_g = [ - [-np.sin(a) * np.cos(b), -np.cos(a) * np.sin(b)], - [-0.5 * np.sin(a) * np.cos(b), -0.5 * np.cos(a) * np.sin(b)], - [0.5 * np.sin(a) * np.cos(b), 0.5 * np.cos(a) * np.sin(b)], - ] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - spy = mocker.spy(JacobianTape, "hessian") - hess = qml.jacobian(jac_fn)(x) - - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called_once() - - expected_hess = [ - [ - [-np.cos(a) * np.cos(b), np.sin(a) * np.sin(b)], - [np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)], - ], - [ - [-0.5 * np.cos(a) * np.cos(b), 0.5 * np.sin(a) * np.sin(b)], - [0.5 * np.sin(a) * np.sin(b), -0.5 * np.cos(a) * np.cos(b)], - ], - [ - [0.5 * np.cos(a) * np.cos(b), -0.5 * np.sin(a) * np.sin(b)], - [-0.5 * np.sin(a) * np.sin(b), 0.5 * np.cos(a) * np.cos(b)], - ], - ] - assert np.allclose(hess, expected_hess, atol=tol, rtol=0) - - def test_grad_matrix_caching(self, mocker, dev_name, diff_method): - """Test the gradient matrix caching""" - if diff_method not in {"parameter-shift"}: - pytest.skip("Test only supports parameter-shift") - - dev = qml.device(dev_name, wires=1) - - @qml.qnode_old.qnode(dev, diff_method="parameter-shift") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - return qml.probs(0) - - x = np.array([1.0, 2.0], requires_grad=True) - - jac_fn = qml.jacobian(circuit) - hessian_fn = qml.jacobian(qml.jacobian(circuit)) - - j_spy = mocker.spy(JacobianTape, "jacobian") - h_spy = mocker.spy(JacobianTape, "hessian") - - jac_fn(x) - assert j_spy.call_count == 1 - - # calling the same Jacobian function repeatedly, - # even with the same parameters, will re-evaluate - # the jacobian. - jac_fn(x) - assert j_spy.call_count == 2 - - # calling the hessian will result in new evaluations - hessian_fn(x) - assert j_spy.call_count == 3 - assert h_spy.call_count == 1 - - # New parameter values - jac_fn(2 * x) - assert j_spy.call_count == 4 - - # new jacobian function - jac_fn2 = qml.jacobian(circuit) - jac_fn(x) - assert j_spy.call_count == 5 - - -def test_adjoint_reuse_device_state(mocker): - """Tests that the autograd interface reuses the device state for adjoint differentiation""" - dev = qml.device("default.qubit", wires=1) - - @qml.qnode_old.qnode(dev, diff_method="adjoint") - def circ(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - spy = mocker.spy(dev, "adjoint_jacobian") - - grad = qml.grad(circ)(np.array(1.0, requires_grad=True)) - assert circ.device.num_executions == 1 - - spy.assert_called_with(mocker.ANY, use_device_state=True) - - assert circ.qtape.jacobian_options["device_pd_options"]["use_device_state"] == True - - -def qtransform(qnode, a, framework=np): - """Transforms every RY(y) gate in a circuit to RX(-a*cos(y))""" - - def construct(self, args, kwargs): - """New quantum tape construct method, that performs - the transform on the tape in a define-by-run manner""" - - # the following global variable is defined simply for testing - # purposes, so that we can easily extract the transformed operations - # for verification. - global t_op - - t_op = [] - - QNode.construct(self, args, kwargs) - - new_ops = [] - for o in self.qtape.operations: - # here, we loop through all tape operations, and make - # the transformation if a RY gate is encountered. - if isinstance(o, qml.RY): - t_op.append(qml.RX(-a * framework.cos(o.data[0]), wires=o.wires)) - new_ops.append(t_op[-1]) - else: - new_ops.append(o) - - self.qtape._ops = new_ops - self.qtape._update() - - import copy - - new_qnode = copy.deepcopy(qnode) - new_qnode.construct = construct.__get__(new_qnode, QNode) - return new_qnode - - -@pytest.mark.parametrize( - "dev_name,diff_method", - [("default.qubit", "finite-diff"), ("default.qubit.autograd", "backprop")], -) -def test_transform(dev_name, diff_method, tol): - """Test an example transform""" - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, interface="autograd", diff_method=diff_method) - def circuit(weights): - # the following global variables are defined simply for testing - # purposes, so that we can easily extract the operations for verification. - global op1, op2 - op1 = qml.RY(weights[0], wires=0) - op2 = qml.RX(weights[1], wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - weights = np.array([0.32, 0.543], requires_grad=True) - a = np.array(0.5, requires_grad=True) - - def loss(weights, a): - # the following global variable is defined simply for testing - # purposes, so that we can easily extract the transformed QNode - # for verification. - global new_circuit - - # transform the circuit QNode with trainable weight 'a' - new_circuit = qtransform(circuit, a) - - # evaluate the transformed QNode - res = new_circuit(weights) - - # evaluate the original QNode with pre-processed parameters - res2 = circuit(np.sin(weights)) - - # return the sum of the two QNode evaluations - return res + res2 - - res = loss(weights, a) - - # verify that the transformed QNode has the expected operations - assert circuit.qtape.operations == [op1, op2] - # RY(y) gate is transformed to RX(-a*cos(y)) - assert new_circuit.qtape.operations[0] == t_op[0] - # RX gate is is not transformed - assert new_circuit.qtape.operations[1].name == op2.name - assert new_circuit.qtape.operations[1].wires == op2.wires - - # check that the incident gate arguments of both QNode tapes are correct - assert np.all(np.array(circuit.qtape.get_parameters()) == np.sin(weights)) - assert np.all( - np.array(new_circuit.qtape.get_parameters()) == [-a * np.cos(weights[0]), weights[1]] - ) - - # verify that the gradient has the correct shape - grad = qml.grad(loss)(weights, a) - assert len(grad) == 2 - assert grad[0].shape == weights.shape - assert grad[1].shape == a.shape - - # compare against the expected values - assert np.allclose(res, 1.8244501889992706, atol=tol, rtol=0) - assert np.allclose(grad[0], [-0.26610258, -0.47053553], atol=tol, rtol=0) - assert np.allclose(grad[1], 0.06486032, atol=tol, rtol=0) diff --git a/tests/interfaces/test_qnode_jax.py b/tests/interfaces/test_qnode_jax.py deleted file mode 100644 index c4650cd5bb3..00000000000 --- a/tests/interfaces/test_qnode_jax.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the JAX interface""" -import pytest - -jax = pytest.importorskip("jax") -jnp = pytest.importorskip("jax.numpy") -import numpy as np -import pennylane as qml -from pennylane.qnode_old import qnode, QNode -from pennylane.tape import JacobianTape, QubitParamShiftTape - - -def test_qnode_intergration(): - """Test a simple use of qnode with a JAX interface and non-JAX device""" - dev = qml.device("default.mixed", wires=2) # A non-JAX device - - @qnode(dev, interface="jax") - def circuit(weights): - qml.RX(weights[0], wires=0) - qml.RZ(weights[1], wires=1) - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - weights = jnp.array([0.1, 0.2]) - val = circuit(weights) - assert "DeviceArray" in val.__repr__() - - -def test_to_jax(): - """Test the to_jax method""" - dev = qml.device("default.mixed", wires=2) - - @qnode(dev, interface="autograd") - def circuit(weights): - qml.RX(weights[0], wires=0) - qml.RZ(weights[1], wires=1) - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - circuit.to_jax() - weights = jnp.array([0.1, 0.2]) - val = circuit(weights) - assert "DeviceArray" in val.__repr__() - - -def test_simple_jacobian(tol): - """Test the use of jax.jaxrev""" - dev = qml.device("default.mixed", wires=2) # A non-JAX device. - - @qnode(dev, interface="jax", diff_method="parameter-shift") - def circuit(weights): - qml.RX(weights[0], wires=0) - qml.RY(weights[1], wires=1) - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - weights = jnp.array([0.1, 0.2]) - grads = jax.jacrev(circuit)(weights) - # This is the easiest way to ensure our object is a DeviceArray instead - # of a numpy array. - assert "DeviceArray" in grads.__repr__() - assert grads.shape == (2,) - np.testing.assert_allclose(grads, np.array([-0.09784342, -0.19767685]), atol=tol, rtol=0) - - -def test_simple_grad(): - """Test the use of jax.grad""" - dev = qml.device("default.mixed", wires=2) # A non-JAX device. - - @qnode(dev, interface="jax", diff_method="parameter-shift") - def circuit(weights): - qml.RX(weights[0], wires=0) - qml.RZ(weights[1], wires=1) - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - weights = jnp.array([0.1, 0.2]) - val = jax.grad(circuit)(weights) - assert "DeviceArray" in val.__repr__() - - -@pytest.mark.parametrize("diff_method", ["parameter-shift", "finite-diff"]) -def test_differentiable_expand(diff_method): - """Test that operation and nested tapes expansion - is differentiable""" - - class U3(qml.U3): - def expand(self): - theta, phi, lam = self.data - wires = self.wires - - with JacobianTape() as tape: - qml.Rot(lam, theta, -lam, wires=wires) - qml.PhaseShift(phi + lam, wires=wires) - - return tape - - dev = qml.device("default.mixed", wires=1) - a = jnp.array(0.1) - p = jnp.array([0.1, 0.2, 0.3]) - - @qnode(dev, diff_method=diff_method, interface="jax") - def circuit(a, p): - qml.RX(a, wires=0) - U3(p[0], p[1], p[2], wires=0) - return qml.expval(qml.PauliX(0)) - - res = circuit(a, p) - - expected = np.cos(a) * np.cos(p[1]) * np.sin(p[0]) + np.sin(a) * ( - np.cos(p[2]) * np.sin(p[1]) + np.cos(p[0]) * np.cos(p[1]) * np.sin(p[2]) - ) - tol = 1e-5 - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = jax.grad(circuit, argnums=1)(a, p) - expected = np.array( - [ - np.cos(p[1]) * (np.cos(a) * np.cos(p[0]) - np.sin(a) * np.sin(p[0]) * np.sin(p[2])), - np.cos(p[1]) * np.cos(p[2]) * np.sin(a) - - np.sin(p[1]) * (np.cos(a) * np.sin(p[0]) + np.cos(p[0]) * np.sin(a) * np.sin(p[2])), - np.sin(a) * (np.cos(p[0]) * np.cos(p[1]) * np.cos(p[2]) - np.sin(p[1]) * np.sin(p[2])), - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - -def test_adjoint_reuse_device_state(mocker): - """Tests that the jax interface reuses the device state for adjoint differentiation""" - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, interface="jax", diff_method="adjoint") - def circ(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - spy = mocker.spy(dev, "adjoint_jacobian") - - grad = jax.grad(circ)(1.0) - assert circ.device.num_executions == 1 - - spy.assert_called_with(mocker.ANY, use_device_state=True) - - assert circ.qtape.jacobian_options["device_pd_options"]["use_device_state"] == True - - -def qtransform(qnode, a, framework=jnp): - """Transforms every RY(y) gate in a circuit to RX(-a*cos(y))""" - - def construct(self, args, kwargs): - """New quantum tape construct method, that performs - the transform on the tape in a define-by-run manner""" - - t_op = [] - - QNode.construct(self, args, kwargs) - - new_ops = [] - for o in self.qtape.operations: - # here, we loop through all tape operations, and make - # the transformation if a RY gate is encountered. - if isinstance(o, qml.RY): - t_op.append(qml.RX(-a * framework.cos(o.data[0]), wires=o.wires)) - new_ops.append(t_op[-1]) - else: - new_ops.append(o) - - self.qtape._ops = new_ops - self.qtape._update() - - import copy - - new_qnode = copy.deepcopy(qnode) - new_qnode.construct = construct.__get__(new_qnode, QNode) - return new_qnode - - -@pytest.mark.parametrize( - "dev_name,diff_method", - [("default.mixed", "finite-diff"), ("default.qubit.autograd", "parameter-shift")], -) -def test_transform(dev_name, diff_method, tol): - """Test an example transform""" - dev = qml.device(dev_name, wires=1) - - @qnode(dev, interface="jax", diff_method=diff_method) - def circuit(weights): - op1 = qml.RY(weights[0], wires=0) - op2 = qml.RX(weights[1], wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - weights = np.array([0.32, 0.543]) - a = np.array(0.5) - - def loss(weights, a): - # transform the circuit QNode with trainable weight 'a' - new_circuit = qtransform(circuit, a) - - # evaluate the transformed QNode - res = new_circuit(weights) - - # evaluate the original QNode with pre-processed parameters - res2 = circuit(jnp.sin(weights)) - - # return the sum of the two QNode evaluations - return res + res2 - - res = loss(weights, a) - - grad = jax.grad(loss, argnums=[0, 1])(weights, a) - assert len(grad) == 2 - assert grad[0].shape == weights.shape - assert grad[1].shape == a.shape - - # compare against the expected values - tol = 1e-5 - assert np.allclose(res, 1.8244501889992706, atol=tol, rtol=0) - assert np.allclose(grad[0], [-0.26610258, -0.47053553], atol=tol, rtol=0) - assert np.allclose(grad[1], 0.06486032, atol=tol, rtol=0) diff --git a/tests/interfaces/test_qnode_tf.py b/tests/interfaces/test_qnode_tf.py deleted file mode 100644 index 07da0a89a62..00000000000 --- a/tests/interfaces/test_qnode_tf.py +++ /dev/null @@ -1,1015 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the tf interface""" -import pytest - -tf = pytest.importorskip("tensorflow", minversion="2.1") - -import numpy as np - -import pennylane as qml -from pennylane.qnode_old import qnode, QNode -from pennylane.tape import JacobianTape - - -@pytest.mark.parametrize( - "dev_name,diff_method", - [ - ["default.qubit", "finite-diff"], - ["default.qubit", "parameter-shift"], - ["default.qubit", "backprop"], - ["default.qubit", "adjoint"], - ], -) -class TestQNode: - """Tests the tensorflow interface used with a QNode.""" - - def test_import_error(self, dev_name, diff_method, mocker): - """Test that an exception is caught on import error""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - mock = mocker.patch("pennylane.interfaces.tf.TFInterface.apply") - mock.side_effect = ImportError() - - def func(x, y): - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device(dev_name, wires=2) - qn = QNode(func, dev, interface="tf", diff_method=diff_method) - - with pytest.raises( - qml.QuantumFunctionError, - match="TensorFlow not found. Please install the latest version of TensorFlow to enable the 'tf' interface", - ): - qn(0.1, 0.1) - - def test_execution_no_interface(self, dev_name, diff_method): - """Test execution works without an interface, and that trainable parameters - are correctly inferred within a gradient tape.""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method) - def circuit(a): - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - return qml.expval(qml.PauliZ(0)) - - a = tf.Variable(0.1) - - with tf.GradientTape() as tape: - res = circuit(a) - - assert circuit.qtape.interface == "autograd" - - # without the interface, the tape simply returns an array of results - assert isinstance(res, np.ndarray) - assert res.shape == tuple() - - # without the interface, the tape is unable to deduce - # trainable parameters - assert circuit.qtape.trainable_params == [0] - - # gradients should cause an error - with pytest.raises(AttributeError, match="has no attribute '_id'"): - assert tape.gradient(res, a) is None - - def test_execution_with_interface(self, dev_name, diff_method): - """Test execution works with the interface""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, interface="tf", diff_method=diff_method) - def circuit(a): - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - return qml.expval(qml.PauliZ(0)) - - a = tf.Variable(0.1) - circuit(a) - - # if executing outside a gradient tape, the number of trainable parameters - # cannot be determined by TensorFlow - assert circuit.qtape.trainable_params == [] - - with tf.GradientTape() as tape: - res = circuit(a) - - assert circuit.qtape.interface == "tf" - - # with the interface, the tape returns tensorflow tensors - assert isinstance(res, tf.Tensor) - assert res.shape == tuple() - - # the tape is able to deduce trainable parameters - assert circuit.qtape.trainable_params == [0] - - # gradients should work - grad = tape.gradient(res, a) - assert isinstance(grad, tf.Tensor) - assert grad.shape == tuple() - - def test_interface_swap(self, dev_name, diff_method, tol): - """Test that the TF interface can be applied to a QNode - with a pre-existing interface""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, interface="autograd", diff_method=diff_method) - def circuit(a): - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - return qml.expval(qml.PauliZ(0)) - - from pennylane import numpy as anp - - a = anp.array(0.1, requires_grad=True) - - res1 = circuit(a) - grad_fn = qml.grad(circuit) - grad1 = grad_fn(a) - - # switch to TF interface - circuit.to_tf() - - a = tf.Variable(0.1, dtype=tf.float64) - - with tf.GradientTape() as tape: - res2 = circuit(a) - - grad2 = tape.gradient(res2, a) - assert np.allclose(res1, res2, atol=tol, rtol=0) - assert np.allclose(grad1, grad2, atol=tol, rtol=0) - - def test_drawing(self, dev_name, diff_method): - """Test circuit drawing when using the TF interface""" - - x = tf.Variable(0.1, dtype=tf.float64) - y = tf.Variable([0.2, 0.3], dtype=tf.float64) - z = tf.Variable(0.4, dtype=tf.float64) - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, interface="tf", diff_method=diff_method) - def circuit(p1, p2=y, **kwargs): - qml.RX(p1, wires=0) - qml.RY(p2[0] * p2[1], wires=1) - qml.RX(kwargs["p3"], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.state() - - circuit(p1=x, p3=z) - - result = circuit.draw() - expected = """\ - 0: ──RX(0.1)───RX(0.4)──╭C──╭┤ State - 1: ──RY(0.06)───────────╰X──╰┤ State -""" - - assert result == expected - - def test_jacobian(self, dev_name, diff_method, mocker, tol): - """Test jacobian calculation""" - spy = mocker.spy(JacobianTape, "jacobian") - a = tf.Variable(0.1, dtype=tf.float64) - b = tf.Variable(0.2, dtype=tf.float64) - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1))] - - with tf.GradientTape() as tape: - res = circuit(a, b) - - assert circuit.qtape.trainable_params == [0, 1] - - assert isinstance(res, tf.Tensor) - assert res.shape == (2,) - - expected = [tf.cos(a), -tf.cos(a) * tf.sin(b)] - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, [a, b]) - expected = [[-tf.sin(a), tf.sin(a) * tf.sin(b)], [0, -tf.cos(a) * tf.cos(b)]] - assert np.allclose(res, expected, atol=tol, rtol=0) - - if diff_method == "finite-diff": - spy.assert_called() - elif diff_method == "backprop": - spy.assert_not_called() - - def test_jacobian_dtype(self, dev_name, diff_method, tol): - """Test calculating the jacobian with a different datatype""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - a = tf.Variable(0.1, dtype=tf.float32) - b = tf.Variable(0.2, dtype=tf.float32) - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, diff_method=diff_method) - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1))] - - circuit.to_tf(dtype=tf.float32) - assert circuit.dtype is tf.float32 - - with tf.GradientTape() as tape: - res = circuit(a, b) - - assert circuit.qtape.interface == "tf" - assert circuit.qtape.trainable_params == [0, 1] - - assert isinstance(res, tf.Tensor) - assert res.shape == (2,) - assert res.dtype is tf.float32 - - res = tape.jacobian(res, [a, b]) - assert [r.dtype is tf.float32 for r in res] - - def test_jacobian_options(self, dev_name, diff_method, mocker, tol): - """Test setting finite-difference jacobian options""" - if diff_method != "finite-diff": - pytest.skip("Test only works with finite diff") - - spy = mocker.spy(JacobianTape, "numeric_pd") - - a = tf.Variable([0.1, 0.2]) - - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, interface="tf", h=1e-8, order=2, diff_method=diff_method) - def circuit(a): - qml.RY(a[0], wires=0) - qml.RX(a[1], wires=0) - return qml.expval(qml.PauliZ(0)) - - with tf.GradientTape() as tape: - res = circuit(a) - - tape.jacobian(res, a) - - for args in spy.call_args_list: - assert args[1]["order"] == 2 - assert args[1]["h"] == 1e-8 - - def test_changing_trainability(self, dev_name, diff_method, mocker, tol): - """Test changing the trainability of parameters changes the - number of differentiation requests made""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - a = tf.Variable(0.1, dtype=tf.float64) - b = tf.Variable(0.2, dtype=tf.float64) - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, interface="tf", diff_method="finite-diff") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1)) - - with tf.GradientTape() as tape: - res = circuit(a, b) - - # the tape has reported both gate arguments as trainable - assert circuit.qtape.trainable_params == [0, 1] - - expected = [tf.cos(a), -tf.cos(a) * tf.sin(b)] - assert np.allclose(res, expected, atol=tol, rtol=0) - - spy = mocker.spy(JacobianTape, "numeric_pd") - - jac = tape.jacobian(res, [a, b]) - expected = [ - [-tf.sin(a), tf.sin(a) * tf.sin(b)], - [0, -tf.cos(a) * tf.cos(b)], - ] - assert np.allclose(jac, expected, atol=tol, rtol=0) - - # JacobianTape.numeric_pd has been called for each argument - assert len(spy.call_args_list) == 2 - - # make the second QNode argument a constant - a = tf.Variable(0.54, dtype=tf.float64) - b = tf.constant(0.8, dtype=tf.float64) - - with tf.GradientTape() as tape: - res = circuit(a, b) - - # the tape has reported only the first argument as trainable - assert circuit.qtape.trainable_params == [0] - - expected = [tf.cos(a), -tf.cos(a) * tf.sin(b)] - assert np.allclose(res, expected, atol=tol, rtol=0) - - spy.call_args_list = [] - jac = tape.jacobian(res, a) - expected = [-tf.sin(a), tf.sin(a) * tf.sin(b)] - assert np.allclose(jac, expected, atol=tol, rtol=0) - - # JacobianTape.numeric_pd has been called only once - assert len(spy.call_args_list) == 1 - - def test_classical_processing(self, dev_name, diff_method, tol): - """Test classical processing within the quantum tape""" - a = tf.Variable(0.1, dtype=tf.float64) - b = tf.constant(0.2, dtype=tf.float64) - c = tf.Variable(0.3, dtype=tf.float64) - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(x, y, z): - qml.RY(x * z, wires=0) - qml.RZ(y, wires=0) - qml.RX(z + z**2 + tf.sin(a), wires=0) - return qml.expval(qml.PauliZ(0)) - - with tf.GradientTape() as tape: - res = circuit(a, b, c) - - if diff_method == "finite-diff": - assert circuit.qtape.trainable_params == [0, 2] - assert circuit.qtape.get_parameters() == [a * c, c + c**2 + tf.sin(a)] - - res = tape.jacobian(res, [a, b, c]) - - assert isinstance(res[0], tf.Tensor) - assert res[1] is None - assert isinstance(res[2], tf.Tensor) - - def test_no_trainable_parameters(self, dev_name, diff_method, tol): - """Test evaluation if there are no trainable parameters""" - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - a = 0.1 - b = tf.constant(0.2, dtype=tf.float64) - - with tf.GradientTape() as tape: - res = circuit(a, b) - - if diff_method == "finite-diff": - assert circuit.qtape.trainable_params == [] - - assert res.shape == (2,) - assert isinstance(res, tf.Tensor) - - @pytest.mark.parametrize("U", [tf.constant([[0, 1], [1, 0]]), np.array([[0, 1], [1, 0]])]) - def test_matrix_parameter(self, dev_name, diff_method, U, tol): - """Test that the TF interface works correctly - with a matrix parameter""" - a = tf.Variable(0.1, dtype=tf.float64) - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(U, a): - qml.QubitUnitary(U, wires=0) - qml.RY(a, wires=0) - return qml.expval(qml.PauliZ(0)) - - with tf.GradientTape() as tape: - res = circuit(U, a) - - if diff_method == "finite-diff": - assert circuit.qtape.trainable_params == [1] - - assert np.allclose(res, -tf.cos(a), atol=tol, rtol=0) - - res = tape.jacobian(res, a) - assert np.allclose(res, tf.sin(a), atol=tol, rtol=0) - - def test_differentiable_expand(self, dev_name, diff_method, tol): - """Test that operation and nested tapes expansion - is differentiable""" - - class U3(qml.U3): - def expand(self): - theta, phi, lam = self.data - wires = self.wires - - with JacobianTape() as tape: - qml.Rot(lam, theta, -lam, wires=wires) - qml.PhaseShift(phi + lam, wires=wires) - - return tape - - dev = qml.device(dev_name, wires=1) - a = np.array(0.1) - p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float64) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(a, p): - qml.RX(a, wires=0) - U3(p[0], p[1], p[2], wires=0) - return qml.expval(qml.PauliX(0)) - - with tf.GradientTape() as tape: - res = circuit(a, p) - - assert circuit.qtape.trainable_params == [1, 2, 3, 4] - assert [i.name for i in circuit.qtape.operations] == ["RX", "Rot", "PhaseShift"] - assert np.all(circuit.qtape.get_parameters() == [p[2], p[0], -p[2], p[1] + p[2]]) - - expected = tf.cos(a) * tf.cos(p[1]) * tf.sin(p[0]) + tf.sin(a) * ( - tf.cos(p[2]) * tf.sin(p[1]) + tf.cos(p[0]) * tf.cos(p[1]) * tf.sin(p[2]) - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, p) - expected = np.array( - [ - tf.cos(p[1]) * (tf.cos(a) * tf.cos(p[0]) - tf.sin(a) * tf.sin(p[0]) * tf.sin(p[2])), - tf.cos(p[1]) * tf.cos(p[2]) * tf.sin(a) - - tf.sin(p[1]) - * (tf.cos(a) * tf.sin(p[0]) + tf.cos(p[0]) * tf.sin(a) * tf.sin(p[2])), - tf.sin(a) - * (tf.cos(p[0]) * tf.cos(p[1]) * tf.cos(p[2]) - tf.sin(p[1]) * tf.sin(p[2])), - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_probability_differentiation(self, dev_name, diff_method, tol): - """Tests correct output shape and evaluation for a tape - with multiple probs outputs""" - - if diff_method == "adjoint": - pytest.skip("The adjoint method does not currently support returning probabilities") - - dev = qml.device(dev_name, wires=2) - x = tf.Variable(0.543, dtype=tf.float64) - y = tf.Variable(-0.654, dtype=tf.float64) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.probs(wires=[0]), qml.probs(wires=[1]) - - with tf.GradientTape() as tape: - res = circuit(x, y) - - expected = np.array( - [ - [tf.cos(x / 2) ** 2, tf.sin(x / 2) ** 2], - [(1 + tf.cos(x) * tf.cos(y)) / 2, (1 - tf.cos(x) * tf.cos(y)) / 2], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, [x, y]) - expected = np.array( - [ - [ - [-tf.sin(x) / 2, tf.sin(x) / 2], - [-tf.sin(x) * tf.cos(y) / 2, tf.cos(y) * tf.sin(x) / 2], - ], - [ - [0, 0], - [-tf.cos(x) * tf.sin(y) / 2, tf.cos(x) * tf.sin(y) / 2], - ], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_ragged_differentiation(self, dev_name, diff_method, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - if diff_method == "adjoint": - pytest.skip("The adjoint method does not currently support returning probabilities") - - dev = qml.device(dev_name, wires=2) - x = tf.Variable(0.543, dtype=tf.float64) - y = tf.Variable(-0.654, dtype=tf.float64) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return [qml.expval(qml.PauliZ(0)), qml.probs(wires=[1])] - - with tf.GradientTape() as tape: - res = circuit(x, y) - - expected = np.array( - [ - tf.cos(x), - (1 + tf.cos(x) * tf.cos(y)) / 2, - (1 - tf.cos(x) * tf.cos(y)) / 2, - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, [x, y]) - expected = np.array( - [ - [-tf.sin(x), -tf.sin(x) * tf.cos(y) / 2, tf.cos(y) * tf.sin(x) / 2], - [0, -tf.cos(x) * tf.sin(y) / 2, tf.cos(x) * tf.sin(y) / 2], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_sampling(self, dev_name, diff_method): - """Test sampling works as expected""" - if diff_method == "backprop": - pytest.skip("Sampling not possible with backprop differentiation.") - - dev = qml.device(dev_name, wires=2, shots=10) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(): - qml.Hadamard(wires=[0]) - qml.CNOT(wires=[0, 1]) - return [qml.sample(qml.PauliZ(0)), qml.sample(qml.PauliX(1))] - - with tf.GradientTape() as tape: - res = circuit() - - assert res.shape == (2, 10) - assert isinstance(res, tf.Tensor) - - def test_second_derivative(self, dev_name, diff_method, mocker, tol): - """Test second derivative calculation of a scalar valued QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - return qml.expval(qml.PauliZ(0)) - - x = tf.Variable([1.0, 2.0], dtype=tf.float64) - - with tf.GradientTape() as tape1: - with tf.GradientTape() as tape2: - res = circuit(x) - g = tape2.gradient(res, x) - res2 = tf.reduce_sum(g) - - spy = mocker.spy(JacobianTape, "hessian") - g2 = tape1.gradient(res2, x) - - if diff_method == "parameter-shift": - spy.assert_called_once() - elif diff_method == "backprop": - spy.assert_not_called() - - a, b = x * 1.0 - - expected_res = tf.cos(a) * tf.cos(b) - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - expected_g = [-tf.sin(a) * tf.cos(b), -tf.cos(a) * tf.sin(b)] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - expected_g2 = [ - -tf.cos(a) * tf.cos(b) + tf.sin(a) * tf.sin(b), - tf.sin(a) * tf.sin(b) - tf.cos(a) * tf.cos(b), - ] - assert np.allclose(g2, expected_g2, atol=tol, rtol=0) - - def test_hessian(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a scalar valued QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - return qml.expval(qml.PauliZ(0)) - - x = tf.Variable([1.0, 2.0], dtype=tf.float64) - - with tf.GradientTape() as tape1: - with tf.GradientTape() as tape2: - res = circuit(x) - g = tape2.gradient(res, x) - - spy = mocker.spy(JacobianTape, "hessian") - hess = tape1.jacobian(g, x) - - if diff_method == "parameter-shift": - spy.assert_called_once() - elif diff_method == "backprop": - spy.assert_not_called() - - a, b = x * 1.0 - - expected_res = tf.cos(a) * tf.cos(b) - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - expected_g = [-tf.sin(a) * tf.cos(b), -tf.cos(a) * tf.sin(b)] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - expected_hess = [ - [-tf.cos(a) * tf.cos(b), tf.sin(a) * tf.sin(b)], - [tf.sin(a) * tf.sin(b), -tf.cos(a) * tf.cos(b)], - ] - assert np.allclose(hess, expected_hess, atol=tol, rtol=0) - - def test_hessian_vector_valued(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a vector valued QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - return qml.probs(wires=0) - - x = tf.Variable([1.0, 2.0], dtype=tf.float64) - - with tf.GradientTape(persistent=True) as tape1: - with tf.GradientTape(persistent=True) as tape2: - res = circuit(x) - - spy = mocker.spy(JacobianTape, "hessian") - g = tape2.jacobian(res, x, experimental_use_pfor=False) - - hess = tape1.jacobian(g, x, experimental_use_pfor=False) - - if diff_method == "parameter-shift": - spy.assert_called_once() - elif diff_method == "backprop": - spy.assert_not_called() - - a, b = x * 1.0 - - expected_res = [ - 0.5 + 0.5 * tf.cos(a) * tf.cos(b), - 0.5 - 0.5 * tf.cos(a) * tf.cos(b), - ] - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - expected_g = [ - [-0.5 * tf.sin(a) * tf.cos(b), -0.5 * tf.cos(a) * tf.sin(b)], - [0.5 * tf.sin(a) * tf.cos(b), 0.5 * tf.cos(a) * tf.sin(b)], - ] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - expected_hess = [ - [ - [-0.5 * tf.cos(a) * tf.cos(b), 0.5 * tf.sin(a) * tf.sin(b)], - [0.5 * tf.sin(a) * tf.sin(b), -0.5 * tf.cos(a) * tf.cos(b)], - ], - [ - [0.5 * tf.cos(a) * tf.cos(b), -0.5 * tf.sin(a) * tf.sin(b)], - [-0.5 * tf.sin(a) * tf.sin(b), 0.5 * tf.cos(a) * tf.cos(b)], - ], - ] - - np.testing.assert_allclose(hess, expected_hess, atol=tol, rtol=0, verbose=True) - - def test_hessian_vector_valued_postprocessing(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a vector valued QNode with post-processing""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=0) - return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(0))] - - x = tf.Variable([0.76, -0.87], dtype=tf.float64) - - with tf.GradientTape(persistent=True) as tape1: - with tf.GradientTape(persistent=True) as tape2: - res = tf.tensordot(x, circuit(x), axes=[0, 0]) - - spy = mocker.spy(JacobianTape, "hessian") - g = tape2.jacobian(res, x, experimental_use_pfor=False) - - hess = tape1.jacobian(g, x, experimental_use_pfor=False) - - if diff_method == "parameter-shift": - spy.assert_called_once() - elif diff_method == "backprop": - spy.assert_not_called() - - a, b = x * 1.0 - - expected_res = a * tf.cos(a) * tf.cos(b) + b * tf.cos(a) * tf.cos(b) - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - expected_g = [ - tf.cos(b) * (tf.cos(a) - (a + b) * tf.sin(a)), - tf.cos(a) * (tf.cos(b) - (a + b) * tf.sin(b)), - ] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - expected_hess = [ - [ - -(tf.cos(b) * ((a + b) * tf.cos(a) + 2 * tf.sin(a))), - -(tf.cos(b) * tf.sin(a)) + (-tf.cos(a) + (a + b) * tf.sin(a)) * tf.sin(b), - ], - [ - -(tf.cos(b) * tf.sin(a)) + (-tf.cos(a) + (a + b) * tf.sin(a)) * tf.sin(b), - -(tf.cos(a) * ((a + b) * tf.cos(b) + 2 * tf.sin(b))), - ], - ] - assert np.allclose(hess, expected_hess, atol=tol, rtol=0) - - def test_hessian_ragged(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a ragged QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="tf") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - qml.RY(x[0], wires=1) - qml.RX(x[1], wires=1) - return qml.expval(qml.PauliZ(0)), qml.probs(wires=1) - - x = tf.Variable([1.0, 2.0], dtype=tf.float64) - res = circuit(x) - - with tf.GradientTape(persistent=True) as tape1: - with tf.GradientTape(persistent=True) as tape2: - res = circuit(x) - - spy = mocker.spy(JacobianTape, "hessian") - g = tape2.jacobian(res, x, experimental_use_pfor=False) - - hess = tape1.jacobian(g, x, experimental_use_pfor=False) - - if diff_method == "parameter-shift": - spy.assert_called_once() - elif diff_method == "backprop": - spy.assert_not_called() - - a, b = x * 1.0 - - expected_res = [ - tf.cos(a) * tf.cos(b), - 0.5 + 0.5 * tf.cos(a) * tf.cos(b), - 0.5 - 0.5 * tf.cos(a) * tf.cos(b), - ] - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - expected_g = [ - [-tf.sin(a) * tf.cos(b), -tf.cos(a) * tf.sin(b)], - [-0.5 * tf.sin(a) * tf.cos(b), -0.5 * tf.cos(a) * tf.sin(b)], - [0.5 * tf.sin(a) * tf.cos(b), 0.5 * tf.cos(a) * tf.sin(b)], - ] - assert np.allclose(g, expected_g, atol=tol, rtol=0) - - expected_hess = [ - [ - [-tf.cos(a) * tf.cos(b), tf.sin(a) * tf.sin(b)], - [tf.sin(a) * tf.sin(b), -tf.cos(a) * tf.cos(b)], - ], - [ - [-0.5 * tf.cos(a) * tf.cos(b), 0.5 * tf.sin(a) * tf.sin(b)], - [0.5 * tf.sin(a) * tf.sin(b), -0.5 * tf.cos(a) * tf.cos(b)], - ], - [ - [0.5 * tf.cos(a) * tf.cos(b), -0.5 * tf.sin(a) * tf.sin(b)], - [-0.5 * tf.sin(a) * tf.sin(b), 0.5 * tf.cos(a) * tf.cos(b)], - ], - ] - np.testing.assert_allclose(hess, expected_hess, atol=tol, rtol=0, verbose=True) - - -class Test_adjoint: - def test_adjoint_default_save_state(self, mocker): - """Tests that the state will be saved by default""" - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, diff_method="adjoint", interface="tf") - def circ(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=1) - qml.CNOT(wires=(0, 1)) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1)) - - expected_grad = lambda x: np.array([-np.sin(x[0]), np.cos(x[1])]) - - spy = mocker.spy(dev, "adjoint_jacobian") - - x1 = tf.Variable([0.1, 0.2]) - x2 = tf.Variable([0.3, 0.4]) - - with tf.GradientTape() as tape1: - res1 = circ(x1) - - with tf.GradientTape() as tape2: - res2 = circ(x2) - - grad1 = tape1.gradient(res1, x1) - grad2 = tape2.gradient(res2, x2) - - assert np.allclose(grad1, expected_grad(x1)) - assert np.allclose(grad2, expected_grad(x2)) - - assert circ.device.num_executions == 2 - spy.assert_called_with(mocker.ANY, starting_state=mocker.ANY) - - def test_adjoint_save_state(self, mocker): - """Tests that the tf interface reuses device state when prompted by `cache_state=True`. - Also makes sure executing a second circuit before backward pass does not interfere - with answer. - """ - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, diff_method="adjoint", interface="tf", adjoint_cache=True) - def circ(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=1) - qml.CNOT(wires=(0, 1)) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1)) - - expected_grad = lambda x: np.array([-np.sin(x[0]), np.cos(x[1])]) - - spy = mocker.spy(dev, "adjoint_jacobian") - - x1 = tf.Variable([0.1, 0.2]) - x2 = tf.Variable([0.3, 0.4]) - - with tf.GradientTape() as tape1: - res1 = circ(x1) - - with tf.GradientTape() as tape2: - res2 = circ(x2) - - grad1 = tape1.gradient(res1, x1) - grad2 = tape2.gradient(res2, x2) - - assert np.allclose(grad1, expected_grad(x1)) - assert np.allclose(grad2, expected_grad(x2)) - - assert circ.device.num_executions == 2 - spy.assert_called_with(mocker.ANY, starting_state=mocker.ANY) - - assert circ.qtape.jacobian_options["adjoint_cache"] == True - - def test_adjoint_no_save_state(self, mocker): - """Tests that with `adjoint_cache=False`, the state is not cached""" - - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, diff_method="adjoint", interface="tf", adjoint_cache=False) - def circ(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - spy = mocker.spy(dev, "adjoint_jacobian") - - x = tf.Variable(0.1) - with tf.GradientTape() as tape: - res = circ(x) - - grad = tape.gradient(res, x) - - assert circ.device.num_executions == 2 - spy.assert_called_with(mocker.ANY) - - assert circ.qtape.jacobian_options.get("adjoint_cache", False) == False - - -def qtransform(qnode, a, framework=tf): - """Transforms every RY(y) gate in a circuit to RX(-a*cos(y))""" - - def construct(self, args, kwargs): - """New quantum tape construct method, that performs - the transform on the tape in a define-by-run manner""" - - # the following global variable is defined simply for testing - # purposes, so that we can easily extract the transformed operations - # for verification. - global t_op - - t_op = [] - - QNode.construct(self, args, kwargs) - - new_ops = [] - for o in self.qtape.operations: - # here, we loop through all tape operations, and make - # the transformation if a RY gate is encountered. - if isinstance(o, qml.RY): - t_op.append(qml.RX(-a * framework.cos(o.data[0]), wires=o.wires)) - new_ops.append(t_op[-1]) - else: - new_ops.append(o) - - self.qtape._ops = new_ops - self.qtape._update() - - import copy - - new_qnode = copy.deepcopy(qnode) - new_qnode.construct = construct.__get__(new_qnode, QNode) - return new_qnode - - -@pytest.mark.parametrize( - "dev_name,diff_method", - [("default.qubit", "finite-diff"), ("default.qubit.tf", "backprop")], -) -def test_transform(dev_name, diff_method, tol): - """Test an example transform""" - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, interface="tf", diff_method=diff_method) - def circuit(weights): - # the following global variables are defined simply for testing - # purposes, so that we can easily extract the operations for verification. - global op1, op2 - op1 = qml.RY(weights[0], wires=0) - op2 = qml.RX(weights[1], wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - weights = tf.Variable([0.32, 0.543], dtype=tf.float64) - a = tf.Variable(0.5, dtype=tf.float64) - - with tf.GradientTape(persistent=True) as tape: - # transform the circuit QNode with trainable weight 'a' - new_qnode = qtransform(circuit, a) - # evaluate the transformed QNode - res = new_qnode(weights) - # evaluate the original QNode with pre-processed parameters - res2 = circuit(tf.sin(weights)) - # the loss is the sum of the two QNode evaluations - loss = res + res2 - - # verify that the transformed QNode has the expected operations - assert circuit.qtape.operations == [op1, op2] - assert new_qnode.qtape.operations[0] == t_op[0] - assert new_qnode.qtape.operations[1].name == op2.name - assert new_qnode.qtape.operations[1].wires == op2.wires - - # check that the incident gate arguments of both QNode tapes are correct - assert np.all(circuit.qtape.get_parameters() == tf.sin(weights)) - assert np.all(new_qnode.qtape.get_parameters() == [-a * tf.cos(weights[0]), weights[1]]) - - # verify that the gradient has the correct shape - grad = tape.gradient(loss, [weights, a]) - assert len(grad) == 2 - assert grad[0].shape == weights.shape - assert grad[1].shape == a.shape - - # compare against the expected values - assert np.allclose(loss, 1.8244501889992706, atol=tol, rtol=0) - assert np.allclose(grad[0], [-0.26610258, -0.47053553], atol=tol, rtol=0) - assert np.allclose(grad[1], 0.06486032, atol=tol, rtol=0) diff --git a/tests/interfaces/test_qnode_torch.py b/tests/interfaces/test_qnode_torch.py deleted file mode 100644 index c58153fa292..00000000000 --- a/tests/interfaces/test_qnode_torch.py +++ /dev/null @@ -1,1013 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the torch interface""" -import pytest - -torch = pytest.importorskip("torch", minversion="1.3") - -import numpy as np - -import pennylane as qml -from pennylane.qnode_old import qnode, QNode -from pennylane.tape import JacobianTape -from torch.autograd.functional import hessian, jacobian - - -@pytest.mark.parametrize( - "dev_name,diff_method", - [ - ["default.qubit", "finite-diff"], - ["default.qubit", "parameter-shift"], - ["default.qubit", "adjoint"], - ["default.qubit", "backprop"], - ], -) -class TestQNode: - """Same tests as above, but this time via the QNode interface!""" - - def test_import_error(self, dev_name, diff_method, mocker): - """Test that an exception is caught on import error""" - if diff_method == "backprop": - pytest.skip("Test only works in parameter-shift mode") - - mock = mocker.patch("pennylane.interfaces.torch.TorchInterface.apply") - mock.side_effect = ImportError() - - def func(x, y): - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device(dev_name, wires=2) - qn = QNode(func, dev, interface="torch", diff_method=diff_method) - - with pytest.raises( - qml.QuantumFunctionError, - match="PyTorch not found. Please install the latest version of PyTorch to enable the 'torch' interface", - ): - qn(0.1, 0.1) - - def test_execution_no_interface(self, dev_name, diff_method): - """Test execution works without an interface""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method) - def circuit(a): - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - return qml.expval(qml.PauliZ(0)) - - a = torch.tensor(0.1, requires_grad=True) - - res = circuit(a) - - assert circuit.qtape.interface == "autograd" - - # without the interface, the tape simply returns an array of results - assert isinstance(res, np.ndarray) - assert res.shape == tuple() - - # without the interface, the tape is unable to deduce - # trainable parameters - assert circuit.qtape.trainable_params == [0] - - def test_execution_with_interface(self, dev_name, diff_method): - """Test execution works with the interface""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(a): - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - return qml.expval(qml.PauliZ(0)) - - a = torch.tensor(0.1, requires_grad=True) - res = circuit(a) - - assert circuit.qtape.interface == "torch" - - # with the interface, the tape returns torch tensors - - assert isinstance(res, torch.Tensor) - assert res.shape == tuple() - - # the tape is able to deduce trainable parameters - assert circuit.qtape.trainable_params == [0] - - # gradients should work - res.backward() - grad = a.grad - assert isinstance(grad, torch.Tensor) - assert grad.shape == tuple() - - def test_interface_swap(self, dev_name, diff_method, tol): - """Test that the Torch interface can be applied to a QNode - with a pre-existing interface""" - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="autograd") - def circuit(a): - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - return qml.expval(qml.PauliZ(0)) - - from pennylane import numpy as anp - - a = anp.array(0.1, requires_grad=True) - - res1 = circuit(a) - grad_fn = qml.grad(circuit) - grad1 = grad_fn(a) - - # switch to Torch interface - circuit.to_torch() - - a = torch.tensor(0.1, dtype=torch.float64, requires_grad=True) - - res2 = circuit(a) - res2.backward() - grad2 = a.grad - assert np.allclose(res1, res2.detach().numpy(), atol=tol, rtol=0) - assert np.allclose(grad1, grad2, atol=tol, rtol=0) - - def test_drawing(self, dev_name, diff_method): - """Test circuit drawing when using the torch interface""" - - x = torch.tensor(0.1, requires_grad=True) - y = torch.tensor([0.2, 0.3], requires_grad=True) - z = torch.tensor(0.4, requires_grad=True) - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, interface="torch") - def circuit(p1, p2=y, **kwargs): - qml.RX(p1, wires=0) - qml.RY(p2[0] * p2[1], wires=1) - qml.RX(kwargs["p3"], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.probs(wires=0), qml.var(qml.PauliZ(1)) - - circuit(p1=x, p3=z) - - result = circuit.draw() - expected = """\ - 0: ──RX(0.1)───RX(0.4)──╭C──┤ Probs - 1: ──RY(0.06)───────────╰X──┤ Var[Z] -""" - - assert result == expected - - def test_jacobian(self, dev_name, diff_method, mocker, tol): - """Test jacobian calculation""" - spy = mocker.spy(JacobianTape, "jacobian") - - a_val = 0.1 - b_val = 0.2 - - a = torch.tensor(a_val, dtype=torch.float64, requires_grad=True) - b = torch.tensor(b_val, dtype=torch.float64, requires_grad=True) - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1))] - - res = circuit(a, b) - - assert circuit.qtape.trainable_params == [0, 1] - - assert isinstance(res, torch.Tensor) - assert res.shape == (2,) - - expected = [np.cos(a_val), -np.cos(a_val) * np.sin(b_val)] - assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - - loss = torch.sum(res) - - loss.backward() - expected = [ - -np.sin(a_val) + np.sin(a_val) * np.sin(b_val), - -np.cos(a_val) * np.cos(b_val), - ] - assert np.allclose(a.grad, expected[0], atol=tol, rtol=0) - assert np.allclose(b.grad, expected[1], atol=tol, rtol=0) - - if diff_method == "finite-diff": - spy.assert_called() - elif diff_method == "backprop": - spy.assert_not_called() - - def test_jacobian_dtype(self, dev_name, diff_method, tol): - """Test calculating the jacobian with a different datatype""" - if diff_method == "backprop": - pytest.skip("Test does not support backprop") - - a = torch.tensor(0.1, dtype=torch.float32, requires_grad=True) - b = torch.tensor(0.2, dtype=torch.float32, requires_grad=True) - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, interface="torch", diff_method=diff_method) - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1))] - - circuit.to_torch(dtype=torch.float32) - assert circuit.dtype is torch.float32 - - res = circuit(a, b) - - assert circuit.qtape.interface == "torch" - assert circuit.qtape.trainable_params == [0, 1] - - assert isinstance(res, torch.Tensor) - assert res.shape == (2,) - assert res.dtype is torch.float32 - - loss = torch.sum(res) - loss.backward() - assert a.grad.dtype is torch.float32 - assert b.grad.dtype is torch.float32 - - def test_jacobian_options(self, dev_name, diff_method, mocker, tol): - """Test setting jacobian options""" - if diff_method != "finite-diff": - pytest.skip("Test only works with finite-diff") - - spy = mocker.spy(JacobianTape, "numeric_pd") - - a = torch.tensor([0.1, 0.2], requires_grad=True) - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="torch", h=1e-8, order=2) - def circuit(a): - qml.RY(a[0], wires=0) - qml.RX(a[1], wires=0) - return qml.expval(qml.PauliZ(0)) - - res = circuit(a) - res.backward() - - for args in spy.call_args_list: - assert args[1]["order"] == 2 - assert args[1]["h"] == 1e-8 - - def test_changing_trainability(self, dev_name, diff_method, mocker, tol): - """Test that changing the trainability of parameters changes the - number of differentiation requests made""" - if diff_method != "finite-diff": - pytest.skip("Test only works with finite-diff") - - a_val = 0.1 - b_val = 0.2 - - a = torch.tensor(a_val, dtype=torch.float64, requires_grad=True) - b = torch.tensor(b_val, dtype=torch.float64, requires_grad=True) - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, interface="torch", diff_method="finite-diff") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1)) - - res = circuit(a, b) - - # the tape has reported both gate arguments as trainable - assert circuit.qtape.trainable_params == [0, 1] - - expected = [np.cos(a_val), -np.cos(a_val) * np.sin(b_val)] - assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - - spy = mocker.spy(JacobianTape, "numeric_pd") - - loss = torch.sum(res) - loss.backward() - - expected = [ - -np.sin(a_val) + np.sin(a_val) * np.sin(b_val), - -np.cos(a_val) * np.cos(b_val), - ] - assert np.allclose([a.grad, b.grad], expected, atol=tol, rtol=0) - - # JacobianTape.numeric_pd has been called for each argument - assert len(spy.call_args_list) == 2 - - # make the second QNode argument a constant - a_val = 0.54 - b_val = 0.8 - - a = torch.tensor(a_val, dtype=torch.float64, requires_grad=True) - b = torch.tensor(b_val, dtype=torch.float64, requires_grad=False) - - res = circuit(a, b) - - # the tape has reported only the first argument as trainable - assert circuit.qtape.trainable_params == [0] - - expected = [np.cos(a_val), -np.cos(a_val) * np.sin(b_val)] - assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - - spy.call_args_list = [] - loss = torch.sum(res) - loss.backward() - expected = -np.sin(a_val) + np.sin(a_val) * np.sin(b_val) - assert np.allclose(a.grad, expected, atol=tol, rtol=0) - - # JacobianTape.numeric_pd has been called only once - assert len(spy.call_args_list) == 1 - - def test_classical_processing(self, dev_name, diff_method, tol): - """Test classical processing within the quantum tape""" - a = torch.tensor(0.1, dtype=torch.float64, requires_grad=True) - b = torch.tensor(0.2, dtype=torch.float64, requires_grad=False) - c = torch.tensor(0.3, dtype=torch.float64, requires_grad=True) - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(a, b, c): - qml.RY(a * c, wires=0) - qml.RZ(b, wires=0) - qml.RX(c + c**2 + torch.sin(a), wires=0) - return qml.expval(qml.PauliZ(0)) - - res = circuit(a, b, c) - - if diff_method == "finite-diff": - assert circuit.qtape.trainable_params == [0, 2] - assert circuit.qtape.get_parameters() == [a * c, c + c**2 + torch.sin(a)] - - res.backward() - - assert isinstance(a.grad, torch.Tensor) - assert b.grad is None - assert isinstance(c.grad, torch.Tensor) - - def test_no_trainable_parameters(self, dev_name, diff_method, tol): - """Test evaluation and Jacobian if there are no trainable parameters""" - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - a = 0.1 - b = torch.tensor(0.2, dtype=torch.float64, requires_grad=False) - - res = circuit(a, b) - - if diff_method == "finite-diff": - assert circuit.qtape.trainable_params == [] - - assert res.shape == (2,) - assert isinstance(res, torch.Tensor) - - with pytest.raises( - RuntimeError, - match="element 0 of tensors does not require grad and does not have a grad_fn", - ): - res.backward() - - @pytest.mark.parametrize( - "U", - [ - torch.tensor([[0, 1], [1, 0]], requires_grad=False), - np.array([[0, 1], [1, 0]]), - ], - ) - def test_matrix_parameter(self, dev_name, diff_method, U, tol): - """Test that the Torch interface works correctly - with a matrix parameter""" - a_val = 0.1 - a = torch.tensor(a_val, dtype=torch.float64, requires_grad=True) - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(U, a): - qml.QubitUnitary(U, wires=0) - qml.RY(a, wires=0) - return qml.expval(qml.PauliZ(0)) - - res = circuit(U, a) - - if diff_method == "finite-diff": - assert circuit.qtape.trainable_params == [1] - - assert np.allclose(res.detach(), -np.cos(a_val), atol=tol, rtol=0) - - res.backward() - assert np.allclose(a.grad, np.sin(a_val), atol=tol, rtol=0) - - def test_differentiable_expand(self, dev_name, diff_method, tol): - """Test that operation and nested tapes expansion - is differentiable""" - - class U3(qml.U3): - def expand(self): - theta, phi, lam = self.data - wires = self.wires - - with JacobianTape() as tape: - qml.Rot(lam, theta, -lam, wires=wires) - qml.PhaseShift(phi + lam, wires=wires) - - return tape - - dev = qml.device(dev_name, wires=1) - a = np.array(0.1) - p_val = [0.1, 0.2, 0.3] - p = torch.tensor(p_val, dtype=torch.float64, requires_grad=True) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(a, p): - qml.RX(a, wires=0) - U3(p[0], p[1], p[2], wires=0) - return qml.expval(qml.PauliX(0)) - - res = circuit(a, p) - - assert circuit.qtape.trainable_params == [1, 2, 3, 4] - assert [i.name for i in circuit.qtape.operations] == ["RX", "Rot", "PhaseShift"] - assert np.all(circuit.qtape.get_parameters() == [p[2], p[0], -p[2], p[1] + p[2]]) - - expected = np.cos(a) * np.cos(p_val[1]) * np.sin(p_val[0]) + np.sin(a) * ( - np.cos(p_val[2]) * np.sin(p_val[1]) - + np.cos(p_val[0]) * np.cos(p_val[1]) * np.sin(p_val[2]) - ) - assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - - res.backward() - expected = np.array( - [ - np.cos(p_val[1]) - * (np.cos(a) * np.cos(p_val[0]) - np.sin(a) * np.sin(p_val[0]) * np.sin(p_val[2])), - np.cos(p_val[1]) * np.cos(p_val[2]) * np.sin(a) - - np.sin(p_val[1]) - * (np.cos(a) * np.sin(p_val[0]) + np.cos(p_val[0]) * np.sin(a) * np.sin(p_val[2])), - np.sin(a) - * ( - np.cos(p_val[0]) * np.cos(p_val[1]) * np.cos(p_val[2]) - - np.sin(p_val[1]) * np.sin(p_val[2]) - ), - ] - ) - assert np.allclose(p.grad, expected, atol=tol, rtol=0) - - def test_probability_differentiation(self, dev_name, diff_method, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - - if diff_method == "adjoint": - pytest.skip("The adjoint method does not currently support returning probabilities") - - dev = qml.device(dev_name, wires=2) - x_val = 0.543 - y_val = -0.654 - x = torch.tensor(x_val, requires_grad=True) - y = torch.tensor(y_val, requires_grad=True) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.probs(wires=[0]), qml.probs(wires=[1]) - - res = circuit(x, y) - - expected = np.array( - [ - [np.cos(x_val / 2) ** 2, np.sin(x_val / 2) ** 2], - [ - (1 + np.cos(x_val) * np.cos(y_val)) / 2, - (1 - np.cos(x_val) * np.cos(y_val)) / 2, - ], - ] - ) - - if diff_method == "backprop": - # TODO: check why this differs from other interfaces - expected = expected.flatten() - - assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - - loss = torch.sum(res) - loss.backward() - expected = np.array( - [ - -np.sin(x_val) / 2 - + np.sin(x_val) / 2 - - np.sin(x_val) * np.cos(y_val) / 2 - + np.cos(y_val) * np.sin(x_val) / 2, - -np.cos(x_val) * np.sin(y_val) / 2 + np.cos(x_val) * np.sin(y_val) / 2, - ] - ) - assert np.allclose(x.grad, expected[0], atol=tol, rtol=0) - assert np.allclose(y.grad, expected[1], atol=tol, rtol=0) - - def test_ragged_differentiation(self, dev_name, diff_method, monkeypatch, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - if diff_method == "adjoint": - pytest.skip("The adjoint method does not currently support returning probabilities") - - dev = qml.device(dev_name, wires=2) - x_val = 0.543 - y_val = -0.654 - x = torch.tensor(x_val, requires_grad=True) - y = torch.tensor(y_val, requires_grad=True) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return [qml.expval(qml.PauliZ(0)), qml.probs(wires=[1])] - - res = circuit(x, y) - - expected = np.array( - [ - np.cos(x_val), - (1 + np.cos(x_val) * np.cos(y_val)) / 2, - (1 - np.cos(x_val) * np.cos(y_val)) / 2, - ] - ) - assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - - loss = torch.sum(res) - loss.backward() - expected = np.array( - [ - -np.sin(x_val) - + -np.sin(x_val) * np.cos(y_val) / 2 - + np.cos(y_val) * np.sin(x_val) / 2, - -np.cos(x_val) * np.sin(y_val) / 2 + np.cos(x_val) * np.sin(y_val) / 2, - ] - ) - assert np.allclose(x.grad, expected[0], atol=tol, rtol=0) - assert np.allclose(y.grad, expected[1], atol=tol, rtol=0) - - def test_sampling(self, dev_name, diff_method): - """Test sampling works as expected""" - if diff_method == "backprop": - pytest.skip("Sampling not possible with backprop differentiation.") - - dev = qml.device(dev_name, wires=2, shots=10) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(): - qml.Hadamard(wires=[0]) - qml.CNOT(wires=[0, 1]) - return [qml.sample(qml.PauliZ(0)), qml.sample(qml.PauliX(1))] - - res = circuit() - - assert res.shape == (2, 10) - assert isinstance(res, torch.Tensor) - - def test_sampling_expval(self, dev_name, diff_method): - """Test sampling works as expected if combined with expectation values""" - if diff_method == "backprop": - pytest.skip("Sampling not possible with backprop differentiation.") - - dev = qml.device(dev_name, wires=2, shots=10) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(): - qml.Hadamard(wires=[0]) - qml.CNOT(wires=[0, 1]) - return qml.sample(qml.PauliZ(0)), qml.expval(qml.PauliX(1)) - - res = circuit() - - assert len(res) == 2 - assert isinstance(res, tuple) - assert res[0].shape == (10,) - assert isinstance(res[0], torch.Tensor) - assert isinstance(res[1], torch.Tensor) - - def test_hessian(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a scalar valued QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - return qml.expval(qml.PauliZ(0)) - - x = torch.tensor([1.0, 2.0], requires_grad=True) - res = circuit(x) - - res.backward() - g = x.grad - - spy = mocker.spy(JacobianTape, "hessian") - hess = hessian(circuit, x) - - if diff_method == "parameter-shift": - spy.assert_called_once() - elif diff_method == "backprop": - spy.assert_not_called() - - a, b = x.detach().numpy() - - expected_res = np.cos(a) * np.cos(b) - assert np.allclose(res.detach(), expected_res, atol=tol, rtol=0) - - expected_g = [-np.sin(a) * np.cos(b), -np.cos(a) * np.sin(b)] - assert np.allclose(g.detach(), expected_g, atol=tol, rtol=0) - - expected_hess = [ - [-np.cos(a) * np.cos(b), np.sin(a) * np.sin(b)], - [np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)], - ] - assert np.allclose(hess.detach(), expected_hess, atol=tol, rtol=0) - - def test_hessian_vector_valued(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a vector valued QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - return qml.probs(wires=0) - - x = torch.tensor([1.0, 2.0], requires_grad=True) - res = circuit(x) - jac_fn = lambda x: jacobian(circuit, x, create_graph=True) - - g = jac_fn(x) - - spy = mocker.spy(JacobianTape, "hessian") - hess = jacobian(jac_fn, x) - - if diff_method == "parameter-shift": - spy.assert_called_once() - elif diff_method == "backprop": - spy.assert_not_called() - - a, b = x.detach().numpy() - - expected_res = [ - 0.5 + 0.5 * np.cos(a) * np.cos(b), - 0.5 - 0.5 * np.cos(a) * np.cos(b), - ] - assert np.allclose(res.detach(), expected_res, atol=tol, rtol=0) - - expected_g = [ - [-0.5 * np.sin(a) * np.cos(b), -0.5 * np.cos(a) * np.sin(b)], - [0.5 * np.sin(a) * np.cos(b), 0.5 * np.cos(a) * np.sin(b)], - ] - assert np.allclose(g.detach(), expected_g, atol=tol, rtol=0) - - expected_hess = [ - [ - [-0.5 * np.cos(a) * np.cos(b), 0.5 * np.sin(a) * np.sin(b)], - [0.5 * np.sin(a) * np.sin(b), -0.5 * np.cos(a) * np.cos(b)], - ], - [ - [0.5 * np.cos(a) * np.cos(b), -0.5 * np.sin(a) * np.sin(b)], - [-0.5 * np.sin(a) * np.sin(b), 0.5 * np.cos(a) * np.cos(b)], - ], - ] - assert np.allclose(hess.detach(), expected_hess, atol=tol, rtol=0) - - def test_hessian_ragged(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a ragged QNode""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=2) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(x): - qml.RY(x[0], wires=0) - qml.RX(x[1], wires=0) - qml.RY(x[0], wires=1) - qml.RX(x[1], wires=1) - return qml.expval(qml.PauliZ(0)), qml.probs(wires=1) - - x = torch.tensor([1.0, 2.0], requires_grad=True) - res = circuit(x) - jac_fn = lambda x: jacobian(circuit, x, create_graph=True) - - g = jac_fn(x) - - spy = mocker.spy(JacobianTape, "hessian") - hess = jacobian(jac_fn, x) - - if diff_method == "parameter-shift": - spy.assert_called_once() - elif diff_method == "backprop": - spy.assert_not_called() - - a, b = x.detach().numpy() - - expected_res = [ - np.cos(a) * np.cos(b), - 0.5 + 0.5 * np.cos(a) * np.cos(b), - 0.5 - 0.5 * np.cos(a) * np.cos(b), - ] - assert np.allclose(res.detach(), expected_res, atol=tol, rtol=0) - - expected_g = [ - [-np.sin(a) * np.cos(b), -np.cos(a) * np.sin(b)], - [-0.5 * np.sin(a) * np.cos(b), -0.5 * np.cos(a) * np.sin(b)], - [0.5 * np.sin(a) * np.cos(b), 0.5 * np.cos(a) * np.sin(b)], - ] - assert np.allclose(g.detach(), expected_g, atol=tol, rtol=0) - - expected_hess = [ - [ - [-np.cos(a) * np.cos(b), np.sin(a) * np.sin(b)], - [np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)], - ], - [ - [-0.5 * np.cos(a) * np.cos(b), 0.5 * np.sin(a) * np.sin(b)], - [0.5 * np.sin(a) * np.sin(b), -0.5 * np.cos(a) * np.cos(b)], - ], - [ - [0.5 * np.cos(a) * np.cos(b), -0.5 * np.sin(a) * np.sin(b)], - [-0.5 * np.sin(a) * np.sin(b), 0.5 * np.cos(a) * np.cos(b)], - ], - ] - assert np.allclose(hess.detach(), expected_hess, atol=tol, rtol=0) - - @pytest.mark.xfail( - reason="Test fails on Torch 1.10, however the Tape interfaces are deprecated and will not be fixed." - ) - def test_hessian_vector_valued_postprocessing(self, dev_name, diff_method, mocker, tol): - """Test hessian calculation of a vector valued QNode with post-processing""" - if diff_method not in {"parameter-shift", "backprop"}: - pytest.skip("Test only supports parameter-shift or backprop") - - dev = qml.device(dev_name, wires=1) - - @qnode(dev, diff_method=diff_method, interface="torch") - def circuit(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=0) - return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(0))] - - x = torch.tensor([0.76, -0.87], requires_grad=True, dtype=torch.float64) - - def cost_fn(x): - return x @ circuit(x) - - a, b = x.detach().numpy() - - res = cost_fn(x) - expected_res = np.array([a, b]) @ [np.cos(a) * np.cos(b), np.cos(a) * np.cos(b)] - assert np.allclose(res.detach(), expected_res, atol=tol, rtol=0) - - res.backward() - - g = x.grad - expected_g = [ - np.cos(b) * (np.cos(a) - (a + b) * np.sin(a)), - np.cos(a) * (np.cos(b) - (a + b) * np.sin(b)), - ] - assert np.allclose(g.detach(), expected_g, atol=tol, rtol=0) - - spy = mocker.spy(JacobianTape, "hessian") - hess = hessian(cost_fn, x) - - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called_once() - - expected_hess = [ - [ - -(np.cos(b) * ((a + b) * np.cos(a) + 2 * np.sin(a))), - -(np.cos(b) * np.sin(a)) + (-np.cos(a) + (a + b) * np.sin(a)) * np.sin(b), - ], - [ - -(np.cos(b) * np.sin(a)) + (-np.cos(a) + (a + b) * np.sin(a)) * np.sin(b), - -(np.cos(a) * ((a + b) * np.cos(b) + 2 * np.sin(b))), - ], - ] - - assert np.allclose(hess.detach(), expected_hess, atol=tol, rtol=0) - - -class Test_adjoint: - def test_adjoint_default_save_state(self, mocker): - """tests that the state will be saved by default""" - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, diff_method="adjoint", interface="torch") - def circ(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=1) - qml.CNOT(wires=(0, 1)) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1)) - - expected_grad = lambda x: torch.tensor([-torch.sin(x[0]), torch.cos(x[1])]) - - spy = mocker.spy(dev, "adjoint_jacobian") - - x1 = torch.tensor([0.1, 0.2], requires_grad=True) - x2 = torch.tensor([0.3, 0.4], requires_grad=True) - - res1 = circ(x1) - res2 = circ(x2) - - res1.backward(torch.Tensor([1, 1])) - res2.backward(torch.Tensor([1, 1])) - - assert np.allclose(x1.grad, expected_grad(x1)) - assert np.allclose(x2.grad, expected_grad(x2)) - - assert circ.device.num_executions == 2 - - spy.assert_called_with(mocker.ANY, starting_state=mocker.ANY) - - def test_adjoint_save_state(self, mocker): - """Tests that the torch interface reuses device state when prompted by `cache_state=True`. - Also tests a second execution before backward pass does not alter gradient. - """ - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev, diff_method="adjoint", interface="torch", adjoint_cache=True) - def circ(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=1) - qml.CNOT(wires=(0, 1)) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1)) - - expected_grad = lambda x: torch.tensor([-torch.sin(x[0]), torch.cos(x[1])]) - - spy = mocker.spy(dev, "adjoint_jacobian") - - x1 = torch.tensor([0.1, 0.2], requires_grad=True) - x2 = torch.tensor([0.3, 0.4], requires_grad=True) - - res1 = circ(x1) - res2 = circ(x2) - - res1.backward(torch.Tensor([1, 1])) - res2.backward(torch.Tensor([1, 1])) - - assert np.allclose(x1.grad, expected_grad(x1)) - assert np.allclose(x2.grad, expected_grad(x2)) - - assert circ.device.num_executions == 2 - - spy.assert_called_with(mocker.ANY, starting_state=mocker.ANY) - - assert circ.qtape.jacobian_options["adjoint_cache"] == True - - def test_adjoint_no_save_state(self, mocker): - """Tests that with `adjoint_cache=False`, the state is not cached""" - - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, diff_method="adjoint", interface="torch", adjoint_cache=False) - def circ(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - spy = mocker.spy(dev, "adjoint_jacobian") - - x = torch.tensor(0.1, requires_grad=True) - res = circ(x) - res.backward() - - assert circ.device.num_executions == 2 - - spy.assert_called_with(mocker.ANY) - - assert circ.qtape.jacobian_options.get("adjoint_cache", False) == False - - -def qtransform(qnode, a, framework=torch): - """Transforms every RY(y) gate in a circuit to RX(-a*cos(y))""" - - def construct(self, args, kwargs): - """New quantum tape construct method, that performs - the transform on the tape in a define-by-run manner""" - - # the following global variable is defined simply for testing - # purposes, so that we can easily extract the transformed operations - # for verification. - global t_op - - t_op = [] - - QNode.construct(self, args, kwargs) - - new_ops = [] - for o in self.qtape.operations: - # here, we loop through all tape operations, and make - # the transformation if a RY gate is encountered. - if isinstance(o, qml.RY): - t_op.append(qml.RX(-a * framework.cos(o.data[0]), wires=o.wires)) - new_ops.append(t_op[-1]) - else: - new_ops.append(o) - - self.qtape._ops = new_ops - self.qtape._update() - - import copy - - new_qnode = copy.deepcopy(qnode) - new_qnode.construct = construct.__get__(new_qnode, QNode) - return new_qnode - - -def test_transform(tol): - """Test an example transform""" - - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, interface="torch") - def circuit(weights): - # the following global variables are defined simply for testing - # purposes, so that we can easily extract the operations for verification. - global op1, op2 - op1 = qml.RY(weights[0], wires=0) - op2 = qml.RX(weights[1], wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - weights = torch.tensor([0.32, 0.543], requires_grad=True) - a = torch.tensor(0.5, requires_grad=True) - - # transform the circuit QNode with trainable weight 'a' - new_qnode = qtransform(circuit, a) - - # evaluate the transformed QNode - res = new_qnode(weights) - - # evaluate the original QNode with pre-processed parameters - res2 = circuit(torch.sin(weights)) - - # the loss is the sum of the two QNode evaluations - loss = res + res2 - - # verify that the transformed QNode has the expected operations - assert circuit.qtape.operations == [op1, op2] - assert new_qnode.qtape.operations[0] == t_op[0] - assert new_qnode.qtape.operations[1].name == op2.name - assert new_qnode.qtape.operations[1].wires == op2.wires - - # check that the incident gate arguments of both QNode tapes are correct - tape_params = [p.detach() for p in circuit.qtape.get_parameters()] - assert np.all(tape_params == [torch.sin(w) for w in weights]) - - tape_params = [p.detach() for p in new_qnode.qtape.get_parameters()] - assert np.all(tape_params == [-a * torch.cos(weights[0]), weights[1]]) - - # verify that the gradient has the correct shape - loss.backward() - assert weights.grad.shape == weights.shape - assert a.grad.shape == a.shape - - # compare against the expected values - assert np.allclose(loss.detach(), 1.8244501889992706, atol=tol, rtol=0) - assert np.allclose(weights.grad, [-0.26610258, -0.47053553], atol=tol, rtol=0) - assert np.allclose(a.grad, 0.06486032, atol=tol, rtol=0) diff --git a/tests/interfaces/test_tape_autograd.py b/tests/interfaces/test_tape_autograd.py deleted file mode 100644 index 0dff3359444..00000000000 --- a/tests/interfaces/test_tape_autograd.py +++ /dev/null @@ -1,752 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the autograd interface""" -import pytest -from pennylane import numpy as np - -import pennylane as qml -from pennylane.tape import JacobianTape -from pennylane.interfaces.autograd import AutogradInterface - - -class TestAutogradQuantumTape: - """Test the autograd interface applied to a tape""" - - def test_interface_str(self): - """Test that the interface string is correctly identified as autograd""" - with AutogradInterface.apply(JacobianTape()) as tape: - qml.RX(0.5, wires=0) - qml.expval(qml.PauliX(0)) - - assert tape.interface == "autograd" - assert isinstance(tape, AutogradInterface) - - def test_get_parameters(self): - """Test that the get_parameters function correctly gets the trainable parameters and all - parameters, depending on the trainable_only argument""" - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=False) - c = np.array(0.3, requires_grad=True) - d = np.array(0.4, requires_grad=False) - - with AutogradInterface.apply(JacobianTape()) as tape: - qml.Rot(a, b, c, wires=0) - qml.RX(d, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliX(0)) - - assert tape.trainable_params == [0, 2] - assert np.all(tape.get_parameters(trainable_only=True) == [a, c]) - assert np.all(tape.get_parameters(trainable_only=False) == [a, b, c, d]) - - def test_execution(self): - """Test execution""" - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=False) - - def cost(a, b, device): - with AutogradInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.expval(qml.PauliZ(0)) - assert tape.trainable_params == [0] - return tape.execute(device) - - dev = qml.device("default.qubit", wires=1) - res = cost(a, b, device=dev) - assert res.shape == (1,) - - def test_scalar_jacobian(self, tol): - """Test scalar jacobian calculation""" - a = np.array(0.1, requires_grad=True) - - def cost(a, device): - with AutogradInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.expval(qml.PauliZ(0)) - assert tape.trainable_params == [0] - return tape.execute(device) - - dev = qml.device("default.qubit", wires=2) - res = qml.jacobian(cost)(a, device=dev) - assert res.shape == (1,) - - # compare to standard tape jacobian - with JacobianTape() as tape: - qml.RY(a, wires=0) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = [0] - expected = tape.jacobian(dev) - assert expected.shape == (1, 1) - assert np.allclose(res, np.squeeze(expected), atol=tol, rtol=0) - - def test_jacobian(self, tol): - """Test jacobian calculation""" - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=True) - - def cost(a, b, device): - with AutogradInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - assert tape.trainable_params == [0, 1] - return tape.execute(device) - - dev = qml.device("default.qubit", wires=2) - - res = cost(a, b, device=dev) - expected = [np.cos(a), -np.cos(a) * np.sin(b)] - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = qml.jacobian(cost)(a, b, device=dev) - assert isinstance(res, tuple) and len(res) == 2 - assert res[0].shape == (2,) - assert res[1].shape == (2,) - - expected = ([-np.sin(a), np.sin(a) * np.sin(b)], [0, -np.cos(a) * np.cos(b)]) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - - def test_jacobian_options(self, mocker, tol): - """Test setting jacobian options""" - spy = mocker.spy(JacobianTape, "numeric_pd") - - a = np.array([0.1, 0.2], requires_grad=True) - - dev = qml.device("default.qubit", wires=1) - - def cost(a, device): - with AutogradInterface.apply(JacobianTape()) as qtape: - qml.RY(a[0], wires=0) - qml.RX(a[1], wires=0) - qml.expval(qml.PauliZ(0)) - - qtape.jacobian_options = {"h": 1e-8, "order": 2} - return qtape.execute(dev) - - res = qml.jacobian(cost)(a, device=dev) - - for args in spy.call_args_list: - assert args[1]["order"] == 2 - assert args[1]["h"] == 1e-8 - - def test_reusing_quantum_tape(self, tol): - """Test re-using a quantum tape by passing new parameters""" - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=True) - - dev = qml.device("default.qubit", wires=2) - - with AutogradInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - assert tape.trainable_params == [0, 1] - - def cost(a, b): - tape.set_parameters([a, b]) - return tape.execute(dev) - - jac_fn = qml.jacobian(cost) - jac = jac_fn(a, b) - - a = np.array(0.54, requires_grad=True) - b = np.array(0.8, requires_grad=True) - - res2 = cost(2 * a, b) - expected = [np.cos(2 * a), -np.cos(2 * a) * np.sin(b)] - assert np.allclose(res2, expected, atol=tol, rtol=0) - - jac_fn = qml.jacobian(lambda a, b: cost(2 * a, b)) - jac = jac_fn(a, b) - assert isinstance(jac, tuple) and len(jac) == 2 - - expected = ( - [-2 * np.sin(2 * a), 2 * np.sin(2 * a) * np.sin(b)], - [0, -np.cos(2 * a) * np.cos(b)], - ) - assert np.allclose(jac[0], expected[0], atol=tol, rtol=0) - assert np.allclose(jac[1], expected[1], atol=tol, rtol=0) - - def test_classical_processing(self, tol): - """Test classical processing within the quantum tape""" - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=False) - c = np.array(0.3, requires_grad=True) - - def cost(a, b, c, device): - with AutogradInterface.apply(JacobianTape()) as tape: - qml.RY(a * c, wires=0) - qml.RZ(b, wires=0) - qml.RX(c + c**2 + np.sin(a), wires=0) - qml.expval(qml.PauliZ(0)) - assert tape.trainable_params == [0, 2] - return tape.execute(device) - - dev = qml.device("default.qubit", wires=2) - res = qml.jacobian(cost)(a, b, c, device=dev) - assert isinstance(res, tuple) and len(res) == 2 - assert res[0].shape == (1,) - assert res[1].shape == (1,) - - def test_no_trainable_parameters(self, tol): - """Test evaluation and Jacobian if there are no trainable parameters""" - a = np.array(0.1, requires_grad=False) - b = np.array(0.2, requires_grad=False) - - def cost(a, b, device): - with AutogradInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliZ(1)) - assert tape.trainable_params == [] - return tape.execute(device) - - dev = qml.device("default.qubit", wires=2) - res = cost(a, b, device=dev) - assert res.shape == (2,) - - with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"): - res = qml.jacobian(cost)(a, b, device=dev) - assert not res - - def loss(a, b): - return np.sum(cost(a, b, device=dev)) - - with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"): - res = qml.grad(loss)(a, b) - - assert not res - - def test_matrix_parameter(self, tol): - """Test that the autograd interface works correctly - with a matrix parameter""" - U = np.array([[0, 1], [1, 0]], requires_grad=False) - a = np.array(0.1, requires_grad=True) - - def cost(a, U, device): - with AutogradInterface.apply(JacobianTape()) as tape: - qml.QubitUnitary(U, wires=0) - qml.RY(a, wires=0) - qml.expval(qml.PauliZ(0)) - assert tape.trainable_params == [1] - return tape.execute(device) - - dev = qml.device("default.qubit", wires=2) - res = cost(a, U, device=dev) - assert np.allclose(res, -np.cos(a), atol=tol, rtol=0) - - jac_fn = qml.jacobian(cost) - res = jac_fn(a, U, device=dev) - assert np.allclose(res, np.sin(a), atol=tol, rtol=0) - - def test_differentiable_expand(self, tol): - """Test that operation and nested tapes expansion - is differentiable""" - - class U3(qml.U3): - def expand(self): - tape = JacobianTape() - theta, phi, lam = self.data - wires = self.wires - tape._ops += [ - qml.Rot(lam, theta, -lam, wires=wires), - qml.PhaseShift(phi + lam, wires=wires), - ] - return tape - - def cost_fn(a, p, device): - tape = JacobianTape() - - with tape: - qml.RX(a, wires=0) - U3(*p, wires=0) - qml.expval(qml.PauliX(0)) - - tape = AutogradInterface.apply(tape.expand()) - - assert tape.trainable_params == [1, 2, 3, 4] - assert [i.name for i in tape.operations] == ["RX", "Rot", "PhaseShift"] - assert np.all(np.array(tape.get_parameters()) == [p[2], p[0], -p[2], p[1] + p[2]]) - - return tape.execute(device=device) - - a = np.array(0.1, requires_grad=False) - p = np.array([0.1, 0.2, 0.3], requires_grad=True) - - dev = qml.device("default.qubit", wires=1) - res = cost_fn(a, p, device=dev) - expected = np.cos(a) * np.cos(p[1]) * np.sin(p[0]) + np.sin(a) * ( - np.cos(p[2]) * np.sin(p[1]) + np.cos(p[0]) * np.cos(p[1]) * np.sin(p[2]) - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - jac_fn = qml.jacobian(cost_fn) - res = jac_fn(a, p, device=dev) - expected = np.array( - [ - np.cos(p[1]) * (np.cos(a) * np.cos(p[0]) - np.sin(a) * np.sin(p[0]) * np.sin(p[2])), - np.cos(p[1]) * np.cos(p[2]) * np.sin(a) - - np.sin(p[1]) - * (np.cos(a) * np.sin(p[0]) + np.cos(p[0]) * np.sin(a) * np.sin(p[2])), - np.sin(a) - * (np.cos(p[0]) * np.cos(p[1]) * np.cos(p[2]) - np.sin(p[1]) * np.sin(p[2])), - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_probability_differentiation(self, tol): - """Tests correct output shape and evaluation for a tape - with prob outputs""" - - def cost(x, y, device): - with AutogradInterface.apply(JacobianTape()) as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=[0]) - qml.probs(wires=[1]) - - return tape.execute(device) - - dev = qml.device("default.qubit", wires=2) - x = np.array(0.543, requires_grad=True) - y = np.array(-0.654, requires_grad=True) - - res = cost(x, y, device=dev) - expected = np.array( - [ - [np.cos(x / 2) ** 2, np.sin(x / 2) ** 2], - [(1 + np.cos(x) * np.cos(y)) / 2, (1 - np.cos(x) * np.cos(y)) / 2], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - jac_fn = qml.jacobian(cost) - res = jac_fn(x, y, device=dev) - assert isinstance(res, tuple) and len(res) == 2 - assert res[0].shape == (2, 2) - assert res[1].shape == (2, 2) - expected = ( - [ - [-np.sin(x) / 2, np.sin(x) / 2], - [-np.sin(x) * np.cos(y) / 2, np.sin(x) * np.cos(y) / 2], - ], - [ - [0, 0], - [-np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], - ], - ) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - - def test_ragged_differentiation(self, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - - def cost(x, y, device): - with AutogradInterface.apply(JacobianTape()) as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.probs(wires=[1]) - - return tape.execute(device) - - dev = qml.device("default.qubit", wires=2) - x = np.array(0.543, requires_grad=True) - y = np.array(-0.654, requires_grad=True) - - res = cost(x, y, device=dev) - expected = np.array( - [np.cos(x), (1 + np.cos(x) * np.cos(y)) / 2, (1 - np.cos(x) * np.cos(y)) / 2] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - jac_fn = qml.jacobian(cost) - res = jac_fn(x, y, device=dev) - expected = ( - [-np.sin(x), -np.sin(x) * np.cos(y) / 2, np.sin(x) * np.cos(y) / 2], - [0, -np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], - ) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - - def test_sampling(self): - """Test sampling works as expected""" - - def cost(x, device): - with AutogradInterface.apply(JacobianTape()) as tape: - qml.Hadamard(wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.sample(qml.PauliZ(0)) - qml.sample(qml.PauliX(1)) - - return tape.execute(device) - - dev = qml.device("default.qubit", wires=2, shots=10) - x = np.array(0.543, requires_grad=True) - res = cost(x, device=dev) - assert res.shape == (2, 10) - - -class TestAutogradPassthru: - """Test that the quantum tape works with an autograd passthru - device. - - These tests are very similar to the tests above, with three key differences: - - * We do **not** apply the autograd interface. These tapes simply use passthru - backprop, no custom gradient registration needed. - - * We do not test the trainable_params attribute. Since these tapes have no - autograd interface, the tape does not need to bookkeep which parameters - are trainable; this is done by autograd internally. - - * We use mock.spy to ensure that the tape's Jacobian method is not being called. - """ - - def test_execution(self): - """Test execution""" - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=False) - - def cost(a, b, device): - with JacobianTape() as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.expval(qml.PauliZ(0)) - return tape.execute(device) - - dev = qml.device("default.qubit.autograd", wires=1) - res = cost(a, b, device=dev) - assert res.shape == (1,) - - def test_scalar_jacobian(self, tol, mocker): - """Test scalar jacobian calculation""" - spy = mocker.spy(JacobianTape, "jacobian") - a = np.array(0.1, requires_grad=True) - - def cost(a, device): - with JacobianTape() as tape: - qml.RY(a, wires=0) - qml.expval(qml.PauliZ(0)) - return tape.execute(device) - - dev = qml.device("default.qubit.autograd", wires=2) - res = qml.jacobian(cost)(a, device=dev) - spy.assert_not_called() - assert res.shape == (1,) - - # compare to standard tape jacobian - with JacobianTape() as tape: - qml.RY(a, wires=0) - qml.expval(qml.PauliZ(0)) - - expected = tape.jacobian(dev) - assert expected.shape == (1, 1) - assert np.allclose(res, np.squeeze(expected), atol=tol, rtol=0) - - def test_jacobian(self, mocker, tol): - """Test jacobian calculation""" - spy = mocker.spy(JacobianTape, "jacobian") - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=True) - - def cost(a, b, device): - with JacobianTape() as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - return tape.execute(device) - - dev = qml.device("default.qubit.autograd", wires=2) - res = qml.jacobian(cost)(a, b, device=dev) - spy.assert_not_called() - assert isinstance(res, tuple) and len(res) == 2 - assert res[0].shape == (2,) - assert res[1].shape == (2,) - - # compare to standard tape jacobian - with JacobianTape() as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - expected = tape.jacobian(dev) - assert expected.shape == (2, 2) - assert np.allclose(res[0], expected.T[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected.T[1], atol=tol, rtol=0) - - def test_classical_processing(self, mocker, tol): - """Test classical processing within the quantum tape""" - spy = mocker.spy(JacobianTape, "jacobian") - a = np.array(0.1, requires_grad=True) - b = np.array(0.2, requires_grad=False) - c = np.array(0.3, requires_grad=True) - - def cost(a, b, c, device): - with JacobianTape() as tape: - qml.RY(a * c, wires=0) - qml.RZ(b, wires=0) - qml.RX(c + c**2 + np.sin(a), wires=0) - qml.expval(qml.PauliZ(0)) - return tape.execute(device) - - dev = qml.device("default.qubit.autograd", wires=2) - res = qml.jacobian(cost)(a, b, c, device=dev) - assert isinstance(res, tuple) and len(res) == 2 - assert res[0].shape == (1,) - assert res[1].shape == (1,) - spy.assert_not_called() - - def test_no_trainable_parameters(self, mocker, tol): - """Test evaluation and Jacobian if there are no trainable parameters""" - spy = mocker.spy(JacobianTape, "jacobian") - a = np.array(0.1, requires_grad=False) - b = np.array(0.2, requires_grad=False) - - def cost(a, b, device): - with JacobianTape() as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliZ(1)) - return tape.execute(device) - - dev = qml.device("default.qubit.autograd", wires=2) - res = cost(a, b, device=dev) - assert res.shape == (2,) - spy.assert_not_called() - - with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"): - res = qml.jacobian(cost)(a, b, device=dev) - assert not res - - def loss(a, b): - return np.sum(cost(a, b, device=dev)) - - with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"): - res = qml.grad(loss)(a, b) - - assert not res - - def test_matrix_parameter(self, mocker, tol): - """Test jacobian computation when the tape includes a matrix parameter""" - spy = mocker.spy(JacobianTape, "jacobian") - U = np.array([[0, 1], [1, 0]], requires_grad=False) - a = np.array(0.1, requires_grad=True) - - def cost(a, U, device): - with JacobianTape() as tape: - qml.QubitUnitary(U, wires=0) - qml.RY(a, wires=0) - qml.expval(qml.PauliZ(0)) - return tape.execute(device) - - dev = qml.device("default.qubit.autograd", wires=2) - res = cost(a, U, device=dev) - assert np.allclose(res, -np.cos(a), atol=tol, rtol=0) - - jac_fn = qml.jacobian(cost) - res = jac_fn(a, U, device=dev) - assert np.allclose(res, np.sin(a), atol=tol, rtol=0) - spy.assert_not_called() - - def test_differentiable_expand(self, mocker, tol): - """Test that operation and nested tapes expansion - is differentiable""" - spy = mocker.spy(JacobianTape, "jacobian") - - class U3(qml.U3): - def expand(self): - tape = JacobianTape() - theta, phi, lam = self.data - wires = self.wires - tape._ops += [ - qml.Rot(lam, theta, -lam, wires=wires), - qml.PhaseShift(phi + lam, wires=wires), - ] - return tape - - def cost_fn(a, p, device): - tape = JacobianTape() - - with tape: - qml.RX(a, wires=0) - U3(*p, wires=0) - qml.expval(qml.PauliX(0)) - - tape = tape.expand() - - assert [i.name for i in tape.operations] == ["RX", "Rot", "PhaseShift"] - assert np.all(tape.get_parameters() == [a, p[2], p[0], -p[2], p[1] + p[2]]) - - return tape.execute(device=device) - - a = np.array(0.1, requires_grad=False) - p = np.array([0.1, 0.2, 0.3], requires_grad=True) - - dev = qml.device("default.qubit.autograd", wires=1) - - res = cost_fn(a, p, device=dev) - expected = np.cos(a) * np.cos(p[1]) * np.sin(p[0]) + np.sin(a) * ( - np.cos(p[2]) * np.sin(p[1]) + np.cos(p[0]) * np.cos(p[1]) * np.sin(p[2]) - ) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - jac_fn = qml.jacobian(cost_fn) - - res = jac_fn(a, p, device=dev) - expected = np.array( - [ - np.cos(p[1]) * (np.cos(a) * np.cos(p[0]) - np.sin(a) * np.sin(p[0]) * np.sin(p[2])), - np.cos(p[1]) * np.cos(p[2]) * np.sin(a) - - np.sin(p[1]) - * (np.cos(a) * np.sin(p[0]) + np.cos(p[0]) * np.sin(a) * np.sin(p[2])), - np.sin(a) - * (np.cos(p[0]) * np.cos(p[1]) * np.cos(p[2]) - np.sin(p[1]) * np.sin(p[2])), - ] - ) - - assert np.allclose(res, expected, atol=tol, rtol=0) - spy.assert_not_called() - - def test_probability_differentiation(self, mocker, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - spy = mocker.spy(JacobianTape, "jacobian") - - def cost(x, y, device): - with JacobianTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=[0]) - qml.probs(wires=[1]) - - return tape.execute(device) - - dev = qml.device("default.qubit.autograd", wires=2) - x = np.array(0.543, requires_grad=True) - y = np.array(-0.654, requires_grad=True) - - res = cost(x, y, device=dev) - expected = np.array( - [ - [np.cos(x / 2) ** 2, np.sin(x / 2) ** 2], - [(1 + np.cos(x) * np.cos(y)) / 2, (1 - np.cos(x) * np.cos(y)) / 2], - ] - ) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - jac_fn = qml.jacobian(cost) - res = jac_fn(x, y, device=dev) - assert isinstance(res, tuple) and len(res) == 2 - assert res[0].shape == (2, 2) - assert res[1].shape == (2, 2) - - expected = ( - [ - [-np.sin(x) / 2, np.sin(x) / 2], - [-np.sin(x) * np.cos(y) / 2, np.sin(x) * np.cos(y) / 2], - ], - [ - [0, 0], - [-np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], - ], - ) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - spy.assert_not_called() - - def test_ragged_differentiation(self, mocker, monkeypatch, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - spy = mocker.spy(JacobianTape, "jacobian") - dev = qml.device("default.qubit.autograd", wires=2) - - def _asarray(args, dtype=np.float64): - return np.hstack(args).flatten() - - # The current DefaultQubitAutograd device provides an _asarray method that does - # not work correctly for ragged arrays. For ragged arrays, we would like _asarray to - # flatten the array. Here, we patch the _asarray method on the device to achieve this - # behaviour; once the tape has moved from the beta folder, we should implement - # this change directly in the device. - monkeypatch.setattr(dev, "_asarray", _asarray) - - def cost(x, y, device): - with JacobianTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.probs(wires=[1]) - - return tape.execute(device) - - x = np.array(0.543, requires_grad=True) - y = np.array(-0.654, requires_grad=True) - - res = cost(x, y, device=dev) - expected = np.array( - [np.cos(x), (1 + np.cos(x) * np.cos(y)) / 2, (1 - np.cos(x) * np.cos(y)) / 2] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - jac_fn = qml.jacobian(cost) - res = jac_fn(x, y, device=dev) - assert isinstance(res, tuple) and len(res) == 2 - assert res[0].shape == (3,) - assert res[1].shape == (3,) - expected = ( - [-np.sin(x), -np.sin(x) * np.cos(y) / 2, np.sin(x) * np.cos(y) / 2], - [0, -np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], - ) - assert np.allclose(res[0], expected[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - spy.assert_not_called() - - def test_sampling(self): - """Test sampling works as expected""" - - def cost(x, device): - with JacobianTape() as tape: - qml.Hadamard(wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.sample(qml.PauliZ(0)) - qml.sample(qml.PauliX(1)) - - return tape.execute(device) - - dev = qml.device("default.qubit.autograd", wires=2, shots=10) - x = np.array(0.543, requires_grad=True) - res = cost(x, device=dev) - assert res.shape == (2, 10) diff --git a/tests/interfaces/test_tape_jax.py b/tests/interfaces/test_tape_jax.py deleted file mode 100644 index 9286c688a00..00000000000 --- a/tests/interfaces/test_tape_jax.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the JAX interface""" -import pytest - -jax = pytest.importorskip("jax") -jnp = pytest.importorskip("jax.numpy") -import numpy as np -from functools import partial -import pennylane as qml -from pennylane.tape import JacobianTape -from pennylane.interfaces.jax import JAXInterface - - -class TestJAXQuantumTape: - """Test the JAX interface applied to a tape""" - - def test_interface_str(self): - """Test that the interface string is correctly identified as JAX""" - with JAXInterface.apply(JacobianTape()) as tape: - qml.RX(0.5, wires=0) - qml.expval(qml.PauliX(0)) - - assert tape.interface == "jax" - assert isinstance(tape, JAXInterface) - - def test_get_parameters(self): - """Test that the get_parameters function correctly gets the trainable parameters and all - parameters, depending on the trainable_only argument""" - a = jnp.array(0.1) - b = jnp.array(0.2) - c = jnp.array(0.3) - d = jnp.array(0.4) - - with JAXInterface.apply(JacobianTape()) as tape: - qml.Rot(a, b, c, wires=0) - qml.RX(d, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliX(0)) - - np.testing.assert_array_equal(tape.get_parameters(), [a, b, c, d]) - - def test_execution(self): - """Test execution""" - a = jnp.array(0.1) - b = jnp.array(0.2) - - def cost(a, b, device): - with JAXInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.expval(qml.PauliZ(0)) - return tape.execute(device) - - dev = qml.device("default.qubit", wires=1) - res = cost(a, b, device=dev) - assert res.shape == (1,) - # Easiest way to test object is a device array instead of np.array - assert "DeviceArray" in res.__repr__() - - def test_state_raises(self): - """Test returning state raises exception""" - a = jnp.array(0.1) - b = jnp.array(0.2) - - def cost(a, b, device): - with JAXInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.state() - return tape.execute(device) - - dev = qml.device("default.qubit", wires=1) - # TODO(chase): Make this actually work and not raise an error. - with pytest.raises(ValueError): - res = cost(a, b, device=dev) - - def test_execution_with_jit(self): - """Test execution""" - a = jnp.array(0.1) - b = jnp.array(0.2) - - def cost(a, b, device): - with JAXInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=0) - qml.expval(qml.PauliZ(0)) - return tape.execute(device) - - # Not a JAX device! - dev = qml.device("default.qubit", wires=1) - dev_cost = partial(cost, device=dev) - res = jax.jit(dev_cost)(a, b) - assert res.shape == (1,) - # Easiest way to test object is a device array instead of np.array - assert "DeviceArray" in res.__repr__() - - def test_qnode_interface(self): - - dev = qml.device("default.mixed", wires=1) - - @qml.qnode(dev, interface="jax") - def circuit(a, b): - qml.RY(a, wires=0) - qml.RX(b, wires=0) - return qml.expval(qml.PauliZ(0)) - - a = jnp.array(0.1) - b = jnp.array(0.2) - - res = circuit(a, b) - assert "DeviceArray" in res.__repr__() diff --git a/tests/interfaces/test_tape_tf.py b/tests/interfaces/test_tape_tf.py deleted file mode 100644 index 43b7c39e608..00000000000 --- a/tests/interfaces/test_tape_tf.py +++ /dev/null @@ -1,784 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the tf interface""" -import pytest - -tf = pytest.importorskip("tensorflow", minversion="2.1") - -import numpy as np - -import pennylane as qml -from pennylane.tape import JacobianTape -from pennylane.interfaces.tf import TFInterface - - -class TestTFQuantumTape: - """Test the TensorFlow interface applied to a tape""" - - def test_interface_construction(self): - """Test that the interface is correctly applied""" - with TFInterface.apply(JacobianTape()) as tape: - qml.RX(0.5, wires=0) - qml.expval(qml.PauliX(0)) - - assert tape.interface == "tf" - assert isinstance(tape, TFInterface) - assert tape.__bare__ == JacobianTape - assert tape.dtype is tf.float64 - - def test_repeated_interface_construction(self): - """Test that the interface is correctly applied multiple times""" - with TFInterface.apply(JacobianTape()) as tape: - qml.RX(0.5, wires=0) - qml.expval(qml.PauliX(0)) - - assert tape.interface == "tf" - assert isinstance(tape, TFInterface) - assert tape.__bare__ == JacobianTape - assert tape.dtype is tf.float64 - - TFInterface.apply(tape, dtype=tf.float32) - assert tape.interface == "tf" - assert isinstance(tape, TFInterface) - assert tape.__bare__ == JacobianTape - assert tape.dtype is tf.float32 - - def test_get_parameters(self): - """Test that the get parameters function correctly sets and returns the - trainable parameters""" - a = tf.Variable(0.1) - b = tf.constant(0.2) - c = tf.Variable(0.3) - d = 0.4 - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape()) as qtape: - qml.Rot(a, b, c, wires=0) - qml.RX(d, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliX(0)) - - assert qtape.trainable_params == [0, 2] - assert np.all(qtape.get_parameters() == [a, c]) - - def test_execution(self): - """Test execution""" - a = tf.Variable(0.1) - dev = qml.device("default.qubit", wires=1) - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape()) as qtape: - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - qml.expval(qml.PauliZ(0)) - - assert qtape.trainable_params == [0] - res = qtape.execute(dev) - - assert isinstance(res, tf.Tensor) - assert res.shape == (1,) - - def test_jacobian(self, mocker, tol): - """Test jacobian calculation""" - spy = mocker.spy(JacobianTape, "jacobian") - a = tf.Variable(0.1, dtype=tf.float64) - b = tf.Variable(0.2, dtype=tf.float64) - - dev = qml.device("default.qubit", wires=2) - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape()) as qtape: - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - assert qtape.trainable_params == [0, 1] - res = qtape.execute(dev) - - assert isinstance(res, tf.Tensor) - assert res.shape == (2,) - - expected = [tf.cos(a), -tf.cos(a) * tf.sin(b)] - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, [a, b]) - expected = [[-tf.sin(a), tf.sin(a) * tf.sin(b)], [0, -tf.cos(a) * tf.cos(b)]] - assert np.allclose(res, expected, atol=tol, rtol=0) - - spy.assert_called() - - def test_jacobian_dtype(self, tol): - """Test calculating the jacobian with a different datatype. Here, we - specify tf.float32, as opposed to the default value of tf.float64.""" - a = tf.Variable(0.1, dtype=tf.float32) - b = tf.Variable(0.2, dtype=tf.float32) - - dev = qml.device("default.qubit", wires=2) - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape(), dtype=tf.float32) as qtape: - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - assert qtape.trainable_params == [0, 1] - res = qtape.execute(dev) - - assert isinstance(res, tf.Tensor) - assert res.shape == (2,) - assert res.dtype is tf.float32 - - res = tape.jacobian(res, [a, b]) - assert [r.dtype is tf.float32 for r in res] - - def test_jacobian_options(self, mocker, tol): - """Test setting jacobian options""" - spy = mocker.spy(JacobianTape, "numeric_pd") - - a = tf.Variable([0.1, 0.2]) - - dev = qml.device("default.qubit", wires=1) - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape()) as qtape: - qml.RY(a[0], wires=0) - qml.RX(a[1], wires=0) - qml.expval(qml.PauliZ(0)) - - res = qtape.execute(dev) - - qtape.jacobian_options = {"h": 1e-8, "order": 2} - tape.jacobian(res, a) - - for args in spy.call_args_list: - assert args[1]["order"] == 2 - assert args[1]["h"] == 1e-8 - - @pytest.mark.slow - def test_reusing_quantum_tape(self, tol): - """Test re-using a quantum tape by passing new parameters""" - a = tf.Variable(0.1, dtype=tf.float64) - b = tf.Variable(0.2, dtype=tf.float64) - - dev = qml.device("default.qubit", wires=2) - - with tf.GradientTape() as tape: - - with TFInterface.apply(JacobianTape()) as qtape: - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - assert qtape.trainable_params == [0, 1] - - res = qtape.execute(dev) - - jac = tape.jacobian(res, [a, b]) - - a = tf.Variable(0.54, dtype=tf.float64) - b = tf.Variable(0.8, dtype=tf.float64) - - with tf.GradientTape() as tape: - res2 = qtape.execute(dev, params=[2 * a, b]) - - expected = [tf.cos(2 * a), -tf.cos(2 * a) * tf.sin(b)] - assert np.allclose(res2, expected, atol=tol, rtol=0) - - jac2 = tape.jacobian(res2, [a, b]) - expected = [ - [-2 * tf.sin(2 * a), 2 * tf.sin(2 * a) * tf.sin(b)], - [0, -tf.cos(2 * a) * tf.cos(b)], - ] - assert np.allclose(jac2, expected, atol=tol, rtol=0) - - def test_reusing_pre_constructed_quantum_tape(self, tol): - """Test re-using a quantum tape that was previously constructed - *outside of* a gradient tape, by passing new parameters""" - a = tf.Variable(0.1, dtype=tf.float64) - b = tf.Variable(0.2, dtype=tf.float64) - - dev = qml.device("default.qubit", wires=2) - - with TFInterface.apply(JacobianTape()) as qtape: - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - with tf.GradientTape() as tape: - qtape.set_parameters([a, b], trainable_only=False) - qtape._update_trainable_params() - assert qtape.trainable_params == [0, 1] - res = qtape.execute(dev) - - jac = tape.jacobian(res, [a, b]) - - a = tf.Variable(0.54, dtype=tf.float64) - b = tf.Variable(0.8, dtype=tf.float64) - - with tf.GradientTape() as tape: - res2 = qtape.execute(dev, params=[2 * a, b]) - - expected = [tf.cos(2 * a), -tf.cos(2 * a) * tf.sin(b)] - assert np.allclose(res2, expected, atol=tol, rtol=0) - - jac2 = tape.jacobian(res2, [a, b]) - expected = [ - [-2 * tf.sin(2 * a), 2 * tf.sin(2 * a) * tf.sin(b)], - [0, -tf.cos(2 * a) * tf.cos(b)], - ] - assert np.allclose(jac2, expected, atol=tol, rtol=0) - - def test_classical_processing(self, tol): - """Test classical processing within the quantum tape""" - a = tf.Variable(0.1, dtype=tf.float64) - b = tf.constant(0.2, dtype=tf.float64) - c = tf.Variable(0.3, dtype=tf.float64) - - dev = qml.device("default.qubit", wires=1) - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape()) as qtape: - qml.RY(a * c, wires=0) - qml.RZ(b, wires=0) - qml.RX(c + c**2 + tf.sin(a), wires=0) - qml.expval(qml.PauliZ(0)) - - assert qtape.trainable_params == [0, 2] - assert qtape.get_parameters() == [a * c, c + c**2 + tf.sin(a)] - res = qtape.execute(dev) - - res = tape.jacobian(res, [a, b, c]) - assert isinstance(res[0], tf.Tensor) - assert res[1] is None - assert isinstance(res[2], tf.Tensor) - - def test_no_trainable_parameters(self, tol): - """Test evaluation if there are no trainable parameters""" - dev = qml.device("default.qubit", wires=2) - - with tf.GradientTape() as tape: - - with TFInterface.apply(JacobianTape()) as qtape: - qml.RY(0.2, wires=0) - qml.RX(tf.constant(0.1), wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliZ(1)) - - assert qtape.trainable_params == [] - - res = qtape.execute(dev) - - assert res.shape == (2,) - assert isinstance(res, tf.Tensor) - - @pytest.mark.parametrize("U", [tf.constant([[0, 1], [1, 0]]), np.array([[0, 1], [1, 0]])]) - def test_matrix_parameter(self, U, tol): - """Test that the TF interface works correctly - with a matrix parameter""" - a = tf.Variable(0.1, dtype=tf.float64) - - dev = qml.device("default.qubit", wires=2) - - with tf.GradientTape() as tape: - - with TFInterface.apply(JacobianTape()) as qtape: - qml.QubitUnitary(U, wires=0) - qml.RY(a, wires=0) - qml.expval(qml.PauliZ(0)) - - assert qtape.trainable_params == [1] - res = qtape.execute(dev) - - assert np.allclose(res, -tf.cos(a), atol=tol, rtol=0) - - res = tape.jacobian(res, a) - assert np.allclose(res, tf.sin(a), atol=tol, rtol=0) - - def test_differentiable_expand(self, tol): - """Test that operation and nested tapes expansion - is differentiable""" - - class U3(qml.U3): - def expand(self): - tape = JacobianTape() - theta, phi, lam = self.data - wires = self.wires - tape._ops += [ - qml.Rot(lam, theta, -lam, wires=wires), - qml.PhaseShift(phi + lam, wires=wires), - ] - return tape - - qtape = JacobianTape() - - dev = qml.device("default.qubit", wires=1) - a = np.array(0.1) - p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float64) - - with tf.GradientTape() as tape: - - with qtape: - qml.RX(a, wires=0) - U3(p[0], p[1], p[2], wires=0) - qml.expval(qml.PauliX(0)) - - qtape = TFInterface.apply(qtape.expand()) - - assert qtape.trainable_params == [1, 2, 3, 4] - assert [i.name for i in qtape.operations] == ["RX", "Rot", "PhaseShift"] - assert np.all(qtape.get_parameters() == [p[2], p[0], -p[2], p[1] + p[2]]) - - res = qtape.execute(device=dev) - - expected = tf.cos(a) * tf.cos(p[1]) * tf.sin(p[0]) + tf.sin(a) * ( - tf.cos(p[2]) * tf.sin(p[1]) + tf.cos(p[0]) * tf.cos(p[1]) * tf.sin(p[2]) - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, p) - expected = np.array( - [ - tf.cos(p[1]) * (tf.cos(a) * tf.cos(p[0]) - tf.sin(a) * tf.sin(p[0]) * tf.sin(p[2])), - tf.cos(p[1]) * tf.cos(p[2]) * tf.sin(a) - - tf.sin(p[1]) - * (tf.cos(a) * tf.sin(p[0]) + tf.cos(p[0]) * tf.sin(a) * tf.sin(p[2])), - tf.sin(a) - * (tf.cos(p[0]) * tf.cos(p[1]) * tf.cos(p[2]) - tf.sin(p[1]) * tf.sin(p[2])), - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_probability_differentiation(self, tol): - """Tests correct output shape and evaluation for a tape - with multiple prob outputs""" - - dev = qml.device("default.qubit", wires=2) - x = tf.Variable(0.543, dtype=tf.float64) - y = tf.Variable(-0.654, dtype=tf.float64) - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape()) as qtape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=[0]) - qml.probs(wires=[1]) - - res = qtape.execute(dev) - - expected = np.array( - [ - [tf.cos(x / 2) ** 2, tf.sin(x / 2) ** 2], - [(1 + tf.cos(x) * tf.cos(y)) / 2, (1 - tf.cos(x) * tf.cos(y)) / 2], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, [x, y]) - expected = np.array( - [ - [ - [-tf.sin(x) / 2, tf.sin(x) / 2], - [-tf.sin(x) * tf.cos(y) / 2, tf.cos(y) * tf.sin(x) / 2], - ], - [ - [0, 0], - [-tf.cos(x) * tf.sin(y) / 2, tf.cos(x) * tf.sin(y) / 2], - ], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_ragged_differentiation(self, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - dev = qml.device("default.qubit", wires=2) - x = tf.Variable(0.543, dtype=tf.float64) - y = tf.Variable(-0.654, dtype=tf.float64) - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape()) as qtape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.probs(wires=[1]) - - res = qtape.execute(dev) - - expected = np.array( - [tf.cos(x), (1 + tf.cos(x) * tf.cos(y)) / 2, (1 - tf.cos(x) * tf.cos(y)) / 2] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, [x, y]) - expected = np.array( - [ - [-tf.sin(x), -tf.sin(x) * tf.cos(y) / 2, tf.cos(y) * tf.sin(x) / 2], - [0, -tf.cos(x) * tf.sin(y) / 2, tf.cos(x) * tf.sin(y) / 2], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_sampling(self): - """Test sampling works as expected""" - dev = qml.device("default.qubit", wires=2, shots=10) - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape()) as qtape: - qml.Hadamard(wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.sample(qml.PauliZ(0)) - qml.sample(qml.PauliX(1)) - - res = qtape.execute(dev) - - assert res.shape == (2, 10) - assert isinstance(res, tf.Tensor) - - -class TestTFPassthru: - """Test that the quantum tape works with a TF passthru - device. - - These tests are very similar to the tests above, with three key differences: - - * We do **not** apply the TF interface. These tapes simply use passthru - backprop, no custom gradient registration needed. - - * We do not test the trainable_params attribute. Since these tapes have no - TF interface, the tape does not need to bookkeep which parameters - are trainable; this is done by TF internally. - - * We use mock.spy to ensure that the tape's Jacobian method is not being called. - """ - - def test_execution(self): - """Test execution""" - a = tf.Variable(0.1) - dev = qml.device("default.qubit.tf", wires=1) - - with tf.GradientTape() as tape: - with JacobianTape() as qtape: - qml.RY(a, wires=0) - qml.RX(0.2, wires=0) - qml.expval(qml.PauliZ(0)) - - res = qtape.execute(dev) - - assert isinstance(res, tf.Tensor) - assert res.shape == (1,) - - def test_jacobian(self, mocker, tol): - """Test jacobian calculation""" - spy = mocker.spy(JacobianTape, "jacobian") - a = tf.Variable(0.1, dtype=tf.float64) - b = tf.Variable(0.2, dtype=tf.float64) - - dev = qml.device("default.qubit.tf", wires=2) - - with tf.GradientTape() as tape: - with JacobianTape() as qtape: - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - res = qtape.execute(dev) - - assert isinstance(res, tf.Tensor) - assert res.shape == (2,) - - expected = [tf.cos(a), -tf.cos(a) * tf.sin(b)] - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, [a, b]) - expected = [[-tf.sin(a), tf.sin(a) * tf.sin(b)], [0, -tf.cos(a) * tf.cos(b)]] - assert np.allclose(res, expected, atol=tol, rtol=0) - - spy.assert_not_called() - - @pytest.mark.slow - def test_reusing_quantum_tape(self, mocker, tol): - """Test re-using a quantum tape by passing new parameters""" - spy = mocker.spy(JacobianTape, "jacobian") - - a = tf.Variable(0.1, dtype=tf.float64) - b = tf.Variable(0.2, dtype=tf.float64) - - dev = qml.device("default.qubit.tf", wires=2) - - with tf.GradientTape() as tape: - - with JacobianTape() as qtape: - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - res = qtape.execute(dev) - - jac = tape.jacobian(res, [a, b]) - - a = tf.Variable(0.54, dtype=tf.float64) - b = tf.Variable(0.8, dtype=tf.float64) - - with tf.GradientTape() as tape: - res2 = qtape.execute(dev, params=[2 * a, b]) - - expected = [tf.cos(2 * a), -tf.cos(2 * a) * tf.sin(b)] - assert np.allclose(res2, expected, atol=tol, rtol=0) - - jac = tape.jacobian(res2, [a, b]) - expected = [ - [-2 * tf.sin(2 * a), 2 * tf.sin(2 * a) * tf.sin(b)], - [0, -tf.cos(2 * a) * tf.cos(b)], - ] - assert np.allclose(jac, expected, atol=tol, rtol=0) - - spy.assert_not_called() - - def test_classical_processing(self, mocker, tol): - """Test classical processing within the quantum tape""" - spy = mocker.spy(JacobianTape, "jacobian") - - a = tf.Variable(0.1, dtype=tf.float64) - b = tf.constant(0.2, dtype=tf.float64) - c = tf.Variable(0.3, dtype=tf.float64) - - dev = qml.device("default.qubit.tf", wires=1) - - with tf.GradientTape() as tape: - with JacobianTape() as qtape: - qml.RY(a * c, wires=0) - qml.RZ(b, wires=0) - qml.RX(c + c**2 + tf.sin(a), wires=0) - qml.expval(qml.PauliZ(0)) - - assert qtape.get_parameters() == [a * c, b, c + c**2 + tf.sin(a)] - res = qtape.execute(dev) - - res = tape.jacobian(res, [a, b, c]) - assert isinstance(res[0], tf.Tensor) - assert res[1] is None - assert isinstance(res[2], tf.Tensor) - - spy.assert_not_called() - - def test_no_trainable_parameters(self, mocker, tol): - """Test evaluation if there are no trainable parameters""" - spy = mocker.spy(JacobianTape, "jacobian") - dev = qml.device("default.qubit.tf", wires=2) - - with tf.GradientTape() as tape: - with JacobianTape() as qtape: - qml.RY(0.2, wires=0) - qml.RX(tf.constant(0.1), wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliZ(1)) - - res = qtape.execute(dev) - - assert res.shape == (2,) - assert isinstance(res, tf.Tensor) - spy.assert_not_called() - - @pytest.mark.parametrize("U", [tf.constant([[0, 1], [1, 0]]), np.array([[0, 1], [1, 0]])]) - def test_matrix_parameter(self, U, mocker, tol): - """Test that the TF interface works correctly - with a matrix parameter""" - spy = mocker.spy(JacobianTape, "jacobian") - a = tf.Variable(0.1, dtype=tf.float64) - - dev = qml.device("default.qubit.tf", wires=2) - - with tf.GradientTape() as tape: - with JacobianTape() as qtape: - qml.QubitUnitary(U, wires=0) - qml.RY(a, wires=0) - qml.expval(qml.PauliZ(0)) - res = qtape.execute(dev) - - assert np.allclose(res, -tf.cos(a), atol=tol, rtol=0) - - res = tape.jacobian(res, a) - assert np.allclose(res, tf.sin(a), atol=tol, rtol=0) - spy.assert_not_called() - - def test_differentiable_expand(self, mocker, tol): - """Test that operation and nested tapes expansion - is differentiable""" - spy = mocker.spy(JacobianTape, "jacobian") - - class U3(qml.U3): - def expand(self): - tape = JacobianTape() - theta, phi, lam = self.data - wires = self.wires - tape._ops += [ - qml.Rot(lam, theta, -lam, wires=wires), - qml.PhaseShift(phi + lam, wires=wires), - ] - return tape - - qtape = JacobianTape() - - dev = qml.device("default.qubit.tf", wires=1) - a = np.array(0.1) - p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float64) - - with tf.GradientTape() as tape: - - with qtape: - qml.RX(a, wires=0) - U3(p[0], p[1], p[2], wires=0) - qml.expval(qml.PauliX(0)) - - qtape = qtape.expand() - - assert [i.name for i in qtape.operations] == ["RX", "Rot", "PhaseShift"] - assert np.all(qtape.get_parameters() == [a, p[2], p[0], -p[2], p[1] + p[2]]) - - res = qtape.execute(device=dev) - - expected = tf.cos(a) * tf.cos(p[1]) * tf.sin(p[0]) + tf.sin(a) * ( - tf.cos(p[2]) * tf.sin(p[1]) + tf.cos(p[0]) * tf.cos(p[1]) * tf.sin(p[2]) - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, p) - expected = np.array( - [ - tf.cos(p[1]) * (tf.cos(a) * tf.cos(p[0]) - tf.sin(a) * tf.sin(p[0]) * tf.sin(p[2])), - tf.cos(p[1]) * tf.cos(p[2]) * tf.sin(a) - - tf.sin(p[1]) - * (tf.cos(a) * tf.sin(p[0]) + tf.cos(p[0]) * tf.sin(a) * tf.sin(p[2])), - tf.sin(a) - * (tf.cos(p[0]) * tf.cos(p[1]) * tf.cos(p[2]) - tf.sin(p[1]) * tf.sin(p[2])), - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - spy.assert_not_called() - - def test_probability_differentiation(self, tol): - """Tests correct output shape and evaluation for a tape - with multiple prob outputs""" - - dev = qml.device("default.qubit.tf", wires=2) - x = tf.Variable(0.543, dtype=tf.float64) - y = tf.Variable(-0.654, dtype=tf.float64) - - with tf.GradientTape() as tape: - with JacobianTape() as qtape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=[0]) - qml.probs(wires=[1]) - - res = qtape.execute(dev) - - expected = np.array( - [ - [tf.cos(x / 2) ** 2, tf.sin(x / 2) ** 2], - [(1 + tf.cos(x) * tf.cos(y)) / 2, (1 - tf.cos(x) * tf.cos(y)) / 2], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, [x, y]) - expected = np.array( - [ - [ - [-tf.sin(x) / 2, tf.sin(x) / 2], - [-tf.sin(x) * tf.cos(y) / 2, tf.cos(y) * tf.sin(x) / 2], - ], - [ - [0, 0], - [-tf.cos(x) * tf.sin(y) / 2, tf.cos(x) * tf.sin(y) / 2], - ], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_ragged_differentiation(self, monkeypatch, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - dev = qml.device("default.qubit.tf", wires=2) - x = tf.Variable(0.543, dtype=tf.float64) - y = tf.Variable(-0.654, dtype=tf.float64) - - def _asarray(args, dtype=tf.float64): - res = [tf.reshape(i, [-1]) for i in args] - res = tf.concat(res, axis=0) - return tf.cast(res, dtype=dtype) - - # The current DefaultQubitTF device provides an _asarray method that does - # not work correctly for ragged arrays. For ragged arrays, we would like _asarray to - # flatten the array. Here, we patch the _asarray method on the device to achieve this - # behaviour; once the tape has moved from the beta folder, we should implement - # this change directly in the device. - monkeypatch.setattr(dev, "_asarray", _asarray) - - with tf.GradientTape() as tape: - with JacobianTape() as qtape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.probs(wires=[1]) - - res = qtape.execute(dev) - - expected = np.array( - [tf.cos(x), (1 + tf.cos(x) * tf.cos(y)) / 2, (1 - tf.cos(x) * tf.cos(y)) / 2] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - res = tape.jacobian(res, [x, y]) - expected = np.array( - [ - [-tf.sin(x), -tf.sin(x) * tf.cos(y) / 2, tf.cos(y) * tf.sin(x) / 2], - [0, -tf.cos(x) * tf.sin(y) / 2, tf.cos(x) * tf.sin(y) / 2], - ] - ) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_sampling(self): - """Test sampling works as expected""" - dev = qml.device("default.qubit.tf", wires=2, shots=10) - - with tf.GradientTape() as tape: - with JacobianTape() as qtape: - qml.Hadamard(wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.sample(qml.PauliZ(0)) - qml.sample(qml.PauliX(1)) - - res = qtape.execute(dev) - - assert res.shape == (2, 10) - assert isinstance(res, tf.Tensor) diff --git a/tests/interfaces/test_tape_torch.py b/tests/interfaces/test_tape_torch.py deleted file mode 100644 index 259b7f3662d..00000000000 --- a/tests/interfaces/test_tape_torch.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the torch interface""" -import pytest - -torch = pytest.importorskip("torch", minversion="1.8.0") - -import numpy as np - -import pennylane as qml -from pennylane.tape import JacobianTape -from pennylane.interfaces.torch import TorchInterface - - -class TestTorchQuantumTape: - """Test the Torch interface applied to a tape""" - - def test_interface_construction(self): - """Test that the interface is correctly applied""" - with TorchInterface.apply(JacobianTape()) as tape: - qml.RX(0.5, wires=0) - qml.expval(qml.PauliX(0)) - - assert tape.interface == "torch" - assert isinstance(tape, TorchInterface) - assert tape.__bare__ == JacobianTape - - def test_repeated_interface_construction(self): - """Test that the interface is correctly applied multiple times""" - with TorchInterface.apply(JacobianTape()) as tape: - qml.RX(0.5, wires=0) - qml.expval(qml.PauliX(0)) - - assert tape.interface == "torch" - assert isinstance(tape, TorchInterface) - assert tape.__bare__ == JacobianTape - - TorchInterface.apply(tape, dtype=torch.float32) - assert tape.interface == "torch" - assert isinstance(tape, TorchInterface) - assert tape.__bare__ == JacobianTape - assert tape.dtype is torch.float32 - - def test_get_parameters(self): - """Test that the get parameters function correctly sets and returns the - trainable parameters""" - a = torch.tensor(0.1, requires_grad=True) - b = torch.tensor(0.2) - c = torch.tensor(0.3, requires_grad=True) - d = 0.4 - - with TorchInterface.apply(JacobianTape()) as tape: - qml.Rot(a, b, c, wires=0) - qml.RX(d, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliX(0)) - - assert tape.trainable_params == [0, 2] - assert np.all(tape.get_parameters() == [a, c]) - - def test_execution(self): - """Test execution""" - a = torch.tensor(0.1, requires_grad=True) - dev = qml.device("default.qubit", wires=1) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.RX(torch.tensor(0.2), wires=0) - qml.expval(qml.PauliZ(0)) - - assert tape.trainable_params == [0] - res = tape.execute(dev) - - assert isinstance(res, torch.Tensor) - assert res.shape == (1,) - - def test_execution_on_tf_device(self): - """Test execution on a TF device""" - tf = pytest.importorskip("tensorflow") - - a = torch.tensor(0.1, requires_grad=True) - dev = qml.device("default.qubit.tf", wires=1) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.RX(torch.tensor(0.2), wires=0) - qml.expval(qml.PauliZ(0)) - - assert tape.trainable_params == [0] - res = tape.execute(dev) - - assert isinstance(res, torch.Tensor) - assert res.shape == (1,) - - def test_jacobian(self, mocker, tol): - """Test jacobian calculation""" - spy = mocker.spy(JacobianTape, "jacobian") - - a_val = 0.1 - b_val = 0.2 - - a = torch.tensor(a_val, requires_grad=True) - b = torch.tensor(b_val, requires_grad=True) - - dev = qml.device("default.qubit", wires=2) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.RZ(torch.tensor(0.543), wires=0) - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - assert tape.trainable_params == [1, 2] - res = tape.execute(dev) - - assert isinstance(res, torch.Tensor) - assert res.shape == (2,) - - expected = [np.cos(a_val), -np.cos(a_val) * np.sin(b_val)] - assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - - loss = torch.sum(res) - - loss.backward() - expected = [-np.sin(a_val) + np.sin(a_val) * np.sin(b_val), -np.cos(a_val) * np.cos(b_val)] - assert np.allclose(a.grad, expected[0], atol=tol, rtol=0) - assert np.allclose(b.grad, expected[1], atol=tol, rtol=0) - - spy.assert_called() - - def test_jacobian_options(self, mocker, tol): - """Test setting jacobian options""" - spy = mocker.spy(JacobianTape, "numeric_pd") - - a = torch.tensor([0.1, 0.2], requires_grad=True) - - dev = qml.device("default.qubit", wires=1) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.RY(a[0], wires=0) - qml.RX(a[1], wires=0) - qml.expval(qml.PauliZ(0)) - - tape.jacobian_options = {"h": 1e-8, "order": 2} - res = tape.execute(dev) - res.backward() - - for args in spy.call_args_list: - assert args[1]["order"] == 2 - assert args[1]["h"] == 1e-8 - - def test_jacobian_dtype(self, tol): - """Test calculating the jacobian with a different datatype""" - a_val = 0.1 - b_val = 0.2 - - a = torch.tensor(a_val, requires_grad=True, dtype=torch.float32) - b = torch.tensor(b_val, requires_grad=True, dtype=torch.float32) - - dev = qml.device("default.qubit", wires=2) - - with TorchInterface.apply(JacobianTape(), dtype=torch.float32) as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - assert tape.trainable_params == [0, 1] - res = tape.execute(dev) - - assert isinstance(res, torch.Tensor) - assert res.shape == (2,) - assert res.dtype is torch.float32 - - loss = torch.sum(res) - loss.backward() - assert a.grad.dtype is torch.float32 - assert b.grad.dtype is torch.float32 - - def test_reusing_quantum_tape(self, tol): - """Test re-using a quantum tape by passing new parameters""" - a = torch.tensor(0.1, requires_grad=True) - b = torch.tensor(0.2, requires_grad=True) - - dev = qml.device("default.qubit", wires=2) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.RY(a, wires=0) - qml.RX(b, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliY(1)) - - assert tape.trainable_params == [0, 1] - - loss = torch.sum(tape.execute(dev)) - loss.backward() - - a_val = 0.54 - b_val = 0.8 - a = torch.tensor(a_val, requires_grad=True) - b = torch.tensor(b_val, requires_grad=True) - res2 = tape.execute(dev, params=[2 * a, b]) - - expected = [np.cos(2 * a_val), -np.cos(2 * a_val) * np.sin(b_val)] - assert np.allclose(res2.detach().numpy(), expected, atol=tol, rtol=0) - - loss = torch.sum(res2) - loss.backward() - - expected = [ - -2 * np.sin(2 * a_val) + 2 * np.sin(2 * a_val) * np.sin(b_val), - -np.cos(2 * a_val) * np.cos(b_val), - ] - - assert np.allclose(a.grad, expected[0], atol=tol, rtol=0) - assert np.allclose(b.grad, expected[1], atol=tol, rtol=0) - - def test_classical_processing(self, tol): - """Test classical processing within the quantum tape""" - p_val = [0.1, 0.2] - params = torch.tensor(p_val, requires_grad=True) - - dev = qml.device("default.qubit", wires=1) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.RY(params[0] * params[1], wires=0) - qml.RZ(0.2, wires=0) - qml.RX(params[1] + params[1] ** 2 + torch.sin(params[0]), wires=0) - qml.expval(qml.PauliZ(0)) - - assert tape.trainable_params == [0, 2] - - tape_params = [i.detach().numpy() for i in tape.get_parameters()] - assert np.allclose( - tape_params, - [p_val[0] * p_val[1], p_val[1] + p_val[1] ** 2 + np.sin(p_val[0])], - atol=tol, - rtol=0, - ) - - res = tape.execute(dev) - res.backward() - - assert isinstance(params.grad, torch.Tensor) - assert params.shape == (2,) - - def test_no_trainable_parameters(self, tol): - """Test evaluation and Jacobian if there are no trainable parameters""" - dev = qml.device("default.qubit", wires=2) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.RY(0.2, wires=0) - qml.RX(torch.tensor(0.1), wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliZ(1)) - - assert tape.trainable_params == [] - - res = tape.execute(dev) - - assert res.shape == (2,) - assert isinstance(res, torch.Tensor) - - with pytest.raises( - RuntimeError, - match="element 0 of tensors does not require grad and does not have a grad_fn", - ): - res.backward() - - @pytest.mark.parametrize("U", [torch.tensor([[0, 1], [1, 0]]), np.array([[0, 1], [1, 0]])]) - def test_matrix_parameter(self, U, tol): - """Test that the Torch interface works correctly - with a matrix parameter""" - a_val = 0.1 - a = torch.tensor(a_val, requires_grad=True) - - dev = qml.device("default.qubit", wires=2) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.QubitUnitary(U, wires=0) - qml.RY(a, wires=0) - qml.expval(qml.PauliZ(0)) - - assert tape.trainable_params == [1] - res = tape.execute(dev) - - assert np.allclose(res.detach().numpy(), -np.cos(a_val), atol=tol, rtol=0) - - res.backward() - assert np.allclose(a.grad, np.sin(a_val), atol=tol, rtol=0) - - def test_differentiable_expand(self, tol): - """Test that operation and nested tapes expansion - is differentiable""" - - class U3(qml.U3): - def expand(self): - tape = JacobianTape() - theta, phi, lam = self.data - wires = self.wires - tape._ops += [ - qml.Rot(lam, theta, -lam, wires=wires), - qml.PhaseShift(phi + lam, wires=wires), - ] - return tape - - tape = JacobianTape() - - dev = qml.device("default.qubit", wires=1) - a = np.array(0.1) - p_val = [0.1, 0.2, 0.3] - p = torch.tensor(p_val, requires_grad=True) - - with tape: - qml.RX(a, wires=0) - U3(p[0], p[1], p[2], wires=0) - qml.expval(qml.PauliX(0)) - - tape = TorchInterface.apply(tape.expand()) - - assert tape.trainable_params == [1, 2, 3, 4] - assert [i.name for i in tape.operations] == ["RX", "Rot", "PhaseShift"] - - tape_params = [i.detach().numpy() for i in tape.get_parameters()] - assert np.allclose( - tape_params, [p_val[2], p_val[0], -p_val[2], p_val[1] + p_val[2]], atol=tol, rtol=0 - ) - - res = tape.execute(device=dev) - - expected = np.cos(a) * np.cos(p_val[1]) * np.sin(p_val[0]) + np.sin(a) * ( - np.cos(p_val[2]) * np.sin(p_val[1]) - + np.cos(p_val[0]) * np.cos(p_val[1]) * np.sin(p_val[2]) - ) - assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - - res.backward() - expected = np.array( - [ - np.cos(p_val[1]) - * (np.cos(a) * np.cos(p_val[0]) - np.sin(a) * np.sin(p_val[0]) * np.sin(p_val[2])), - np.cos(p_val[1]) * np.cos(p_val[2]) * np.sin(a) - - np.sin(p_val[1]) - * (np.cos(a) * np.sin(p_val[0]) + np.cos(p_val[0]) * np.sin(a) * np.sin(p_val[2])), - np.sin(a) - * ( - np.cos(p_val[0]) * np.cos(p_val[1]) * np.cos(p_val[2]) - - np.sin(p_val[1]) * np.sin(p_val[2]) - ), - ] - ) - assert np.allclose(p.grad, expected, atol=tol, rtol=0) - - def test_probability_differentiation(self, tol): - """Tests correct output shape and evaluation for a tape - with multiple prob outputs""" - - dev = qml.device("default.qubit", wires=2) - x_val = 0.543 - y_val = -0.654 - x = torch.tensor(x_val, requires_grad=True) - y = torch.tensor(y_val, requires_grad=True) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=[0]) - qml.probs(wires=[1]) - - res = tape.execute(dev) - - expected = np.array( - [ - [np.cos(x_val / 2) ** 2, np.sin(x_val / 2) ** 2], - [(1 + np.cos(x_val) * np.cos(y_val)) / 2, (1 - np.cos(x_val) * np.cos(y_val)) / 2], - ] - ) - assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - - loss = torch.sum(res) - loss.backward() - expected = np.array( - [ - -np.sin(x_val) / 2 - + np.sin(x_val) / 2 - - np.sin(x_val) * np.cos(y_val) / 2 - + np.cos(y_val) * np.sin(x_val) / 2, - -np.cos(x_val) * np.sin(y_val) / 2 + np.cos(x_val) * np.sin(y_val) / 2, - ] - ) - assert np.allclose(x.grad, expected[0], atol=tol, rtol=0) - assert np.allclose(y.grad, expected[1], atol=tol, rtol=0) - - def test_ragged_differentiation(self, tol): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - dev = qml.device("default.qubit", wires=2) - x_val = 0.543 - y_val = -0.654 - x = torch.tensor(x_val, requires_grad=True) - y = torch.tensor(y_val, requires_grad=True) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.probs(wires=[1]) - - res = tape.execute(dev) - - expected = np.array( - [ - np.cos(x_val), - (1 + np.cos(x_val) * np.cos(y_val)) / 2, - (1 - np.cos(x_val) * np.cos(y_val)) / 2, - ] - ) - assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - - loss = torch.sum(res) - loss.backward() - expected = np.array( - [ - -np.sin(x_val) - + -np.sin(x_val) * np.cos(y_val) / 2 - + np.cos(y_val) * np.sin(x_val) / 2, - -np.cos(x_val) * np.sin(y_val) / 2 + np.cos(x_val) * np.sin(y_val) / 2, - ] - ) - assert np.allclose(x.grad, expected[0], atol=tol, rtol=0) - assert np.allclose(y.grad, expected[1], atol=tol, rtol=0) - - def test_sampling(self): - """Test sampling works as expected""" - dev = qml.device("default.qubit", wires=2, shots=10) - - with TorchInterface.apply(JacobianTape()) as tape: - qml.Hadamard(wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.sample(qml.PauliZ(0)) - qml.sample(qml.PauliX(1)) - - res = tape.execute(dev) - - assert res.shape == (2, 10) - assert isinstance(res, torch.Tensor) - - def test_complex_min_version(self, monkeypatch): - """Test if an error is raised when a version of torch before 1.8.0 is used as the dtype - in the apply() method""" - - with monkeypatch.context() as m: - m.setattr(qml.interfaces.torch, "COMPLEX_SUPPORT", False) - with pytest.raises( - qml.QuantumFunctionError, match=r"Version 1\.8\.0 or above of PyTorch" - ): - TorchInterface.apply(JacobianTape(), dtype=torch.complex128) - - def test_repeated_application_after_expand(self, tol): - """Test that the Torch interface continues to work after - tape expansions, and repeated torch application""" - n_qubits = 2 - dev = qml.device("default.qubit", wires=n_qubits) - - weights = torch.ones((3,)) - - with TorchInterface.apply(qml.tape.QuantumTape()) as tape: - qml.U3(*weights, wires=0) - qml.expval(qml.PauliZ(wires=0)) - - tape = tape.expand() - - res1 = tape.execute(dev) - - TorchInterface.apply(tape) - res2 = tape.execute(dev) - - assert np.allclose(res1, res2, atol=tol, rtol=0) From 9078ed40455bcf100987987250526c32cdeee745 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 13:43:26 +0800 Subject: [PATCH 03/13] remove interfaces and test fixes --- pennylane/interfaces/autograd.py | 302 ----------------------------- pennylane/interfaces/jax.py | 141 -------------- pennylane/interfaces/tf.py | 262 -------------------------- pennylane/interfaces/torch.py | 314 ------------------------------- pennylane/qnn/keras.py | 1 - pennylane/tape/tape.py | 12 +- tests/tape/test_tape.py | 22 --- tests/transforms/test_specs.py | 149 +-------------- 8 files changed, 4 insertions(+), 1199 deletions(-) delete mode 100644 pennylane/interfaces/autograd.py delete mode 100644 pennylane/interfaces/jax.py delete mode 100644 pennylane/interfaces/tf.py delete mode 100644 pennylane/interfaces/torch.py diff --git a/pennylane/interfaces/autograd.py b/pennylane/interfaces/autograd.py deleted file mode 100644 index 5edd5bf1573..00000000000 --- a/pennylane/interfaces/autograd.py +++ /dev/null @@ -1,302 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the mixin interface class for creating differentiable quantum tapes with -Autograd. -""" -# pylint: disable=protected-access -import autograd.extend -import autograd.builtins -from autograd.numpy.numpy_boxes import ArrayBox - -from pennylane import numpy as np -from pennylane.queuing import AnnotatedQueue - - -class AutogradInterface(AnnotatedQueue): - """Mixin class for applying an autograd interface to a :class:`~.JacobianTape`. - - Autograd-compatible quantum tape classes can be created via subclassing: - - .. code-block:: python - - class MyAutogradQuantumTape(AutogradInterface, JacobianTape): - - Alternatively, the autograd interface can be dynamically applied to existing - quantum tapes via the :meth:`~.apply` class method. This modifies the - tape **in place**. - - Once created, the autograd interface can be used to perform quantum-classical - differentiable programming. - - .. note:: - - If using a device that supports native autograd computation and backpropagation, such as - :class:`~.DefaultQubitAutograd`, the Autograd interface **does not need to be applied**. It - is only applied to tapes executed on non-Autograd compatible devices. - - **Example** - - Once an autograd quantum tape has been created, it can be differentiated using autograd: - - .. code-block:: python - - tape = AutogradInterface.apply(JacobianTape()) - - with tape: - qml.Rot(0, 0, 0, wires=0) - expval(qml.PauliX(0)) - - def cost_fn(x, y, z, device): - tape.set_parameters([x, y ** 2, y * np.sin(z)], trainable_only=False) - return tape.execute(device=device) - - >>> x = np.array(0.1, requires_grad=False) - >>> y = np.array(0.2, requires_grad=True) - >>> z = np.array(0.3, requires_grad=True) - >>> dev = qml.device("default.qubit", wires=2) - >>> cost_fn(x, y, z, device=dev) - [0.03991951] - >>> jac_fn = qml.jacobian(cost_fn) - >>> jac_fn(x, y, z, device=dev) - [[ 0.39828408, -0.00045133]] - """ - - # pylint: disable=attribute-defined-outside-init - dtype = np.float64 - - @property - def interface(self): # pylint: disable=missing-function-docstring - return "autograd" - - def _update_trainable_params(self): - """Set the trainable parameters. - - Unlike in :class:`~.JacobianTape`, we also set the private attribute - ``self._all_parameter_values``. - """ - params = self.get_parameters(trainable_only=False, return_arraybox=True) - trainable_params = set() - - for idx, p in enumerate(params): - if getattr(p, "requires_grad", False) or isinstance(p, ArrayBox): - trainable_params.add(idx) - - self.trainable_params = trainable_params - self._all_parameter_values = params - - def get_parameters(self, trainable_only=True, return_arraybox=False): - """Return the parameters incident on the tape operations. - - The returned parameters are provided in order of appearance - on the tape. By default, the returned parameters are wrapped in - an ``autograd.builtins.list`` container. - - Args: - trainable_only (bool): if True, returns only trainable parameters - return_arraybox (bool): if True, the returned parameters are not - wrapped in an ``autograd.builtins.list`` container - Returns: - autograd.builtins.list or list: the corresponding parameter values - - **Example** - - .. code-block:: python - - with JacobianTape() as tape: - qml.RX(0.432, wires=0) - qml.RY(0.543, wires=0) - qml.CNOT(wires=[0, 'a']) - qml.RX(0.133, wires='a') - qml.expval(qml.PauliZ(wires=[0])) - - By default, all parameters are trainable and will be returned: - - >>> tape.get_parameters() - [0.432, 0.543, 0.133] - - Setting the trainable parameter indices will result in only the specified - parameters being returned: - - >>> tape.trainable_params = {1} # set the second parameter as free - >>> tape.get_parameters() - [0.543] - - The ``trainable_only`` argument can be set to ``False`` to instead return - all parameters: - - >>> tape.get_parameters(trainable_only=False) - [0.432, 0.543, 0.133] - """ - params = [] - iterator = self.trainable_params if trainable_only else self._par_info - - for p_idx in iterator: - op = self._par_info[p_idx]["op"] - op_idx = self._par_info[p_idx]["p_idx"] - params.append(op.data[op_idx]) - - return params if return_arraybox else autograd.builtins.list(params) - - @autograd.extend.primitive - def _execute(self, params, device): - # unwrap all NumPy scalar arrays to Python literals - params = [p.item() if p.shape == tuple() else p for p in params] - params = autograd.builtins.tuple(params) - - # unwrap constant parameters - self._all_params_unwrapped = [ - p.numpy() if isinstance(p, np.tensor) else p for p in self._all_parameter_values - ] - - # evaluate the tape - self.set_parameters(self._all_params_unwrapped, trainable_only=False) - res = self.execute_device(params, device=device) - self.set_parameters(self._all_parameter_values, trainable_only=False) - - if self.is_sampled: - return res - - if res.dtype == np.dtype("object"): - return np.hstack(res) - - requires_grad = False - - if self.trainable_params: - requires_grad = True - - return np.array(res, requires_grad=requires_grad) - - @staticmethod - def vjp(ans, self, params, device): # pylint: disable=unused-argument - """Returns the vector-Jacobian product operator for the quantum tape. - The returned function takes the arguments as :meth:`~.JacobianTape.execute`. - - Args: - ans (array): the result of the tape execution - self (.AutogradQuantumTape): the tape instance - params (list[Any]): the quantum tape operation parameters - device (.Device): a PennyLane device that can execute quantum - operations and return measurement statistics - - Returns: - function: this function accepts the backpropagation - gradient output vector, and computes the vector-Jacobian product - """ - - # The following dictionary caches the Jacobian and Hessian matrices, - # so that they can be re-used for different vjp/vhp computations - # within the same backpropagation call. - # This dictionary will exist in memory when autograd.grad is called, - # via closure. Once autograd.grad has returned, this dictionary - # will no longer be in scope and the memory will be freed. - saved_grad_matrices = {} - - def _evaluate_grad_matrix(p, grad_matrix_fn): - """Convenience function for generating gradient matrices - for the given parameter values. - - This function serves two purposes: - - * Avoids duplicating logic surrounding parameter unwrapping/wrapping. - - * Takes advantage of closure, to cache computed gradient matrices via - the ``saved_grad_matrices`` attribute, to avoid gradient matrices being - computed multiple redundant times. - - This is particularly useful when differentiating vector-valued QNodes. - Because Autograd requests the vector-grad matrix product, - and *not* the full grad matrix, differentiating vector-valued - functions will result in multiple backward passes. - - Args: - p (Sequence): quantum tape parameter values use to evaluate the gradient matrix - grad_matrix_fn (str): Name of the gradient matrix function. Should correspond to an existing - tape method. Currently allowed values include ``"jacobian"`` and ``"hessian"``. - - Returns: - array[float]: the gradient matrix - """ - if grad_matrix_fn in saved_grad_matrices: - return saved_grad_matrices[grad_matrix_fn] - - self.set_parameters(self._all_params_unwrapped, trainable_only=False) - grad_matrix = getattr(self, grad_matrix_fn)(device, params=p, **self.jacobian_options) - self.set_parameters(self._all_parameter_values, trainable_only=False) - - saved_grad_matrices[grad_matrix_fn] = grad_matrix - return grad_matrix - - def gradient_product(dy): - """Returns the vector-Jacobian product with given - parameter values p and output gradient dy""" - - if all(np.ndim(p) == 0 for p in params): - # only flatten dy if all parameters are single values - dy = dy.flatten() - - @autograd.extend.primitive - def jacobian(p): - """Returns the Jacobian for parameters p""" - return _evaluate_grad_matrix(p, "jacobian") - - def vhp(ans, p): - def hessian_product(ddy): - """Returns the vector-Hessian product with given - parameter values p, output gradient dy, and output - second-order gradient ddy""" - hessian = _evaluate_grad_matrix(p, "hessian") - - if dy.size > 1: - vhp = dy @ ddy @ hessian @ dy.T / np.linalg.norm(dy) ** 2 - else: - vhp = ddy @ hessian - vhp = vhp.flatten() - - return vhp - - return hessian_product - - # register vhp as the backward method of the jacobian function - autograd.extend.defvjp(jacobian, vhp, argnums=[0]) - - vjp = dy @ jacobian(params) - return vjp - - return gradient_product - - @classmethod - def apply(cls, tape): - """Apply the autograd interface to an existing tape in-place. - - Args: - tape (.JacobianTape): a quantum tape to apply the Autograd interface to - - **Example** - - >>> with JacobianTape() as tape: - ... qml.RX(0.5, wires=0) - ... expval(qml.PauliZ(0)) - >>> AutogradInterface.apply(tape) - >>> tape - , params=1> - """ - tape_class = getattr(tape, "__bare__", tape.__class__) - tape.__bare__ = tape_class - tape.__class__ = type("AutogradQuantumTape", (cls, tape_class), {}) - tape._update_trainable_params() - return tape - - -autograd.extend.defvjp(AutogradInterface._execute, AutogradInterface.vjp, argnums=[1]) diff --git a/pennylane/interfaces/jax.py b/pennylane/interfaces/jax.py deleted file mode 100644 index c3c0dea905b..00000000000 --- a/pennylane/interfaces/jax.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the mixin interface class for creating differentiable quantum tapes with -JAX. -""" -from functools import partial -import jax -from jax.experimental import host_callback -import jax.numpy as jnp -from pennylane.queuing import AnnotatedQueue -from pennylane.operation import Variance, Expectation - - -class JAXInterface(AnnotatedQueue): - """Mixin class for applying an JAX interface to a :class:`~.JacobianTape`. - - JAX-compatible quantum tape classes can be created via subclassing: - - .. code-block:: python - - class MyJAXQuantumTape(JAXInterface, JacobianTape): - - Alternatively, the JAX interface can be dynamically applied to existing - quantum tapes via the :meth:`~.apply` class method. This modifies the - tape **in place**. - - Once created, the JAX interface can be used to perform quantum-classical - differentiable programming. - - .. note:: - - If using a device that supports native JAX computation and backpropagation, such as - :class:`~.DefaultQubitJAX`, the JAX interface **does not need to be applied**. It - is only applied to tapes executed on non-JAX compatible devices. - - **Example** - - Once a JAX quantum tape has been created, it can be differentiated using JAX: - - .. code-block:: python - - tape = JAXInterface.apply(JacobianTape()) - - with tape: - qml.Rot(0, 0, 0, wires=0) - expval(qml.PauliX(0)) - - def cost_fn(x, y, z, device): - tape.set_parameters([x, y ** 2, y * np.sin(z)], trainable_only=False) - return tape.execute(device=device) - - >>> x = jnp.array(0.1, requires_grad=False) - >>> y = jnp.array(0.2, requires_grad=True) - >>> z = jnp.array(0.3, requires_grad=True) - >>> dev = qml.device("default.qubit", wires=2) - >>> cost_fn(x, y, z, device=dev) - DeviceArray([ 0.03991951], dtype=float32) - >>> jac_fn = jax.vjp(cost_fn) - >>> jac_fn(x, y, z, device=dev) - DeviceArray([[ 0.39828408, -0.00045133]], dtype=float32) - """ - - # pylint: disable=attribute-defined-outside-init - dtype = jnp.float64 - - @property - def interface(self): # pylint: disable=missing-function-docstring - return "jax" - - def _execute(self, params, device): - # TODO (chase): Add support for more than 1 measured observable. - if len(self.observables) != 1: - raise ValueError( - "The JAX interface currently only supports quantum nodes with a single return type." - ) - return_type = self.observables[0].return_type - if return_type is not Variance and return_type is not Expectation: - raise ValueError( - f"Only Variance and Expectation returns are supported for the JAX interface, given {return_type}." - ) - - @jax.custom_vjp - def wrapped_exec(params): - exec_fn = partial(self.execute_device, device=device) - return host_callback.call( - exec_fn, - params, - result_shape=jax.ShapeDtypeStruct((1,), JAXInterface.dtype), - ) - - def wrapped_exec_fwd(params): - return wrapped_exec(params), params - - def wrapped_exec_bwd(params, g): - def jacobian(params): - tape = self.copy() - tape.set_parameters(params) - return tape.jacobian(device, params=params, **tape.jacobian_options) - - val = g.reshape((-1,)) * host_callback.call( - jacobian, - params, - result_shape=jax.ShapeDtypeStruct((1, len(params)), JAXInterface.dtype), - ) - return (list(val.reshape((-1,))),) # Comma is on purpose. - - wrapped_exec.defvjp(wrapped_exec_fwd, wrapped_exec_bwd) - return wrapped_exec(params) - - @classmethod - def apply(cls, tape): - """Apply the JAX interface to an existing tape in-place. - - Args: - tape (.JacobianTape): a quantum tape to apply the JAX interface to - - **Example** - - >>> with JacobianTape() as tape: - ... qml.RX(0.5, wires=0) - ... expval(qml.PauliZ(0)) - >>> JAXInterface.apply(tape) - >>> tape - , params=1> - """ - tape_class = getattr(tape, "__bare__", tape.__class__) - tape.__bare__ = tape_class - tape.__class__ = type("JAXQuantumTape", (cls, tape_class), {}) - return tape diff --git a/pennylane/interfaces/tf.py b/pennylane/interfaces/tf.py deleted file mode 100644 index 80cf885bc1a..00000000000 --- a/pennylane/interfaces/tf.py +++ /dev/null @@ -1,262 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the mixin interface class for creating differentiable quantum tapes with -TensorFlow. -""" -# pylint: disable=protected-access, attribute-defined-outside-init -import numpy as np -import tensorflow as tf - -try: - from tensorflow.python.eager.tape import should_record_backprop -except ImportError: - from tensorflow.python.eager.tape import should_record as should_record_backprop - - -from pennylane.queuing import AnnotatedQueue - - -class TFInterface(AnnotatedQueue): - """Mixin class for applying an TensorFlow interface to a :class:`~.JacobianTape`. - - TensorFlow-compatible quantum tape classes can be created via subclassing: - - .. code-block:: python - - class MyTFQuantumTape(TFInterface, JacobianTape): - - Alternatively, the TensorFlow interface can be dynamically applied to existing - quantum tapes via the :meth:`~.apply` class method. This modifies the - tape **in place**. - - Once created, the TensorFlow interface can be used to perform quantum-classical - differentiable programming. - - .. note:: - - If using a device that supports native TensorFlow computation and backpropagation, such as - :class:`~.DefaultQubitTF`, the TensorFlow interface **does not need to be applied**. It is - only applied to tapes executed on non-TensorFlow compatible devices. - - **Example** - - Once a TensorFlow quantum tape has been created, it can be differentiated using the gradient tape: - - .. code-block:: python - - dev = qml.device("default.qubit", wires=1) - p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float64) - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape()) as qtape: - qml.Rot(p[0], p[1] ** 2 + p[0] * p[2], p[1] * tf.sin(p[2]), wires=0) - expval(qml.PauliX(0)) - - result = qtape.execute(dev) - - >>> print(result) - tf.Tensor([0.06982072], shape=(1,), dtype=float64) - >>> grad = tape.gradient(result, p) - >>> print(grad) - tf.Tensor([0.29874274 0.39710271 0.09958091], shape=(3,), dtype=float64) - - The TensorFlow interface defaults to ``tf.float64`` output. This can be modified by - providing the ``dtype`` argument when applying the interface: - - >>> p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float32) - >>> with tf.GradientTape() as tape: - ... TFInterface.apply(qtape, dtype=tf.float32) # reusing the previous qtape - ... result = qtape.execute(dev) - >>> print(result) - tf.Tensor([0.06982072], shape=(1,), dtype=float32) - >>> grad = tape.gradient(result, p) - >>> print(grad) - tf.Tensor([0.2895088 0.38464668 0.09645163], shape=(3,), dtype=float32) - """ - - dtype = tf.float64 - - @property - def interface(self): # pylint: disable=missing-function-docstring - return "tf" - - def _update_trainable_params(self): - params = self.get_parameters(trainable_only=False) - - trainable_params = set() - - for idx, p in enumerate(params): - # Determine which input tensors/Variables are being recorded for backpropagation. - # The function should_record_backprop, documented here: - # https://github.com/tensorflow/tensorflow/tree/master/tensorflow/python/eager/tape.py#L167 - # accepts lists of *Tensors* (not Variables), returning True if all are being watched by one or more - # existing gradient tapes, False if not. - - if isinstance(p, (tf.Variable, tf.Tensor)) and should_record_backprop( - # we need to convert any Variable objects to Tensors here, otherwise - # should_record_backprop will raise an error - [tf.convert_to_tensor(p)] - ): - trainable_params.add(idx) - - self.trainable_params = trainable_params - - @staticmethod - def convert_to_numpy(tensors): - """Converts any TensorFlow tensors in a sequence to NumPy arrays. - - Args: - tensors (Sequence[Any, tf.Variable, tf.Tensor]): input sequence - - Returns: - list[Any, array]: list with all tensors converted to NumPy arrays - """ - return [i.numpy() if isinstance(i, (tf.Variable, tf.Tensor)) else i for i in tensors] - - @tf.custom_gradient - def _execute(self, params, **input_kwargs): - # unwrap free parameters - args = self.convert_to_numpy(params) - - # unwrap constant parameters - all_params = self.get_parameters(trainable_only=False) - all_params_unwrapped = self.convert_to_numpy(all_params) - - self.set_parameters(all_params_unwrapped, trainable_only=False) - res = self.execute_device(args, input_kwargs["device"]) - self.set_parameters(all_params, trainable_only=False) - - use_adjoint_cached_state = False - # tape might not be a jacobian tape - jac_options = getattr(self, "jacobian_options", {}) - # cache state for adjoint jacobian computation - if jac_options.get("jacobian_method", None) == "adjoint_jacobian": - if jac_options.get("adjoint_cache", True): - use_adjoint_cached_state = True - state = input_kwargs["device"]._pre_rotated_state - - # The following dictionary caches the Jacobian and Hessian matrices, - # so that they can be re-used for different vjp/vhp computations - # within the same backpropagation call. - # This dictionary is tied to an instance of the inner function jacobian_product - # called within tf_tape.gradient or tf_tape.jacobian, - # via closure. Once tf_tape.gradient/ jacobian has returned, the jacobian_product instance - # will no longer be in scope and the memory will be freed. - saved_grad_matrices = {} - - def _evaluate_grad_matrix(grad_matrix_fn): - """Convenience function for generating gradient matrices - for the given parameter values. - - This function serves two purposes: - - * Avoids duplicating logic surrounding parameter unwrapping/wrapping - - * Takes advantage of closure, to cache computed gradient matrices via - the ``saved_grad_matrices`` dictionary, to avoid gradient matrices being - computed multiple redundant times. - - This is particularly useful when differentiating vector-valued QNodes. - Because tensorflow requests the vector-grad matrix product, - and *not* the full grad matrix, differentiating vector-valued - functions will result in multiple backward passes. - - Args: - grad_matrix_fn (str): Name of the gradient matrix function. Should correspond to an existing - tape method. Currently allowed values include ``"jacobian"`` and ``"hessian"``. - - Returns: - array[float]: the gradient matrix - """ - if grad_matrix_fn in saved_grad_matrices: - return saved_grad_matrices[grad_matrix_fn] - - if use_adjoint_cached_state: - self.jacobian_options["device_pd_options"] = {"starting_state": state} - - self.set_parameters(all_params_unwrapped, trainable_only=False) - grad_matrix = getattr(self, grad_matrix_fn)( - input_kwargs["device"], params=args, **self.jacobian_options - ) - self.set_parameters(all_params, trainable_only=False) - - grad_matrix = tf.constant(grad_matrix, dtype=self.dtype) - saved_grad_matrices[grad_matrix_fn] = grad_matrix - - return grad_matrix - - def jacobian_product(dy, **tfkwargs): - variables = tfkwargs.get("variables", None) - dy_row = tf.reshape(dy, [1, -1]) - - @tf.custom_gradient - def jacobian(p): - def hessian_product(ddy, **tfkwargs): - variables = tfkwargs.get("variables", None) - hessian = _evaluate_grad_matrix("hessian") - - if self.output_dim == 1: - hessian = tf.expand_dims(hessian, -1) - - vhp = tf.cond( - tf.rank(hessian) > 2, - lambda: dy_row - @ ddy - @ hessian - @ tf.transpose(dy_row) - / tf.linalg.norm(dy_row) ** 2, - lambda: ddy @ hessian, - ) - - vhp = tf.unstack(tf.reshape(vhp, [-1])) - return (vhp, variables) if variables is not None else vhp - - return _evaluate_grad_matrix("jacobian"), hessian_product - - vjp = tf.matmul(dy_row, jacobian(params)) - vjp = tf.unstack(tf.reshape(vjp, [-1])) - return (vjp, variables) if variables is not None else vjp - - if self.is_sampled: - return res, jacobian_product - - if res.dtype == np.dtype("object"): - res = np.hstack(res) - - return tf.convert_to_tensor(res, dtype=self.dtype), jacobian_product - - @classmethod - def apply(cls, tape, dtype=tf.float64): - """Apply the TensorFlow interface to an existing tape in-place. - - Args: - tape (.JacobianTape): a quantum tape to apply the TF interface to - dtype (tf.dtype): the dtype that the returned quantum tape should - output - - **Example** - - >>> with JacobianTape() as tape: - ... qml.RX(0.5, wires=0) - ... expval(qml.PauliZ(0)) - >>> TFInterface.apply(tape) - >>> tape - , params=1> - """ - tape_class = getattr(tape, "__bare__", tape.__class__) - tape.__bare__ = tape_class - tape.__class__ = type("TFQuantumTape", (cls, tape_class), {"dtype": dtype}) - tape._update_trainable_params() - return tape diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py deleted file mode 100644 index ef8f2f4346c..00000000000 --- a/pennylane/interfaces/torch.py +++ /dev/null @@ -1,314 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the mixin interface class for creating differentiable quantum tapes with -PyTorch. -""" -# pylint: disable=protected-access, attribute-defined-outside-init, arguments-differ, no-member, import-self, too-many-statements -import numpy as np -import semantic_version -import torch - -import pennylane as qml -from pennylane.queuing import AnnotatedQueue - -COMPLEX_SUPPORT = semantic_version.match(">=1.8.0", torch.__version__) - - -def args_to_numpy(args): - """Converts all Torch tensors in a list to NumPy arrays - - Args: - args (list): list containing QNode arguments, including Torch tensors - - Returns: - list: returns the same list, with all Torch tensors converted to NumPy arrays - """ - res = [] - - for i in args: - if isinstance(i, torch.Tensor): - if i.is_cuda: # pragma: no cover - res.append(i.cpu().detach().numpy()) - else: - res.append(i.detach().numpy()) - else: - res.append(i) - - # if NumPy array is scalar, convert to a Python float - res = [i.tolist() if (isinstance(i, np.ndarray) and not i.shape) else i for i in res] - - return res - - -class _TorchInterface(torch.autograd.Function): - @staticmethod - def forward(ctx, input_kwargs, *input_): - """Implements the forward pass QNode evaluation""" - # detach all input tensors, convert to NumPy array - ctx.args = args_to_numpy(input_) - ctx.kwargs = input_kwargs - ctx.save_for_backward(*input_) - - tape = ctx.kwargs["tape"] - device = ctx.kwargs["device"] - - # unwrap constant parameters - ctx.all_params = tape.get_parameters(trainable_only=False) - ctx.all_params_unwrapped = args_to_numpy(ctx.all_params) - - # evaluate the tape - tape.set_parameters(ctx.all_params_unwrapped, trainable_only=False) - res = tape.execute_device(ctx.args, device) - tape.set_parameters(ctx.all_params, trainable_only=False) - - if hasattr(res, "numpy"): - res = qml.math.to_numpy(res) - - use_adjoint_cached_state = False - # tape might not be a jacobian tape - jac_options = getattr(tape, "jacobian_options", {}) - # cache state for adjoint jacobian computation - if jac_options.get("jacobian_method", None) == "adjoint_jacobian": - if jac_options.get("adjoint_cache", True): - use_adjoint_cached_state = True - state = device._pre_rotated_state - - ctx.saved_grad_matrices = {} - - def _evaluate_grad_matrix(grad_matrix_fn): - """Convenience function for generating gradient matrices - for the given parameter values. - - This function serves two purposes: - - * Avoids duplicating logic surrounding parameter unwrapping/wrapping - - * Takes advantage of closure, to cache computed gradient matrices via - the ctx.saved_grad_matrices attribute, to avoid gradient matrices being - computed multiple redundant times. - - This is particularly useful when differentiating vector-valued QNodes. - Because PyTorch requests the vector-GradMatrix product, - and *not* the full GradMatrix, differentiating vector-valued - functions will result in multiple backward passes. - - Args: - grad_matrix_fn (str): Name of the gradient matrix function. Should correspond to an existing - tape method. Currently allowed values include ``"jacobian"`` and ``"hessian"``. - - Returns: - array[float]: the gradient matrix - """ - if grad_matrix_fn in ctx.saved_grad_matrices: - return ctx.saved_grad_matrices[grad_matrix_fn] - - if use_adjoint_cached_state: - tape.jacobian_options["device_pd_options"] = {"starting_state": state} - - tape.set_parameters(ctx.all_params_unwrapped, trainable_only=False) - grad_matrix = getattr(tape, grad_matrix_fn)( - device, params=ctx.args, **tape.jacobian_options - ) - tape.set_parameters(ctx.all_params, trainable_only=False) - - grad_matrix = torch.as_tensor(torch.from_numpy(grad_matrix), dtype=tape.dtype) - ctx.saved_grad_matrices[grad_matrix_fn] = grad_matrix - - return grad_matrix - - class _Jacobian(torch.autograd.Function): - @staticmethod - def forward(ctx_, parent_ctx, *input_): - """Implements the forward pass QNode Jacobian evaluation""" - ctx_.dy = parent_ctx.dy - ctx_.save_for_backward(*input_) - jacobian = _evaluate_grad_matrix("jacobian") - return jacobian - - @staticmethod - def backward(ctx_, ddy): # pragma: no cover - """Implements the backward pass QNode vector-Hessian product""" - hessian = _evaluate_grad_matrix("hessian") - - if torch.squeeze(ddy).ndim > 1: - vhp = ctx_.dy.view(1, -1) @ ddy @ hessian @ ctx_.dy.view(-1, 1) - vhp = vhp / torch.linalg.norm(ctx_.dy) ** 2 - else: - vhp = ddy @ hessian - - vhp = torch.unbind(vhp.view(-1)) - - grad_input = [] - - # match the type and device of the input tensors - for i, j in zip(vhp, ctx_.saved_tensors): - res = torch.as_tensor(i, dtype=tape.dtype) - if j.is_cuda: # pragma: no cover - cuda_device = j.get_device() - res = torch.as_tensor(res, device=cuda_device) - grad_input.append(res) - - return (None,) + tuple(grad_input) - - ctx.jacobian = _Jacobian - - # if any input tensor uses the GPU, the output should as well - for i in input_: - if isinstance(i, torch.Tensor): - if i.is_cuda: # pragma: no cover - cuda_device = i.get_device() - return torch.as_tensor( - torch.from_numpy(res), device=cuda_device, dtype=tape.dtype - ) - - if tape.is_sampled and not tape.all_sampled: - return tuple(torch.as_tensor(t, dtype=tape.dtype) for t in res) - - if res.dtype == np.dtype("object"): - res = np.hstack(res) - - return torch.as_tensor(torch.from_numpy(res), dtype=tape.dtype) - - @staticmethod - def backward(ctx, dy): # pragma: no cover - """Implements the backwards pass QNode vector-Jacobian product""" - ctx.dy = dy - - dyv = dy.view(1, -1) - jac_res = ctx.jacobian.apply(ctx, *ctx.saved_tensors) - - # When using CUDA, dyv seems to remain on the GPU, while the result - # of jac_res is returned on CPU, even though the saved_tensors arguments are - # themselves on the GPU. Check whether this has happened, and move things - # back to the GPU if required. - if dyv.is_cuda or jac_res.is_cuda: - if not dyv.is_cuda: - dyv = torch.as_tensor(dyv, device=jac_res.get_device()) - if not jac_res.is_cuda: - jac_res = torch.as_tensor(jac_res, device=dyv.get_device()) - - vjp = dyv @ jac_res - vjp = torch.unbind(vjp.view(-1)) - return (None,) + tuple(vjp) - - -class TorchInterface(AnnotatedQueue): - """Mixin class for applying an Torch interface to a :class:`~.JacobianTape`. - - Torch-compatible quantum tape classes can be created via subclassing: - - .. code-block:: python - - class MyTorchQuantumTape(TorchInterface, JacobianTape): - - Alternatively, the Torch interface can be dynamically applied to existing - quantum tapes via the :meth:`~.apply` class method. This modifies the - tape **in place**. - - Once created, the Torch interface can be used to perform quantum-classical - differentiable programming. - - **Example** - - Once a Torch quantum tape has been created, it can be evaluated and differentiated: - - .. code-block:: python - - dev = qml.device("default.qubit", wires=1) - p = torch.tensor([0.1, 0.2, 0.3], requires_grad=True) - - with TorchInterface.apply(JacobianTape()) as qtape: - qml.Rot(p[0], p[1] ** 2 + p[0] * p[2], p[1] * torch.sin(p[2]), wires=0) - expval(qml.PauliX(0)) - - result = qtape.execute(dev) - - >>> print(result) - tensor([0.0698], dtype=torch.float64, grad_fn=<_TorchInterfaceBackward>) - >>> result.backward() - >>> print(p.grad) - tensor([0.2987, 0.3971, 0.0988]) - - The Torch interface defaults to ``torch.float64`` output. This can be modified by - providing the ``dtype`` argument when applying the interface: - - >>> p = torch.tensor([0.1, 0.2, 0.3], requires_grad=True) - >>> with TorchInterface.apply(JacobianTape(), dtype=torch.float32) as qtape: - ... qml.Rot(p[0], p[1] ** 2 + p[0] * p[2], p[1] * torch.sin(p[2]), wires=0) - ... expval(qml.PauliX(0)) - >>> result = qtape.execute(dev) - >>> print(result) - tensor([0.0698], grad_fn=<_TorchInterfaceBackward>) - >>> print(result.dtype) - torch.float32 - >>> result.backward() - >>> print(p.grad) - tensor([0.2987, 0.3971, 0.0988]) - >>> print(p.grad.dtype) - torch.float32 - """ - - dtype = torch.float64 - - @property - def interface(self): # pylint: disable=missing-function-docstring - return "torch" - - def _update_trainable_params(self): - params = self.get_parameters(trainable_only=False) - - trainable_params = set() - - for idx, p in enumerate(params): - if getattr(p, "requires_grad", False): - trainable_params.add(idx) - - self.trainable_params = trainable_params - return params - - def _execute(self, params, **kwargs): - kwargs["tape"] = self - res = _TorchInterface.apply(kwargs, *params) - return res - - @classmethod - def apply(cls, tape, dtype=torch.float64): - """Apply the Torch interface to an existing tape in-place. - - Args: - tape (.JacobianTape): a quantum tape to apply the Torch interface to - dtype (torch.dtype): the dtype that the returned quantum tape should - output - - **Example** - - >>> with JacobianTape() as tape: - ... qml.RX(0.5, wires=0) - ... expval(qml.PauliZ(0)) - >>> TorchInterface.apply(tape) - >>> tape - , params=1> - """ - if (dtype is torch.complex64 or dtype is torch.complex128) and not COMPLEX_SUPPORT: - raise qml.QuantumFunctionError( - "Version 1.8.0 or above of PyTorch must be installed for complex support, " - "which is required for quantum functions that return the state." - ) - - tape_class = getattr(tape, "__bare__", tape.__class__) - tape.__bare__ = tape_class - tape.__class__ = type("TorchQuantumTape", (cls, tape_class), {"dtype": dtype}) - tape._update_trainable_params() - return tape diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 03f7e17a2cf..e372164078d 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -232,7 +232,6 @@ def __init__( else: self.qnode = batch_input(qnode, argnum=batch_idx) - dtype = tf.float32 if tf.keras.backend.floatx() == tf.float32 else tf.float64 self.qnode.interface = "tf" # Allows output_dim to be specified as an int or as a tuple, e.g, 5, (5,), (5, 2), [5, 2] diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py index 1c4b3749dd1..251d515a364 100644 --- a/pennylane/tape/tape.py +++ b/pennylane/tape/tape.py @@ -154,8 +154,7 @@ def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False): # by default expand all objects stop_at = lambda obj: False - new_tape = tape.__class__() - new_tape.__bare__ = getattr(tape, "__bare__", tape.__class__) + new_tape = QuantumTape() # Check for observables acting on the same wire. If present, observables must be # qubit-wise commuting Pauli words. In this case, the tape is expanded with joint @@ -1238,7 +1237,7 @@ def data(self): def data(self, params): self.set_parameters(params, trainable_only=False) - def copy(self, copy_operations=False, tape_cls=None): + def copy(self, copy_operations=False): """Returns a shallow copy of the quantum tape. Args: @@ -1246,16 +1245,11 @@ def copy(self, copy_operations=False, tape_cls=None): Otherwise, if False, the copied tape operations will simply be references to the original tape operations; changing the parameters of one tape will likewise change the parameters of all copies. - tape_cls (.QuantumTape): Cast the copied tape to a specific quantum tape subclass. - If not provided, the same subclass is used as the original tape. Returns: .QuantumTape: a shallow copy of the tape """ - if tape_cls is None: - tape = self.__class__() - else: - tape = tape_cls() + tape = QuantumTape() if copy_operations: # Perform a shallow copy of all operations in the state prep, operation, and measurement diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index 35a2a538bcb..2db61547e15 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -1354,28 +1354,6 @@ def test_deep_copy(self): # to support PyTorch, which does not support deep copying of tensors assert copied_tape.operations[0].data[0] is tape.operations[0].data[0] - def test_casting(self): - """Test that copying and casting works as expected""" - with QuantumTape() as tape: - qml.BasisState(np.array([1, 0]), wires=[0, 1]) - qml.RY(0.5, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0) @ qml.PauliY(1)) - - # copy and cast to a JacobianTape - copied_tape = tape.copy(tape_cls=qml.tape.JacobianTape) - - # check that the copying worked - assert copied_tape is not tape - assert copied_tape.operations == tape.operations - assert copied_tape.observables == tape.observables - assert copied_tape.measurements == tape.measurements - assert copied_tape.operations[0] is tape.operations[0] - - # check that the casting worked - assert isinstance(copied_tape, qml.tape.JacobianTape) - assert not isinstance(tape, qml.tape.JacobianTape) - class TestStopRecording: """Test that the stop_recording function works as expected""" diff --git a/tests/transforms/test_specs.py b/tests/transforms/test_specs.py index d4739734ba9..c5d5e9e1849 100644 --- a/tests/transforms/test_specs.py +++ b/tests/transforms/test_specs.py @@ -21,154 +21,7 @@ class TestSpecsTransform: - """Tests for the transform specs using the old QNode. This can be - removed when `qml.beta.QNode is made default.""" - - @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 10), ("parameter-shift", 12), ("adjoint", 11)] - ) - def test_empty(self, diff_method, len_info): - - dev = qml.device("default.qubit", wires=1) - - @qml.qnode_old.qnode(dev, diff_method=diff_method) - def circ(): - return qml.expval(qml.PauliZ(0)) - - info_func = qml.specs(circ) - info = info_func() - - circ() - assert info == circ.specs - assert len(info) == len_info - - assert info["gate_sizes"] == defaultdict(int) - assert info["gate_types"] == defaultdict(int) - assert info["num_observables"] == 1 - assert info["num_operations"] == 0 - assert info["num_diagonalizing_gates"] == 0 - assert info["num_used_wires"] == 1 - assert info["depth"] == 0 - assert info["num_device_wires"] == 1 - assert info["diff_method"] == diff_method - - if diff_method == "parameter-shift": - assert info["num_parameter_shift_executions"] == 1 - - if diff_method != "backprop": - assert info["device_name"] == "default.qubit" - assert info["num_trainable_params"] == 0 - else: - assert info["device_name"] == "default.qubit.autograd" - - @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 10), ("parameter-shift", 12), ("adjoint", 11)] - ) - def test_specs(self, diff_method, len_info): - """Test the specs transforms works in standard situations""" - dev = qml.device("default.qubit", wires=4) - - @qml.qnode_old.qnode(dev, diff_method=diff_method) - def circuit(x, y, add_RY=True): - qml.RX(x[0], wires=0) - qml.Toffoli(wires=(0, 1, 2)) - qml.CRY(x[1], wires=(0, 1)) - qml.Rot(x[2], x[3], y, wires=2) - if add_RY: - qml.RY(x[4], wires=1) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1)) - - x = np.array([0.05, 0.1, 0.2, 0.3, 0.5], requires_grad=True) - y = np.array(0.1, requires_grad=False) - - info_func = qml.specs(circuit) - - info = info_func(x, y, add_RY=False) - - circuit(x, y, add_RY=False) - - assert info == circuit.specs - - assert len(info) == len_info - - assert info["gate_sizes"] == defaultdict(int, {1: 2, 3: 1, 2: 1}) - assert info["gate_types"] == defaultdict(int, {"RX": 1, "Toffoli": 1, "CRY": 1, "Rot": 1}) - assert info["num_operations"] == 4 - assert info["num_observables"] == 2 - assert info["num_diagonalizing_gates"] == 1 - assert info["num_used_wires"] == 3 - assert info["depth"] == 3 - assert info["num_device_wires"] == 4 - assert info["diff_method"] == diff_method - - if diff_method == "parameter-shift": - assert info["num_parameter_shift_executions"] == 7 - - if diff_method != "backprop": - assert info["device_name"] == "default.qubit" - assert info["num_trainable_params"] == 4 - else: - assert info["device_name"] == "default.qubit.autograd" - - @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 10), ("parameter-shift", 11), ("adjoint", 11)] - ) - def test_specs_state(self, diff_method, len_info): - """Test specs works when state returned""" - - dev = qml.device("default.qubit", wires=2) - - @qml.qnode_old.qnode(dev, diff_method=diff_method) - def circuit(): - return qml.state() - - info_func = qml.specs(circuit) - info = info_func() - - circuit() - assert info == circuit.specs - assert len(info) == len_info - - assert info["num_observables"] == 1 - assert info["num_diagonalizing_gates"] == 0 - - def test_max_expansion(self): - """Test that a user can calculation specifications for a different max - expansion parameter.""" - - n_layers = 2 - n_wires = 5 - - dev = qml.device("default.qubit", wires=n_wires) - - @qml.qnode_old.qnode(dev) - def circuit(params): - qml.templates.BasicEntanglerLayers(params, wires=range(n_wires)) - return qml.expval(qml.PauliZ(0)) - - params_shape = qml.templates.BasicEntanglerLayers.shape(n_layers=n_layers, n_wires=n_wires) - rng = np.random.default_rng(seed=10) - params = rng.standard_normal(params_shape) - - assert circuit.max_expansion == 10 - info = qml.specs(circuit, max_expansion=0)(params) - assert circuit.max_expansion == 10 - - assert len(info) == 10 - - assert info["gate_sizes"] == defaultdict(int, {5: 1}) - assert info["gate_types"] == defaultdict(int, {"BasicEntanglerLayers": 1}) - assert info["num_operations"] == 1 - assert info["num_observables"] == 1 - assert info["num_used_wires"] == 5 - assert info["depth"] == 1 - assert info["num_device_wires"] == 5 - assert info["device_name"] == "default.qubit.autograd" - assert info["diff_method"] == "backprop" - - -class TestSpecsTransformBetaQNode: - """Tests for the transform specs using the new QNode""" + """Tests for the transform specs using the QNode""" @pytest.mark.parametrize( "diff_method, len_info", [("backprop", 15), ("parameter-shift", 16), ("adjoint", 15)] From 690b8622618d0e7980d849dc9ba09413bdd37fbc Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 14:13:59 +0800 Subject: [PATCH 04/13] Move interfaces and interface tests out of the batch folder --- pennylane/__init__.py | 2 +- pennylane/interfaces/__init__.py | 421 ++++++++++++++++- pennylane/interfaces/{batch => }/autograd.py | 0 pennylane/interfaces/batch/__init__.py | 434 ------------------ pennylane/interfaces/{batch => }/jax.py | 2 +- pennylane/interfaces/{batch => }/jax_jit.py | 2 +- .../interfaces/{batch => }/tensorflow.py | 0 .../{batch => }/tensorflow_autograph.py | 0 pennylane/interfaces/{batch => }/torch.py | 0 pennylane/qnode.py | 2 +- ...est_batch_autograd.py => test_autograd.py} | 8 +- ...tograd_qnode.py => test_autograd_qnode.py} | 0 .../{test_batch_jax.py => test_jax.py} | 20 +- ...t_batch_jax_qnode.py => test_jax_qnode.py} | 2 +- ...batch_tensorflow.py => test_tensorflow.py} | 6 +- ...flow_qnode.py => test_tensorflow_qnode.py} | 0 .../{test_batch_torch.py => test_torch.py} | 6 +- ...tch_torch_qnode.py => test_torch_qnode.py} | 0 18 files changed, 444 insertions(+), 461 deletions(-) rename pennylane/interfaces/{batch => }/autograd.py (100%) delete mode 100644 pennylane/interfaces/batch/__init__.py rename pennylane/interfaces/{batch => }/jax.py (99%) rename pennylane/interfaces/{batch => }/jax_jit.py (96%) rename pennylane/interfaces/{batch => }/tensorflow.py (100%) rename pennylane/interfaces/{batch => }/tensorflow_autograph.py (100%) rename pennylane/interfaces/{batch => }/torch.py (100%) rename tests/interfaces/{test_batch_autograd.py => test_autograd.py} (96%) rename tests/interfaces/{test_batch_autograd_qnode.py => test_autograd_qnode.py} (100%) rename tests/interfaces/{test_batch_jax.py => test_jax.py} (95%) rename tests/interfaces/{test_batch_jax_qnode.py => test_jax_qnode.py} (97%) rename tests/interfaces/{test_batch_tensorflow.py => test_tensorflow.py} (96%) rename tests/interfaces/{test_batch_tensorflow_qnode.py => test_tensorflow_qnode.py} (100%) rename tests/interfaces/{test_batch_torch.py => test_torch.py} (96%) rename tests/interfaces/{test_batch_torch_qnode.py => test_torch_qnode.py} (100%) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 3ce57925958..105022241c2 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -86,7 +86,7 @@ from .collections import QNodeCollection, dot, map, sum import pennylane.grouping # pylint:disable=wrong-import-order import pennylane.gradients # pylint:disable=wrong-import-order -from pennylane.interfaces.batch import execute # pylint:disable=wrong-import-order +from pennylane.interfaces import execute # pylint:disable=wrong-import-order # Look for an existing configuration file default_config = Configuration("config.toml") diff --git a/pennylane/interfaces/__init__.py b/pennylane/interfaces/__init__.py index f3ae97b4526..6e09b200f40 100644 --- a/pennylane/interfaces/__init__.py +++ b/pennylane/interfaces/__init__.py @@ -12,6 +12,423 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This subpackage defines functions convert quantum tapes to interface with different machine -learning libraries. +This subpackage defines functions for interfacing devices' batch execution +capabilities with different machine learning libraries. """ +# pylint: disable=import-outside-toplevel,too-many-arguments,too-many-branches,protected-access +import contextlib +from functools import wraps +import itertools + +from cachetools import LRUCache +import numpy as np + +import pennylane as qml + + +INTERFACE_NAMES = { + "NumPy": (None,), + "Autograd": ("autograd", "numpy"), # for backwards compatibility + "JAX": ("jax", "jax-jit", "jax-python", "JAX"), + "PyTorch": ("torch", "pytorch"), + "TensorFlow": ("tf", "tensorflow", "tensorflow-autograph", "tf-autograph"), +} +"""dict[str, str]: maps allowed interface strings to the name of the interface""" + +SUPPORTED_INTERFACES = list(itertools.chain(*INTERFACE_NAMES.values())) + + +class InterfaceUnsupportedError(NotImplementedError): + """Exception raised when features not supported by an interface are + attempted to be used.""" + + +@contextlib.contextmanager +def set_shots(device, shots): + """Context manager to temporarily change the shots + of a device. + + This context manager can be used in two ways. + + As a standard context manager: + + >>> dev = qml.device("default.qubit", wires=2, shots=None) + >>> with set_shots(dev, shots=100): + ... print(dev.shots) + 100 + >>> print(dev.shots) + None + + Or as a decorator that acts on a function that uses the device: + + >>> set_shots(dev, shots=100)(lambda: dev.shots)() + 100 + """ + if shots == device.shots: + yield + return + + original_shots = device.shots + original_shot_vector = device._shot_vector + + try: + if shots is not False and device.shots != shots: + device.shots = shots + yield + finally: + device.shots = original_shots + device._shot_vector = original_shot_vector + + +def cache_execute(fn, cache, pass_kwargs=False, return_tuple=True, expand_fn=None): + """Decorator that adds caching to a function that executes + multiple tapes on a device. + + This decorator makes use of :attr:`.QuantumTape.hash` to identify + unique tapes. + + - If a tape does not match a hash in the cache, then the tape + has not been previously executed. It is executed, and the result + added to the cache. + + - If a tape matches a hash in the cache, then the tape has been previously + executed. The corresponding cached result is + extracted, and the tape is not passed to the execution function. + + - Finally, there might be the case where one or more tapes in the current + set of tapes to be executed are identical and thus share a hash. If this is the case, + duplicates are removed, to avoid redundant evaluations. + + Args: + fn (callable): The execution function to add caching to. + This function should have the signature ``fn(tapes, **kwargs)``, + and it should return ``list[tensor_like]``, with the + same length as the input ``tapes``. + cache (None or dict or Cache or bool): The cache to use. If ``None``, + caching will not occur. + pass_kwargs (bool): If ``True``, keyword arguments passed to the + wrapped function will be passed directly to ``fn``. If ``False``, + they will be ignored. + return_tuple (bool): If ``True``, the output of ``fn`` is returned + as a tuple ``(fn_ouput, [])``, to match the output of execution functions + that also return gradients. + + Returns: + function: a wrapped version of the execution function ``fn`` with caching + support + """ + if expand_fn is not None: + original_fn = fn + + def fn(tapes, **kwargs): # pylint: disable=function-redefined + tapes = [expand_fn(tape) for tape in tapes] + return original_fn(tapes, **kwargs) + + @wraps(fn) + def wrapper(tapes, **kwargs): + + if not pass_kwargs: + kwargs = {} + + if cache is None or (isinstance(cache, bool) and not cache): + # No caching. Simply execute the execution function + # and return the results. + res = fn(tapes, **kwargs) + return (res, []) if return_tuple else res + + execution_tapes = {} + cached_results = {} + hashes = {} + repeated = {} + + for i, tape in enumerate(tapes): + h = tape.hash + + if h in hashes.values(): + # Tape already exists within ``tapes``. Determine the + # index of the first occurrence of the tape, store this, + # and continue to the next iteration. + idx = list(hashes.keys())[list(hashes.values()).index(h)] + repeated[i] = idx + continue + + hashes[i] = h + + if hashes[i] in cache: + # Tape exists within the cache, store the cached result + cached_results[i] = cache[hashes[i]] + else: + # Tape does not exist within the cache, store the tape + # for execution via the execution function. + execution_tapes[i] = tape + + # if there are no execution tapes, simply return! + if not execution_tapes: + if not repeated: + res = list(cached_results.values()) + return (res, []) if return_tuple else res + + else: + # execute all unique tapes that do not exist in the cache + res = fn(execution_tapes.values(), **kwargs) + + final_res = [] + + for i, tape in enumerate(tapes): + if i in cached_results: + # insert cached results into the results vector + final_res.append(cached_results[i]) + + elif i in repeated: + # insert repeated results into the results vector + final_res.append(final_res[repeated[i]]) + + else: + # insert evaluated results into the results vector + r = res.pop(0) + final_res.append(r) + cache[hashes[i]] = r + + return (final_res, []) if return_tuple else final_res + + wrapper.fn = fn + return wrapper + + +def execute( + tapes, + device, + gradient_fn, + interface="autograd", + mode="best", + gradient_kwargs=None, + cache=True, + cachesize=10000, + max_diff=1, + override_shots=False, + expand_fn="device", + max_expansion=10, + device_batch_transform=True, +): + """Execute a batch of tapes on a device in an autodifferentiable-compatible manner. + + Args: + tapes (Sequence[.QuantumTape]): batch of tapes to execute + device (.Device): Device to use to execute the batch of tapes. + If the device does not provide a ``batch_execute`` method, + by default the tapes will be executed in serial. + gradient_fn (None or callable): The gradient transform function to use + for backward passes. If "device", the device will be queried directly + for the gradient (if supported). + 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``, and ``jax``. + mode (str): Whether the gradients should be computed on the forward + pass (``forward``) or the backward pass (``backward``). Only applies + if the device is queried for the gradient; gradient transform + functions available in ``qml.gradients`` are only supported on the backward + pass. + gradient_kwargs (dict): dictionary of keyword arguments to pass when + determining the gradients of tapes + cache (bool): Whether to cache evaluations. This can result in + a significant reduction in quantum evaluations during gradient computations. + cachesize (int): the size of the cache + max_diff (int): If ``gradient_fn`` is a gradient transform, this option specifies + the maximum number of derivatives to support. Increasing this value allows + for higher order derivatives to be extracted, at the cost of additional + (classical) computational overhead during the backwards pass. + expand_fn (function): Tape expansion function to be called prior to device execution. + Must have signature of the form ``expand_fn(tape, max_expansion)``, and return a + single :class:`~.QuantumTape`. If not provided, by default :meth:`Device.expand_fn` + is called. + max_expansion (int): The number of times the internal circuit should be expanded when + executed on a device. Expansion occurs when an operation or measurement is not + supported, and results in a gate decomposition. If any operations in the decomposition + remain unsupported by the device, another expansion occurs. + device_batch_transform (bool): Whether to apply any batch transforms defined by the device + (within :meth:`Device.batch_transform`) to each tape to be executed. The default behaviour + of the device batch transform is to expand out Hamiltonian measurements into + constituent terms if not supported on the device. + + Returns: + list[list[float]]: A nested list of tape results. Each element in + the returned list corresponds in order to the provided tapes. + + **Example** + + Consider the following cost function: + + .. code-block:: python + + dev = qml.device("lightning.qubit", wires=2) + + def cost_fn(params, x): + with qml.tape.QuantumTape() as tape1: + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=0) + qml.expval(qml.PauliZ(0)) + + with qml.tape.QuantumTape() as tape2: + qml.RX(params[2], wires=0) + qml.RY(x[0], wires=1) + qml.CNOT(wires=[0, 1]) + qml.probs(wires=0) + + tapes = [tape1, tape2] + + # execute both tapes in a batch on the given device + res = qml.execute(tapes, dev, qml.gradients.param_shift, max_diff=2) + + return res[0][0] + res[1][0, 0] - res[1][0, 1] + + In this cost function, two **independent** quantum tapes are being + constructed; one returning an expectation value, the other probabilities. + We then batch execute the two tapes, and reduce the results to obtain + a scalar. + + Let's execute this cost function while tracking the gradient: + + >>> params = np.array([0.1, 0.2, 0.3], requires_grad=True) + >>> x = np.array([0.5], requires_grad=True) + >>> cost_fn(params, x) + tensor(1.93050682, requires_grad=True) + + Since the ``execute`` function is differentiable, we can + also compute the gradient: + + >>> qml.grad(cost_fn)(params, x) + (array([-0.0978434 , -0.19767681, -0.29552021]), array([5.37764278e-17])) + + Finally, we can also compute any nth-order derivative. Let's compute the Jacobian + of the gradient (that is, the Hessian): + + >>> x.requires_grad = False + >>> qml.jacobian(qml.grad(cost_fn))(params, x) + array([[-0.97517033, 0.01983384, 0. ], + [ 0.01983384, -0.97517033, 0. ], + [ 0. , 0. , -0.95533649]]) + """ + gradient_kwargs = gradient_kwargs or {} + + if device_batch_transform: + tapes, batch_fn = qml.transforms.map_batch_transform(device.batch_transform, tapes) + else: + batch_fn = lambda res: res + + if isinstance(cache, bool) and cache: + # cache=True: create a LRUCache object + cache = LRUCache(maxsize=cachesize, getsizeof=lambda x: qml.math.shape(x)[0]) + + batch_execute = set_shots(device, override_shots)(device.batch_execute) + + if expand_fn == "device": + expand_fn = lambda tape: device.expand_fn(tape, max_expansion=max_expansion) + + if gradient_fn is None: + # don't unwrap if it's an interface device + if "passthru_interface" in device.capabilities(): + return batch_fn( + cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes) + ) + with qml.tape.Unwrap(*tapes): + res = cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)( + tapes + ) + + return batch_fn(res) + + if gradient_fn == "backprop" or interface is None: + return batch_fn( + cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes) + ) + + # the default execution function is batch_execute + execute_fn = cache_execute(batch_execute, cache, expand_fn=expand_fn) + _mode = "backward" + + if gradient_fn == "device": + # gradient function is a device method + + # Expand all tapes as per the device's expand function here. + # We must do this now, prior to the interface, to ensure that + # decompositions with parameter processing is tracked by the + # autodiff frameworks. + for i, tape in enumerate(tapes): + tapes[i] = expand_fn(tape) + + if mode in ("forward", "best"): + # replace the forward execution function to return + # both results and gradients + execute_fn = set_shots(device, override_shots)(device.execute_and_gradients) + gradient_fn = None + _mode = "forward" + + elif mode == "backward": + # disable caching on the forward pass + execute_fn = cache_execute(batch_execute, cache=None) + + # replace the backward gradient computation + gradient_fn = cache_execute( + set_shots(device, override_shots)(device.gradients), + cache, + pass_kwargs=True, + return_tuple=False, + ) + + elif mode == "forward": + # In "forward" mode, gradients are automatically handled + # within execute_and_gradients, so providing a gradient_fn + # in this case would have ambiguous behaviour. + raise ValueError("Gradient transforms cannot be used with mode='forward'") + + try: + if interface in INTERFACE_NAMES["Autograd"]: + from .autograd import execute as _execute + elif interface in INTERFACE_NAMES["TensorFlow"]: + import tensorflow as tf + + if not tf.executing_eagerly() or "autograph" in interface: + from .tensorflow_autograph import execute as _execute + else: + from .tensorflow import execute as _execute + + elif interface in INTERFACE_NAMES["PyTorch"]: + from .torch import execute as _execute + elif interface in INTERFACE_NAMES["JAX"]: + _execute = _get_jax_execute_fn(interface, tapes) + else: + raise ValueError( + f"Unknown interface {interface}. Supported " + f"interfaces are {SUPPORTED_INTERFACES}" + ) + except ImportError as e: + interface_name = [k for k, v in INTERFACE_NAMES.items() if interface in v][0] + + raise qml.QuantumFunctionError( + f"{interface_name} not found. Please install the latest " + f"version of {interface_name} to enable the '{interface}' interface." + ) from e + + res = _execute( + tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n=1, max_diff=max_diff, mode=_mode + ) + + return batch_fn(res) + + +def _get_jax_execute_fn(interface, tapes): + """Auxiliary function to determine the execute function to use with the JAX + interface.""" + + # The most general JAX interface was sepcified, automatically determine if + # support for jitting is needed by swapping to "jax-jit" or "jax-python" + if interface == "jax": + from .jax import get_jax_interface_name + + interface = get_jax_interface_name(tapes) + + if interface == "jax-jit": + from .jax_jit import execute as _execute + else: + from .jax import execute as _execute + return _execute diff --git a/pennylane/interfaces/batch/autograd.py b/pennylane/interfaces/autograd.py similarity index 100% rename from pennylane/interfaces/batch/autograd.py rename to pennylane/interfaces/autograd.py diff --git a/pennylane/interfaces/batch/__init__.py b/pennylane/interfaces/batch/__init__.py deleted file mode 100644 index 6e09b200f40..00000000000 --- a/pennylane/interfaces/batch/__init__.py +++ /dev/null @@ -1,434 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This subpackage defines functions for interfacing devices' batch execution -capabilities with different machine learning libraries. -""" -# pylint: disable=import-outside-toplevel,too-many-arguments,too-many-branches,protected-access -import contextlib -from functools import wraps -import itertools - -from cachetools import LRUCache -import numpy as np - -import pennylane as qml - - -INTERFACE_NAMES = { - "NumPy": (None,), - "Autograd": ("autograd", "numpy"), # for backwards compatibility - "JAX": ("jax", "jax-jit", "jax-python", "JAX"), - "PyTorch": ("torch", "pytorch"), - "TensorFlow": ("tf", "tensorflow", "tensorflow-autograph", "tf-autograph"), -} -"""dict[str, str]: maps allowed interface strings to the name of the interface""" - -SUPPORTED_INTERFACES = list(itertools.chain(*INTERFACE_NAMES.values())) - - -class InterfaceUnsupportedError(NotImplementedError): - """Exception raised when features not supported by an interface are - attempted to be used.""" - - -@contextlib.contextmanager -def set_shots(device, shots): - """Context manager to temporarily change the shots - of a device. - - This context manager can be used in two ways. - - As a standard context manager: - - >>> dev = qml.device("default.qubit", wires=2, shots=None) - >>> with set_shots(dev, shots=100): - ... print(dev.shots) - 100 - >>> print(dev.shots) - None - - Or as a decorator that acts on a function that uses the device: - - >>> set_shots(dev, shots=100)(lambda: dev.shots)() - 100 - """ - if shots == device.shots: - yield - return - - original_shots = device.shots - original_shot_vector = device._shot_vector - - try: - if shots is not False and device.shots != shots: - device.shots = shots - yield - finally: - device.shots = original_shots - device._shot_vector = original_shot_vector - - -def cache_execute(fn, cache, pass_kwargs=False, return_tuple=True, expand_fn=None): - """Decorator that adds caching to a function that executes - multiple tapes on a device. - - This decorator makes use of :attr:`.QuantumTape.hash` to identify - unique tapes. - - - If a tape does not match a hash in the cache, then the tape - has not been previously executed. It is executed, and the result - added to the cache. - - - If a tape matches a hash in the cache, then the tape has been previously - executed. The corresponding cached result is - extracted, and the tape is not passed to the execution function. - - - Finally, there might be the case where one or more tapes in the current - set of tapes to be executed are identical and thus share a hash. If this is the case, - duplicates are removed, to avoid redundant evaluations. - - Args: - fn (callable): The execution function to add caching to. - This function should have the signature ``fn(tapes, **kwargs)``, - and it should return ``list[tensor_like]``, with the - same length as the input ``tapes``. - cache (None or dict or Cache or bool): The cache to use. If ``None``, - caching will not occur. - pass_kwargs (bool): If ``True``, keyword arguments passed to the - wrapped function will be passed directly to ``fn``. If ``False``, - they will be ignored. - return_tuple (bool): If ``True``, the output of ``fn`` is returned - as a tuple ``(fn_ouput, [])``, to match the output of execution functions - that also return gradients. - - Returns: - function: a wrapped version of the execution function ``fn`` with caching - support - """ - if expand_fn is not None: - original_fn = fn - - def fn(tapes, **kwargs): # pylint: disable=function-redefined - tapes = [expand_fn(tape) for tape in tapes] - return original_fn(tapes, **kwargs) - - @wraps(fn) - def wrapper(tapes, **kwargs): - - if not pass_kwargs: - kwargs = {} - - if cache is None or (isinstance(cache, bool) and not cache): - # No caching. Simply execute the execution function - # and return the results. - res = fn(tapes, **kwargs) - return (res, []) if return_tuple else res - - execution_tapes = {} - cached_results = {} - hashes = {} - repeated = {} - - for i, tape in enumerate(tapes): - h = tape.hash - - if h in hashes.values(): - # Tape already exists within ``tapes``. Determine the - # index of the first occurrence of the tape, store this, - # and continue to the next iteration. - idx = list(hashes.keys())[list(hashes.values()).index(h)] - repeated[i] = idx - continue - - hashes[i] = h - - if hashes[i] in cache: - # Tape exists within the cache, store the cached result - cached_results[i] = cache[hashes[i]] - else: - # Tape does not exist within the cache, store the tape - # for execution via the execution function. - execution_tapes[i] = tape - - # if there are no execution tapes, simply return! - if not execution_tapes: - if not repeated: - res = list(cached_results.values()) - return (res, []) if return_tuple else res - - else: - # execute all unique tapes that do not exist in the cache - res = fn(execution_tapes.values(), **kwargs) - - final_res = [] - - for i, tape in enumerate(tapes): - if i in cached_results: - # insert cached results into the results vector - final_res.append(cached_results[i]) - - elif i in repeated: - # insert repeated results into the results vector - final_res.append(final_res[repeated[i]]) - - else: - # insert evaluated results into the results vector - r = res.pop(0) - final_res.append(r) - cache[hashes[i]] = r - - return (final_res, []) if return_tuple else final_res - - wrapper.fn = fn - return wrapper - - -def execute( - tapes, - device, - gradient_fn, - interface="autograd", - mode="best", - gradient_kwargs=None, - cache=True, - cachesize=10000, - max_diff=1, - override_shots=False, - expand_fn="device", - max_expansion=10, - device_batch_transform=True, -): - """Execute a batch of tapes on a device in an autodifferentiable-compatible manner. - - Args: - tapes (Sequence[.QuantumTape]): batch of tapes to execute - device (.Device): Device to use to execute the batch of tapes. - If the device does not provide a ``batch_execute`` method, - by default the tapes will be executed in serial. - gradient_fn (None or callable): The gradient transform function to use - for backward passes. If "device", the device will be queried directly - for the gradient (if supported). - 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``, and ``jax``. - mode (str): Whether the gradients should be computed on the forward - pass (``forward``) or the backward pass (``backward``). Only applies - if the device is queried for the gradient; gradient transform - functions available in ``qml.gradients`` are only supported on the backward - pass. - gradient_kwargs (dict): dictionary of keyword arguments to pass when - determining the gradients of tapes - cache (bool): Whether to cache evaluations. This can result in - a significant reduction in quantum evaluations during gradient computations. - cachesize (int): the size of the cache - max_diff (int): If ``gradient_fn`` is a gradient transform, this option specifies - the maximum number of derivatives to support. Increasing this value allows - for higher order derivatives to be extracted, at the cost of additional - (classical) computational overhead during the backwards pass. - expand_fn (function): Tape expansion function to be called prior to device execution. - Must have signature of the form ``expand_fn(tape, max_expansion)``, and return a - single :class:`~.QuantumTape`. If not provided, by default :meth:`Device.expand_fn` - is called. - max_expansion (int): The number of times the internal circuit should be expanded when - executed on a device. Expansion occurs when an operation or measurement is not - supported, and results in a gate decomposition. If any operations in the decomposition - remain unsupported by the device, another expansion occurs. - device_batch_transform (bool): Whether to apply any batch transforms defined by the device - (within :meth:`Device.batch_transform`) to each tape to be executed. The default behaviour - of the device batch transform is to expand out Hamiltonian measurements into - constituent terms if not supported on the device. - - Returns: - list[list[float]]: A nested list of tape results. Each element in - the returned list corresponds in order to the provided tapes. - - **Example** - - Consider the following cost function: - - .. code-block:: python - - dev = qml.device("lightning.qubit", wires=2) - - def cost_fn(params, x): - with qml.tape.QuantumTape() as tape1: - qml.RX(params[0], wires=0) - qml.RY(params[1], wires=0) - qml.expval(qml.PauliZ(0)) - - with qml.tape.QuantumTape() as tape2: - qml.RX(params[2], wires=0) - qml.RY(x[0], wires=1) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=0) - - tapes = [tape1, tape2] - - # execute both tapes in a batch on the given device - res = qml.execute(tapes, dev, qml.gradients.param_shift, max_diff=2) - - return res[0][0] + res[1][0, 0] - res[1][0, 1] - - In this cost function, two **independent** quantum tapes are being - constructed; one returning an expectation value, the other probabilities. - We then batch execute the two tapes, and reduce the results to obtain - a scalar. - - Let's execute this cost function while tracking the gradient: - - >>> params = np.array([0.1, 0.2, 0.3], requires_grad=True) - >>> x = np.array([0.5], requires_grad=True) - >>> cost_fn(params, x) - tensor(1.93050682, requires_grad=True) - - Since the ``execute`` function is differentiable, we can - also compute the gradient: - - >>> qml.grad(cost_fn)(params, x) - (array([-0.0978434 , -0.19767681, -0.29552021]), array([5.37764278e-17])) - - Finally, we can also compute any nth-order derivative. Let's compute the Jacobian - of the gradient (that is, the Hessian): - - >>> x.requires_grad = False - >>> qml.jacobian(qml.grad(cost_fn))(params, x) - array([[-0.97517033, 0.01983384, 0. ], - [ 0.01983384, -0.97517033, 0. ], - [ 0. , 0. , -0.95533649]]) - """ - gradient_kwargs = gradient_kwargs or {} - - if device_batch_transform: - tapes, batch_fn = qml.transforms.map_batch_transform(device.batch_transform, tapes) - else: - batch_fn = lambda res: res - - if isinstance(cache, bool) and cache: - # cache=True: create a LRUCache object - cache = LRUCache(maxsize=cachesize, getsizeof=lambda x: qml.math.shape(x)[0]) - - batch_execute = set_shots(device, override_shots)(device.batch_execute) - - if expand_fn == "device": - expand_fn = lambda tape: device.expand_fn(tape, max_expansion=max_expansion) - - if gradient_fn is None: - # don't unwrap if it's an interface device - if "passthru_interface" in device.capabilities(): - return batch_fn( - cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes) - ) - with qml.tape.Unwrap(*tapes): - res = cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)( - tapes - ) - - return batch_fn(res) - - if gradient_fn == "backprop" or interface is None: - return batch_fn( - cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes) - ) - - # the default execution function is batch_execute - execute_fn = cache_execute(batch_execute, cache, expand_fn=expand_fn) - _mode = "backward" - - if gradient_fn == "device": - # gradient function is a device method - - # Expand all tapes as per the device's expand function here. - # We must do this now, prior to the interface, to ensure that - # decompositions with parameter processing is tracked by the - # autodiff frameworks. - for i, tape in enumerate(tapes): - tapes[i] = expand_fn(tape) - - if mode in ("forward", "best"): - # replace the forward execution function to return - # both results and gradients - execute_fn = set_shots(device, override_shots)(device.execute_and_gradients) - gradient_fn = None - _mode = "forward" - - elif mode == "backward": - # disable caching on the forward pass - execute_fn = cache_execute(batch_execute, cache=None) - - # replace the backward gradient computation - gradient_fn = cache_execute( - set_shots(device, override_shots)(device.gradients), - cache, - pass_kwargs=True, - return_tuple=False, - ) - - elif mode == "forward": - # In "forward" mode, gradients are automatically handled - # within execute_and_gradients, so providing a gradient_fn - # in this case would have ambiguous behaviour. - raise ValueError("Gradient transforms cannot be used with mode='forward'") - - try: - if interface in INTERFACE_NAMES["Autograd"]: - from .autograd import execute as _execute - elif interface in INTERFACE_NAMES["TensorFlow"]: - import tensorflow as tf - - if not tf.executing_eagerly() or "autograph" in interface: - from .tensorflow_autograph import execute as _execute - else: - from .tensorflow import execute as _execute - - elif interface in INTERFACE_NAMES["PyTorch"]: - from .torch import execute as _execute - elif interface in INTERFACE_NAMES["JAX"]: - _execute = _get_jax_execute_fn(interface, tapes) - else: - raise ValueError( - f"Unknown interface {interface}. Supported " - f"interfaces are {SUPPORTED_INTERFACES}" - ) - except ImportError as e: - interface_name = [k for k, v in INTERFACE_NAMES.items() if interface in v][0] - - raise qml.QuantumFunctionError( - f"{interface_name} not found. Please install the latest " - f"version of {interface_name} to enable the '{interface}' interface." - ) from e - - res = _execute( - tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n=1, max_diff=max_diff, mode=_mode - ) - - return batch_fn(res) - - -def _get_jax_execute_fn(interface, tapes): - """Auxiliary function to determine the execute function to use with the JAX - interface.""" - - # The most general JAX interface was sepcified, automatically determine if - # support for jitting is needed by swapping to "jax-jit" or "jax-python" - if interface == "jax": - from .jax import get_jax_interface_name - - interface = get_jax_interface_name(tapes) - - if interface == "jax-jit": - from .jax_jit import execute as _execute - else: - from .jax import execute as _execute - return _execute diff --git a/pennylane/interfaces/batch/jax.py b/pennylane/interfaces/jax.py similarity index 99% rename from pennylane/interfaces/batch/jax.py rename to pennylane/interfaces/jax.py index 4b3c2879419..e2490f33387 100644 --- a/pennylane/interfaces/batch/jax.py +++ b/pennylane/interfaces/jax.py @@ -21,7 +21,7 @@ import pennylane as qml from pennylane.operation import Sample, Probability -from pennylane.interfaces.batch import InterfaceUnsupportedError +from pennylane.interfaces import InterfaceUnsupportedError dtype = jnp.float64 diff --git a/pennylane/interfaces/batch/jax_jit.py b/pennylane/interfaces/jax_jit.py similarity index 96% rename from pennylane/interfaces/batch/jax_jit.py rename to pennylane/interfaces/jax_jit.py index 3cfc476929b..a7cf7a8cea3 100644 --- a/pennylane/interfaces/batch/jax_jit.py +++ b/pennylane/interfaces/jax_jit.py @@ -24,7 +24,7 @@ import numpy as np import pennylane as qml from pennylane.operation import Variance, Expectation -from pennylane.interfaces.batch import InterfaceUnsupportedError +from pennylane.interfaces import InterfaceUnsupportedError dtype = jnp.float64 diff --git a/pennylane/interfaces/batch/tensorflow.py b/pennylane/interfaces/tensorflow.py similarity index 100% rename from pennylane/interfaces/batch/tensorflow.py rename to pennylane/interfaces/tensorflow.py diff --git a/pennylane/interfaces/batch/tensorflow_autograph.py b/pennylane/interfaces/tensorflow_autograph.py similarity index 100% rename from pennylane/interfaces/batch/tensorflow_autograph.py rename to pennylane/interfaces/tensorflow_autograph.py diff --git a/pennylane/interfaces/batch/torch.py b/pennylane/interfaces/torch.py similarity index 100% rename from pennylane/interfaces/batch/torch.py rename to pennylane/interfaces/torch.py diff --git a/pennylane/qnode.py b/pennylane/qnode.py index 692402899d7..126b0bdfff3 100644 --- a/pennylane/qnode.py +++ b/pennylane/qnode.py @@ -24,7 +24,7 @@ import pennylane as qml from pennylane import Device -from pennylane.interfaces.batch import set_shots, SUPPORTED_INTERFACES +from pennylane.interfaces import set_shots, SUPPORTED_INTERFACES class QNode: diff --git a/tests/interfaces/test_batch_autograd.py b/tests/interfaces/test_autograd.py similarity index 96% rename from tests/interfaces/test_batch_autograd.py rename to tests/interfaces/test_autograd.py index 847549226ba..c20ef93c8b9 100644 --- a/tests/interfaces/test_batch_autograd.py +++ b/tests/interfaces/test_autograd.py @@ -22,7 +22,7 @@ import pennylane as qml from pennylane.gradients import finite_diff, param_shift -from pennylane.interfaces.batch import execute +from pennylane.interfaces import execute class TestAutogradExecuteUnitTests: @@ -35,7 +35,7 @@ def test_import_error(self, mocker): mock.side_effect = ImportError() try: - del sys.modules["pennylane.interfaces.batch.autograd"] + del sys.modules["pennylane.interfaces.autograd"] except: pass @@ -206,7 +206,7 @@ class TestCaching: def test_cache_maxsize(self, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces.batch, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cachesize): with qml.tape.QuantumTape() as tape: @@ -227,7 +227,7 @@ def cost(a, cachesize): def test_custom_cache(self, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces.batch, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cache): with qml.tape.QuantumTape() as tape: diff --git a/tests/interfaces/test_batch_autograd_qnode.py b/tests/interfaces/test_autograd_qnode.py similarity index 100% rename from tests/interfaces/test_batch_autograd_qnode.py rename to tests/interfaces/test_autograd_qnode.py diff --git a/tests/interfaces/test_batch_jax.py b/tests/interfaces/test_jax.py similarity index 95% rename from tests/interfaces/test_batch_jax.py rename to tests/interfaces/test_jax.py index 0b0580b276e..1eb330db655 100644 --- a/tests/interfaces/test_batch_jax.py +++ b/tests/interfaces/test_jax.py @@ -22,8 +22,8 @@ import pennylane as qml from pennylane.gradients import param_shift -from pennylane.interfaces.batch import execute -from pennylane.interfaces.batch import InterfaceUnsupportedError +from pennylane.interfaces import execute +from pennylane.interfaces import InterfaceUnsupportedError @pytest.mark.parametrize("interface", ["jax-jit", "jax-python"]) @@ -198,7 +198,7 @@ class TestCaching: def test_cache_maxsize(self, interface, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces.batch, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cachesize): with qml.tape.QuantumTape() as tape: @@ -225,7 +225,7 @@ def cost(a, cachesize): def test_custom_cache(self, interface, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces.batch, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cache): with qml.tape.QuantumTape() as tape: @@ -247,7 +247,7 @@ def cost(a, cache): def test_custom_cache_multiple(self, interface, mocker): """Test the use of a custom cache object with multiple tapes""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces.batch, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") a = jnp.array(0.1) b = jnp.array(0.2) @@ -630,7 +630,7 @@ def cost(a, cache): qml.RY(a[2], wires=0) qml.expval(qml.PauliZ(1)) - res = qml.interfaces.batch.execute( + res = qml.interfaces.execute( [tape], dev, cache=cache, interface=interface, **execute_kwargs ) return res[0][0] @@ -658,7 +658,7 @@ def cost(a, cache): qml.RY(a[2], wires=0) [qml.apply(r) for r in ret] - res = qml.interfaces.batch.execute( + res = qml.interfaces.execute( # Test only applicable for the jax jit interface [tape], dev, @@ -693,7 +693,7 @@ def cost(a, cache): qml.expval(qml.PauliZ(0)) qml.expval(qml.PauliZ(1)) - res = qml.interfaces.batch.execute( + res = qml.interfaces.execute( [tape], dev, cache=cache, interface="jax-python", **execute_kwargs ) return res[0] @@ -717,7 +717,7 @@ def cost(a, cache): qml.expval(qml.PauliZ(0)) qml.expval(qml.PauliZ(1)) - res = qml.interfaces.batch.execute( + res = qml.interfaces.execute( [tape], dev, cache=cache, interface="jax-python", **execute_kwargs ) return res[0] @@ -827,7 +827,7 @@ def cost(a, cache): qml.expval(qml.PauliZ(0)) qml.expval(qml.PauliZ(1)) - res = qml.interfaces.batch.execute( + res = qml.interfaces.execute( [tape], dev, cache=cache, interface="jax-python", **execute_kwargs ) return res[0] diff --git a/tests/interfaces/test_batch_jax_qnode.py b/tests/interfaces/test_jax_qnode.py similarity index 97% rename from tests/interfaces/test_batch_jax_qnode.py rename to tests/interfaces/test_jax_qnode.py index 159fc742580..d14c6163644 100644 --- a/tests/interfaces/test_batch_jax_qnode.py +++ b/tests/interfaces/test_jax_qnode.py @@ -19,7 +19,7 @@ import pennylane as qml from pennylane import qnode, QNode from pennylane.tape import QuantumTape -from pennylane.interfaces.batch import InterfaceUnsupportedError +from pennylane.interfaces import InterfaceUnsupportedError qubit_device_and_diff_method = [ ["default.qubit", "backprop", "forward", "jax"], diff --git a/tests/interfaces/test_batch_tensorflow.py b/tests/interfaces/test_tensorflow.py similarity index 96% rename from tests/interfaces/test_batch_tensorflow.py rename to tests/interfaces/test_tensorflow.py index 7d0c25157f7..4a78518d270 100644 --- a/tests/interfaces/test_batch_tensorflow.py +++ b/tests/interfaces/test_tensorflow.py @@ -19,7 +19,7 @@ import pennylane as qml from pennylane.gradients import finite_diff, param_shift -from pennylane.interfaces.batch import execute +from pennylane.interfaces import execute tf = pytest.importorskip("tensorflow", minversion="2.1") @@ -133,7 +133,7 @@ class TestCaching: def test_cache_maxsize(self, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces.batch, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") a = tf.Variable([0.1, 0.2]) with tf.GradientTape() as t: @@ -154,7 +154,7 @@ def test_cache_maxsize(self, mocker): def test_custom_cache(self, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces.batch, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") a = tf.Variable([0.1, 0.2]) custom_cache = {} diff --git a/tests/interfaces/test_batch_tensorflow_qnode.py b/tests/interfaces/test_tensorflow_qnode.py similarity index 100% rename from tests/interfaces/test_batch_tensorflow_qnode.py rename to tests/interfaces/test_tensorflow_qnode.py diff --git a/tests/interfaces/test_batch_torch.py b/tests/interfaces/test_torch.py similarity index 96% rename from tests/interfaces/test_batch_torch.py rename to tests/interfaces/test_torch.py index e677d8fc810..99bf4a7d32c 100644 --- a/tests/interfaces/test_batch_torch.py +++ b/tests/interfaces/test_torch.py @@ -21,7 +21,7 @@ import pennylane as qml from pennylane.gradients import finite_diff, param_shift -from pennylane.interfaces.batch import execute +from pennylane.interfaces import execute class TestTorchExecuteUnitTests: @@ -155,7 +155,7 @@ class TestCaching: def test_cache_maxsize(self, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces.batch, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cachesize): with qml.tape.QuantumTape() as tape: @@ -179,7 +179,7 @@ def cost(a, cachesize): def test_custom_cache(self, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces.batch, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cache): with qml.tape.QuantumTape() as tape: diff --git a/tests/interfaces/test_batch_torch_qnode.py b/tests/interfaces/test_torch_qnode.py similarity index 100% rename from tests/interfaces/test_batch_torch_qnode.py rename to tests/interfaces/test_torch_qnode.py From 5aa99bfa63493d97e954d544b94d7d71ff0cf62a Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 14:22:07 +0800 Subject: [PATCH 05/13] rename torch gpu test --- tests/gpu/{test_torch.py => test_gpu_torch.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/gpu/{test_torch.py => test_gpu_torch.py} (100%) diff --git a/tests/gpu/test_torch.py b/tests/gpu/test_gpu_torch.py similarity index 100% rename from tests/gpu/test_torch.py rename to tests/gpu/test_gpu_torch.py From e491ade144c139cde76fa81d997f0bf42510cfb8 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 14:23:35 +0800 Subject: [PATCH 06/13] fix codecov --- codecov.yml | 4 ++-- pennylane/gradients/hamiltonian_grad.py | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/codecov.yml b/codecov.yml index 45693573031..e6ba31df130 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,7 +6,7 @@ ignore: codecov: notify: - after_n_builds: 9 + after_n_builds: 10 comment: - after_n_builds: 9 + after_n_builds: 10 diff --git a/pennylane/gradients/hamiltonian_grad.py b/pennylane/gradients/hamiltonian_grad.py index d901b466061..f9f26e9c796 100644 --- a/pennylane/gradients/hamiltonian_grad.py +++ b/pennylane/gradients/hamiltonian_grad.py @@ -16,24 +16,18 @@ import pennylane as qml -def hamiltonian_grad(tape, idx, params=None): +def hamiltonian_grad(tape, idx): """Computes the tapes necessary to get the gradient of a tape with respect to a Hamiltonian observable's coefficients. Args: tape (qml.tape.QuantumTape): tape with a single Hamiltonian expectation as measurement idx (int): index of parameter that we differentiate with respect to - params (array): explicit parameters to set """ op, p_idx = tape.get_operation(idx) new_tape = tape.copy(copy_operations=True) - if params is not None: - # TODO: remove the params argument when the old QNode is removed - new_tape.set_parameters(params=params) - # get position in queue - queue_position = tape.observables.index(op) new_tape._measurements[queue_position] = qml.expval(op.ops[p_idx]) From d7e704df4e1451e5a6433d9d9e8cac3b71d698b2 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 14:49:36 +0800 Subject: [PATCH 07/13] Reorganize interface source code and documentation --- pennylane/interfaces/__init__.py | 434 ++---------------------------- pennylane/interfaces/execute.py | 396 +++++++++++++++++++++++++++ pennylane/interfaces/set_shots.py | 56 ++++ pennylane/interfaces/torch.py | 2 +- 4 files changed, 478 insertions(+), 410 deletions(-) create mode 100644 pennylane/interfaces/execute.py create mode 100644 pennylane/interfaces/set_shots.py diff --git a/pennylane/interfaces/__init__.py b/pennylane/interfaces/__init__.py index 6e09b200f40..bbd3ae5396c 100644 --- a/pennylane/interfaces/__init__.py +++ b/pennylane/interfaces/__init__.py @@ -12,423 +12,39 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This subpackage defines functions for interfacing devices' batch execution +This subpackage defines functions for interfacing devices' execution capabilities with different machine learning libraries. -""" -# pylint: disable=import-outside-toplevel,too-many-arguments,too-many-branches,protected-access -import contextlib -from functools import wraps -import itertools - -from cachetools import LRUCache -import numpy as np - -import pennylane as qml - - -INTERFACE_NAMES = { - "NumPy": (None,), - "Autograd": ("autograd", "numpy"), # for backwards compatibility - "JAX": ("jax", "jax-jit", "jax-python", "JAX"), - "PyTorch": ("torch", "pytorch"), - "TensorFlow": ("tf", "tensorflow", "tensorflow-autograph", "tf-autograph"), -} -"""dict[str, str]: maps allowed interface strings to the name of the interface""" - -SUPPORTED_INTERFACES = list(itertools.chain(*INTERFACE_NAMES.values())) - - -class InterfaceUnsupportedError(NotImplementedError): - """Exception raised when features not supported by an interface are - attempted to be used.""" - - -@contextlib.contextmanager -def set_shots(device, shots): - """Context manager to temporarily change the shots - of a device. - - This context manager can be used in two ways. - - As a standard context manager: - - >>> dev = qml.device("default.qubit", wires=2, shots=None) - >>> with set_shots(dev, shots=100): - ... print(dev.shots) - 100 - >>> print(dev.shots) - None - - Or as a decorator that acts on a function that uses the device: - - >>> set_shots(dev, shots=100)(lambda: dev.shots)() - 100 - """ - if shots == device.shots: - yield - return - - original_shots = device.shots - original_shot_vector = device._shot_vector - - try: - if shots is not False and device.shots != shots: - device.shots = shots - yield - finally: - device.shots = original_shots - device._shot_vector = original_shot_vector - - -def cache_execute(fn, cache, pass_kwargs=False, return_tuple=True, expand_fn=None): - """Decorator that adds caching to a function that executes - multiple tapes on a device. - - This decorator makes use of :attr:`.QuantumTape.hash` to identify - unique tapes. - - - If a tape does not match a hash in the cache, then the tape - has not been previously executed. It is executed, and the result - added to the cache. - - - If a tape matches a hash in the cache, then the tape has been previously - executed. The corresponding cached result is - extracted, and the tape is not passed to the execution function. - - - Finally, there might be the case where one or more tapes in the current - set of tapes to be executed are identical and thus share a hash. If this is the case, - duplicates are removed, to avoid redundant evaluations. - - Args: - fn (callable): The execution function to add caching to. - This function should have the signature ``fn(tapes, **kwargs)``, - and it should return ``list[tensor_like]``, with the - same length as the input ``tapes``. - cache (None or dict or Cache or bool): The cache to use. If ``None``, - caching will not occur. - pass_kwargs (bool): If ``True``, keyword arguments passed to the - wrapped function will be passed directly to ``fn``. If ``False``, - they will be ignored. - return_tuple (bool): If ``True``, the output of ``fn`` is returned - as a tuple ``(fn_ouput, [])``, to match the output of execution functions - that also return gradients. - - Returns: - function: a wrapped version of the execution function ``fn`` with caching - support - """ - if expand_fn is not None: - original_fn = fn - - def fn(tapes, **kwargs): # pylint: disable=function-redefined - tapes = [expand_fn(tape) for tape in tapes] - return original_fn(tapes, **kwargs) - - @wraps(fn) - def wrapper(tapes, **kwargs): - - if not pass_kwargs: - kwargs = {} - - if cache is None or (isinstance(cache, bool) and not cache): - # No caching. Simply execute the execution function - # and return the results. - res = fn(tapes, **kwargs) - return (res, []) if return_tuple else res - - execution_tapes = {} - cached_results = {} - hashes = {} - repeated = {} - - for i, tape in enumerate(tapes): - h = tape.hash - - if h in hashes.values(): - # Tape already exists within ``tapes``. Determine the - # index of the first occurrence of the tape, store this, - # and continue to the next iteration. - idx = list(hashes.keys())[list(hashes.values()).index(h)] - repeated[i] = idx - continue - - hashes[i] = h - - if hashes[i] in cache: - # Tape exists within the cache, store the cached result - cached_results[i] = cache[hashes[i]] - else: - # Tape does not exist within the cache, store the tape - # for execution via the execution function. - execution_tapes[i] = tape - - # if there are no execution tapes, simply return! - if not execution_tapes: - if not repeated: - res = list(cached_results.values()) - return (res, []) if return_tuple else res - - else: - # execute all unique tapes that do not exist in the cache - res = fn(execution_tapes.values(), **kwargs) - - final_res = [] - - for i, tape in enumerate(tapes): - if i in cached_results: - # insert cached results into the results vector - final_res.append(cached_results[i]) - - elif i in repeated: - # insert repeated results into the results vector - final_res.append(final_res[repeated[i]]) - else: - # insert evaluated results into the results vector - r = res.pop(0) - final_res.append(r) - cache[hashes[i]] = r +.. currentmodule:: pennylane - return (final_res, []) if return_tuple else final_res +Execution functions and utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - wrapper.fn = fn - return wrapper +.. autosummary:: + :toctree: api + ~execute + ~interfaces.cache_execute + ~interfaces.set_shots -def execute( - tapes, - device, - gradient_fn, - interface="autograd", - mode="best", - gradient_kwargs=None, - cache=True, - cachesize=10000, - max_diff=1, - override_shots=False, - expand_fn="device", - max_expansion=10, - device_batch_transform=True, -): - """Execute a batch of tapes on a device in an autodifferentiable-compatible manner. +Supported interfaces +~~~~~~~~~~~~~~~~~~~~ - Args: - tapes (Sequence[.QuantumTape]): batch of tapes to execute - device (.Device): Device to use to execute the batch of tapes. - If the device does not provide a ``batch_execute`` method, - by default the tapes will be executed in serial. - gradient_fn (None or callable): The gradient transform function to use - for backward passes. If "device", the device will be queried directly - for the gradient (if supported). - 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``, and ``jax``. - mode (str): Whether the gradients should be computed on the forward - pass (``forward``) or the backward pass (``backward``). Only applies - if the device is queried for the gradient; gradient transform - functions available in ``qml.gradients`` are only supported on the backward - pass. - gradient_kwargs (dict): dictionary of keyword arguments to pass when - determining the gradients of tapes - cache (bool): Whether to cache evaluations. This can result in - a significant reduction in quantum evaluations during gradient computations. - cachesize (int): the size of the cache - max_diff (int): If ``gradient_fn`` is a gradient transform, this option specifies - the maximum number of derivatives to support. Increasing this value allows - for higher order derivatives to be extracted, at the cost of additional - (classical) computational overhead during the backwards pass. - expand_fn (function): Tape expansion function to be called prior to device execution. - Must have signature of the form ``expand_fn(tape, max_expansion)``, and return a - single :class:`~.QuantumTape`. If not provided, by default :meth:`Device.expand_fn` - is called. - max_expansion (int): The number of times the internal circuit should be expanded when - executed on a device. Expansion occurs when an operation or measurement is not - supported, and results in a gate decomposition. If any operations in the decomposition - remain unsupported by the device, another expansion occurs. - device_batch_transform (bool): Whether to apply any batch transforms defined by the device - (within :meth:`Device.batch_transform`) to each tape to be executed. The default behaviour - of the device batch transform is to expand out Hamiltonian measurements into - constituent terms if not supported on the device. +.. autosummary:: + :toctree: api - Returns: - list[list[float]]: A nested list of tape results. Each element in - the returned list corresponds in order to the provided tapes. + ~interfaces.autograd + ~interfaces.jax + ~interfaces.jax_jit + ~interfaces.tensorflow + ~interfaces.tensorflow_autograph + ~interfaces.torch - **Example** - - Consider the following cost function: - - .. code-block:: python - - dev = qml.device("lightning.qubit", wires=2) - - def cost_fn(params, x): - with qml.tape.QuantumTape() as tape1: - qml.RX(params[0], wires=0) - qml.RY(params[1], wires=0) - qml.expval(qml.PauliZ(0)) - - with qml.tape.QuantumTape() as tape2: - qml.RX(params[2], wires=0) - qml.RY(x[0], wires=1) - qml.CNOT(wires=[0, 1]) - qml.probs(wires=0) - - tapes = [tape1, tape2] - - # execute both tapes in a batch on the given device - res = qml.execute(tapes, dev, qml.gradients.param_shift, max_diff=2) - - return res[0][0] + res[1][0, 0] - res[1][0, 1] - - In this cost function, two **independent** quantum tapes are being - constructed; one returning an expectation value, the other probabilities. - We then batch execute the two tapes, and reduce the results to obtain - a scalar. - - Let's execute this cost function while tracking the gradient: - - >>> params = np.array([0.1, 0.2, 0.3], requires_grad=True) - >>> x = np.array([0.5], requires_grad=True) - >>> cost_fn(params, x) - tensor(1.93050682, requires_grad=True) - - Since the ``execute`` function is differentiable, we can - also compute the gradient: - - >>> qml.grad(cost_fn)(params, x) - (array([-0.0978434 , -0.19767681, -0.29552021]), array([5.37764278e-17])) - - Finally, we can also compute any nth-order derivative. Let's compute the Jacobian - of the gradient (that is, the Hessian): - - >>> x.requires_grad = False - >>> qml.jacobian(qml.grad(cost_fn))(params, x) - array([[-0.97517033, 0.01983384, 0. ], - [ 0.01983384, -0.97517033, 0. ], - [ 0. , 0. , -0.95533649]]) - """ - gradient_kwargs = gradient_kwargs or {} - - if device_batch_transform: - tapes, batch_fn = qml.transforms.map_batch_transform(device.batch_transform, tapes) - else: - batch_fn = lambda res: res - - if isinstance(cache, bool) and cache: - # cache=True: create a LRUCache object - cache = LRUCache(maxsize=cachesize, getsizeof=lambda x: qml.math.shape(x)[0]) - - batch_execute = set_shots(device, override_shots)(device.batch_execute) - - if expand_fn == "device": - expand_fn = lambda tape: device.expand_fn(tape, max_expansion=max_expansion) - - if gradient_fn is None: - # don't unwrap if it's an interface device - if "passthru_interface" in device.capabilities(): - return batch_fn( - cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes) - ) - with qml.tape.Unwrap(*tapes): - res = cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)( - tapes - ) - - return batch_fn(res) - - if gradient_fn == "backprop" or interface is None: - return batch_fn( - cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes) - ) - - # the default execution function is batch_execute - execute_fn = cache_execute(batch_execute, cache, expand_fn=expand_fn) - _mode = "backward" - - if gradient_fn == "device": - # gradient function is a device method - - # Expand all tapes as per the device's expand function here. - # We must do this now, prior to the interface, to ensure that - # decompositions with parameter processing is tracked by the - # autodiff frameworks. - for i, tape in enumerate(tapes): - tapes[i] = expand_fn(tape) - - if mode in ("forward", "best"): - # replace the forward execution function to return - # both results and gradients - execute_fn = set_shots(device, override_shots)(device.execute_and_gradients) - gradient_fn = None - _mode = "forward" - - elif mode == "backward": - # disable caching on the forward pass - execute_fn = cache_execute(batch_execute, cache=None) - - # replace the backward gradient computation - gradient_fn = cache_execute( - set_shots(device, override_shots)(device.gradients), - cache, - pass_kwargs=True, - return_tuple=False, - ) - - elif mode == "forward": - # In "forward" mode, gradients are automatically handled - # within execute_and_gradients, so providing a gradient_fn - # in this case would have ambiguous behaviour. - raise ValueError("Gradient transforms cannot be used with mode='forward'") - - try: - if interface in INTERFACE_NAMES["Autograd"]: - from .autograd import execute as _execute - elif interface in INTERFACE_NAMES["TensorFlow"]: - import tensorflow as tf - - if not tf.executing_eagerly() or "autograph" in interface: - from .tensorflow_autograph import execute as _execute - else: - from .tensorflow import execute as _execute - - elif interface in INTERFACE_NAMES["PyTorch"]: - from .torch import execute as _execute - elif interface in INTERFACE_NAMES["JAX"]: - _execute = _get_jax_execute_fn(interface, tapes) - else: - raise ValueError( - f"Unknown interface {interface}. Supported " - f"interfaces are {SUPPORTED_INTERFACES}" - ) - except ImportError as e: - interface_name = [k for k, v in INTERFACE_NAMES.items() if interface in v][0] - - raise qml.QuantumFunctionError( - f"{interface_name} not found. Please install the latest " - f"version of {interface_name} to enable the '{interface}' interface." - ) from e - - res = _execute( - tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n=1, max_diff=max_diff, mode=_mode - ) - - return batch_fn(res) - - -def _get_jax_execute_fn(interface, tapes): - """Auxiliary function to determine the execute function to use with the JAX - interface.""" - - # The most general JAX interface was sepcified, automatically determine if - # support for jitting is needed by swapping to "jax-jit" or "jax-python" - if interface == "jax": - from .jax import get_jax_interface_name +""" +from .execute import cache_execute, execute, INTERFACE_NAMES, SUPPORTED_INTERFACES +from .set_shots import set_shots - interface = get_jax_interface_name(tapes) - if interface == "jax-jit": - from .jax_jit import execute as _execute - else: - from .jax import execute as _execute - return _execute +class InterfaceUnsupportedError(NotImplementedError): + """Exception raised when features not supported by an interface are + attempted to be used.""" diff --git a/pennylane/interfaces/execute.py b/pennylane/interfaces/execute.py new file mode 100644 index 00000000000..15228d1c3c2 --- /dev/null +++ b/pennylane/interfaces/execute.py @@ -0,0 +1,396 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Contains the cache_execute decoratator, for adding caching to a function +that executes multiple tapes on a device. + +Also contains the general execute function, for exectuting tapes on +devices with autodifferentiation support. +""" +# pylint: disable=import-outside-toplevel,too-many-arguments,too-many-branches +from functools import wraps +import itertools +from cachetools import LRUCache + +import pennylane as qml + +from .set_shots import set_shots + + +INTERFACE_NAMES = { + "NumPy": (None,), + "Autograd": ("autograd", "numpy"), # for backwards compatibility + "JAX": ("jax", "jax-jit", "jax-python", "JAX"), + "PyTorch": ("torch", "pytorch"), + "TensorFlow": ("tf", "tensorflow", "tensorflow-autograph", "tf-autograph"), +} +"""dict[str, str]: maps the name of the interface to allowed interface strings""" + +#: list[str]: allowed interface strings +SUPPORTED_INTERFACES = list(itertools.chain(*INTERFACE_NAMES.values())) +"""list[str]: allowed interface strings""" + + +def cache_execute(fn, cache, pass_kwargs=False, return_tuple=True, expand_fn=None): + """Decorator that adds caching to a function that executes + multiple tapes on a device. + + This decorator makes use of :attr:`.QuantumTape.hash` to identify + unique tapes. + + - If a tape does not match a hash in the cache, then the tape + has not been previously executed. It is executed, and the result + added to the cache. + + - If a tape matches a hash in the cache, then the tape has been previously + executed. The corresponding cached result is + extracted, and the tape is not passed to the execution function. + + - Finally, there might be the case where one or more tapes in the current + set of tapes to be executed are identical and thus share a hash. If this is the case, + duplicates are removed, to avoid redundant evaluations. + + Args: + fn (callable): The execution function to add caching to. + This function should have the signature ``fn(tapes, **kwargs)``, + and it should return ``list[tensor_like]``, with the + same length as the input ``tapes``. + cache (None or dict or Cache or bool): The cache to use. If ``None``, + caching will not occur. + pass_kwargs (bool): If ``True``, keyword arguments passed to the + wrapped function will be passed directly to ``fn``. If ``False``, + they will be ignored. + return_tuple (bool): If ``True``, the output of ``fn`` is returned + as a tuple ``(fn_ouput, [])``, to match the output of execution functions + that also return gradients. + + Returns: + function: a wrapped version of the execution function ``fn`` with caching + support + """ + if expand_fn is not None: + original_fn = fn + + def fn(tapes, **kwargs): # pylint: disable=function-redefined + tapes = [expand_fn(tape) for tape in tapes] + return original_fn(tapes, **kwargs) + + @wraps(fn) + def wrapper(tapes, **kwargs): + + if not pass_kwargs: + kwargs = {} + + if cache is None or (isinstance(cache, bool) and not cache): + # No caching. Simply execute the execution function + # and return the results. + res = fn(tapes, **kwargs) + return (res, []) if return_tuple else res + + execution_tapes = {} + cached_results = {} + hashes = {} + repeated = {} + + for i, tape in enumerate(tapes): + h = tape.hash + + if h in hashes.values(): + # Tape already exists within ``tapes``. Determine the + # index of the first occurrence of the tape, store this, + # and continue to the next iteration. + idx = list(hashes.keys())[list(hashes.values()).index(h)] + repeated[i] = idx + continue + + hashes[i] = h + + if hashes[i] in cache: + # Tape exists within the cache, store the cached result + cached_results[i] = cache[hashes[i]] + else: + # Tape does not exist within the cache, store the tape + # for execution via the execution function. + execution_tapes[i] = tape + + # if there are no execution tapes, simply return! + if not execution_tapes: + if not repeated: + res = list(cached_results.values()) + return (res, []) if return_tuple else res + + else: + # execute all unique tapes that do not exist in the cache + res = fn(execution_tapes.values(), **kwargs) + + final_res = [] + + for i, tape in enumerate(tapes): + if i in cached_results: + # insert cached results into the results vector + final_res.append(cached_results[i]) + + elif i in repeated: + # insert repeated results into the results vector + final_res.append(final_res[repeated[i]]) + + else: + # insert evaluated results into the results vector + r = res.pop(0) + final_res.append(r) + cache[hashes[i]] = r + + return (final_res, []) if return_tuple else final_res + + wrapper.fn = fn + return wrapper + + +def execute( + tapes, + device, + gradient_fn, + interface="autograd", + mode="best", + gradient_kwargs=None, + cache=True, + cachesize=10000, + max_diff=1, + override_shots=False, + expand_fn="device", + max_expansion=10, + device_batch_transform=True, +): + """Execute a batch of tapes on a device in an autodifferentiable-compatible manner. + + Args: + tapes (Sequence[.QuantumTape]): batch of tapes to execute + device (.Device): Device to use to execute the batch of tapes. + If the device does not provide a ``batch_execute`` method, + by default the tapes will be executed in serial. + gradient_fn (None or callable): The gradient transform function to use + for backward passes. If "device", the device will be queried directly + for the gradient (if supported). + 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``, and ``jax``. + mode (str): Whether the gradients should be computed on the forward + pass (``forward``) or the backward pass (``backward``). Only applies + if the device is queried for the gradient; gradient transform + functions available in ``qml.gradients`` are only supported on the backward + pass. + gradient_kwargs (dict): dictionary of keyword arguments to pass when + determining the gradients of tapes + cache (bool): Whether to cache evaluations. This can result in + a significant reduction in quantum evaluations during gradient computations. + cachesize (int): the size of the cache + max_diff (int): If ``gradient_fn`` is a gradient transform, this option specifies + the maximum number of derivatives to support. Increasing this value allows + for higher order derivatives to be extracted, at the cost of additional + (classical) computational overhead during the backwards pass. + expand_fn (function): Tape expansion function to be called prior to device execution. + Must have signature of the form ``expand_fn(tape, max_expansion)``, and return a + single :class:`~.QuantumTape`. If not provided, by default :meth:`Device.expand_fn` + is called. + max_expansion (int): The number of times the internal circuit should be expanded when + executed on a device. Expansion occurs when an operation or measurement is not + supported, and results in a gate decomposition. If any operations in the decomposition + remain unsupported by the device, another expansion occurs. + device_batch_transform (bool): Whether to apply any batch transforms defined by the device + (within :meth:`Device.batch_transform`) to each tape to be executed. The default behaviour + of the device batch transform is to expand out Hamiltonian measurements into + constituent terms if not supported on the device. + + Returns: + list[list[float]]: A nested list of tape results. Each element in + the returned list corresponds in order to the provided tapes. + + **Example** + + Consider the following cost function: + + .. code-block:: python + + dev = qml.device("lightning.qubit", wires=2) + + def cost_fn(params, x): + with qml.tape.QuantumTape() as tape1: + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=0) + qml.expval(qml.PauliZ(0)) + + with qml.tape.QuantumTape() as tape2: + qml.RX(params[2], wires=0) + qml.RY(x[0], wires=1) + qml.CNOT(wires=[0, 1]) + qml.probs(wires=0) + + tapes = [tape1, tape2] + + # execute both tapes in a batch on the given device + res = qml.execute(tapes, dev, qml.gradients.param_shift, max_diff=2) + + return res[0][0] + res[1][0, 0] - res[1][0, 1] + + In this cost function, two **independent** quantum tapes are being + constructed; one returning an expectation value, the other probabilities. + We then batch execute the two tapes, and reduce the results to obtain + a scalar. + + Let's execute this cost function while tracking the gradient: + + >>> params = np.array([0.1, 0.2, 0.3], requires_grad=True) + >>> x = np.array([0.5], requires_grad=True) + >>> cost_fn(params, x) + tensor(1.93050682, requires_grad=True) + + Since the ``execute`` function is differentiable, we can + also compute the gradient: + + >>> qml.grad(cost_fn)(params, x) + (array([-0.0978434 , -0.19767681, -0.29552021]), array([5.37764278e-17])) + + Finally, we can also compute any nth-order derivative. Let's compute the Jacobian + of the gradient (that is, the Hessian): + + >>> x.requires_grad = False + >>> qml.jacobian(qml.grad(cost_fn))(params, x) + array([[-0.97517033, 0.01983384, 0. ], + [ 0.01983384, -0.97517033, 0. ], + [ 0. , 0. , -0.95533649]]) + """ + gradient_kwargs = gradient_kwargs or {} + + if device_batch_transform: + tapes, batch_fn = qml.transforms.map_batch_transform(device.batch_transform, tapes) + else: + batch_fn = lambda res: res + + if isinstance(cache, bool) and cache: + # cache=True: create a LRUCache object + cache = LRUCache(maxsize=cachesize, getsizeof=lambda x: qml.math.shape(x)[0]) + + batch_execute = set_shots(device, override_shots)(device.batch_execute) + + if expand_fn == "device": + expand_fn = lambda tape: device.expand_fn(tape, max_expansion=max_expansion) + + if gradient_fn is None: + # don't unwrap if it's an interface device + if "passthru_interface" in device.capabilities(): + return batch_fn( + cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes) + ) + with qml.tape.Unwrap(*tapes): + res = cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)( + tapes + ) + + return batch_fn(res) + + if gradient_fn == "backprop" or interface is None: + return batch_fn( + cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes) + ) + + # the default execution function is batch_execute + execute_fn = cache_execute(batch_execute, cache, expand_fn=expand_fn) + _mode = "backward" + + if gradient_fn == "device": + # gradient function is a device method + + # Expand all tapes as per the device's expand function here. + # We must do this now, prior to the interface, to ensure that + # decompositions with parameter processing is tracked by the + # autodiff frameworks. + for i, tape in enumerate(tapes): + tapes[i] = expand_fn(tape) + + if mode in ("forward", "best"): + # replace the forward execution function to return + # both results and gradients + execute_fn = set_shots(device, override_shots)(device.execute_and_gradients) + gradient_fn = None + _mode = "forward" + + elif mode == "backward": + # disable caching on the forward pass + execute_fn = cache_execute(batch_execute, cache=None) + + # replace the backward gradient computation + gradient_fn = cache_execute( + set_shots(device, override_shots)(device.gradients), + cache, + pass_kwargs=True, + return_tuple=False, + ) + + elif mode == "forward": + # In "forward" mode, gradients are automatically handled + # within execute_and_gradients, so providing a gradient_fn + # in this case would have ambiguous behaviour. + raise ValueError("Gradient transforms cannot be used with mode='forward'") + + try: + if interface in INTERFACE_NAMES["Autograd"]: + from .autograd import execute as _execute + elif interface in INTERFACE_NAMES["TensorFlow"]: + import tensorflow as tf + + if not tf.executing_eagerly() or "autograph" in interface: + from .tensorflow_autograph import execute as _execute + else: + from .tensorflow import execute as _execute + + elif interface in INTERFACE_NAMES["PyTorch"]: + from .torch import execute as _execute + elif interface in INTERFACE_NAMES["JAX"]: + _execute = _get_jax_execute_fn(interface, tapes) + else: + raise ValueError( + f"Unknown interface {interface}. Supported " + f"interfaces are {SUPPORTED_INTERFACES}" + ) + except ImportError as e: + interface_name = [k for k, v in INTERFACE_NAMES.items() if interface in v][0] + + raise qml.QuantumFunctionError( + f"{interface_name} not found. Please install the latest " + f"version of {interface_name} to enable the '{interface}' interface." + ) from e + + res = _execute( + tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n=1, max_diff=max_diff, mode=_mode + ) + + return batch_fn(res) + + +def _get_jax_execute_fn(interface, tapes): + """Auxiliary function to determine the execute function to use with the JAX + interface.""" + + # The most general JAX interface was sepcified, automatically determine if + # support for jitting is needed by swapping to "jax-jit" or "jax-python" + if interface == "jax": + from .jax import get_jax_interface_name + + interface = get_jax_interface_name(tapes) + + if interface == "jax-jit": + from .jax_jit import execute as _execute + else: + from .jax import execute as _execute + return _execute diff --git a/pennylane/interfaces/set_shots.py b/pennylane/interfaces/set_shots.py new file mode 100644 index 00000000000..3d3643dc1af --- /dev/null +++ b/pennylane/interfaces/set_shots.py @@ -0,0 +1,56 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Contains the set_shots context manager, which allows devices shots +to be temporarily modified. +""" +# pylint: disable=protected-access +import contextlib + + +@contextlib.contextmanager +def set_shots(device, shots): + """Context manager to temporarily change the shots + of a device. + + This context manager can be used in two ways. + + As a standard context manager: + + >>> dev = qml.device("default.qubit", wires=2, shots=None) + >>> with set_shots(dev, shots=100): + ... print(dev.shots) + 100 + >>> print(dev.shots) + None + + Or as a decorator that acts on a function that uses the device: + + >>> set_shots(dev, shots=100)(lambda: dev.shots)() + 100 + """ + if shots == device.shots: + yield + return + + original_shots = device.shots + original_shot_vector = device._shot_vector + + try: + if shots is not False and device.shots != shots: + device.shots = shots + yield + finally: + device.shots = original_shots + device._shot_vector = original_shot_vector diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py index 5d683d679d1..8e261c41538 100644 --- a/pennylane/interfaces/torch.py +++ b/pennylane/interfaces/torch.py @@ -52,7 +52,7 @@ class ExecuteTapes(torch.autograd.Function): for backward passes * ``"gradient_kwargs"``: gradient keyword arguments to pass to the gradient function - * ``"max_diff`"`: the maximum order of derivatives to support + * ``"max_diff``: the maximum order of derivatives to support Further, note that the ``parameters`` argument is dependent on the ``tapes``; this function should always be called From 3f248e1f0701379ed42d2609946488b97f3eaa88 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 15:44:31 +0800 Subject: [PATCH 08/13] fix docs --- pennylane/tape/tape.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py index 251d515a364..46e22df01c9 100644 --- a/pennylane/tape/tape.py +++ b/pennylane/tape/tape.py @@ -254,7 +254,7 @@ class QuantumTape(AnnotatedQueue): Once constructed, the quantum tape can be executed directly on a supported - device via the :func:`~.execute` function: + device via the :func:`~.pennylane.execute` function: >>> dev = qml.device("default.qubit", wires=[0, 'a']) >>> qml.execute([tape], dev, gradient_fn=None) @@ -1294,7 +1294,7 @@ def execute(self, device, params=None): .. warning:: Executing tapes using ``tape.execute(dev)`` is deprecated. - Please use the :func:`~.execute` function instead. + Please use the :func:`~.pennylane.execute` function instead. Args: device (.Device): a PennyLane device @@ -1346,12 +1346,12 @@ def execute_device(self, params, device): This is a low-level method, intended to be called by an interface, and does not support autodifferentiation. - For more details on differentiable tape execution, see :meth:`~.execute`. + For more details on differentiable tape execution, see :func:`~.pennylane.execute`. .. warning:: Executing tapes using ``tape.execute(dev)`` is deprecated. - Please use the :func:`~.execute` function instead. + Please use the :func:`~.pennylane.execute` function instead. Args: device (~.Device): a PennyLane device From 23d7f8aa814e97c8df37a42203bac54893b2ba6b Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 15:52:01 +0800 Subject: [PATCH 09/13] changelog --- doc/releases/changelog-dev.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 8530b8c95e5..b26a0cab2e5 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -21,20 +21,31 @@ before and after the controlled operation [(#2288)](https://github.com/PennyLaneAI/pennylane/pull/2288) +

Deprecations

+

Breaking changes

+* The deprecated QNode, available via `qml.qnode_old.QNode`, has been removed. Please + transition to using the standard `qml.QNode`. + [(#2336)](https://github.com/PennyLaneAI/pennylane/pull/2336) + +* The deprecated, non-batch compatible interfaces, have been removed. + [(#2336)](https://github.com/PennyLaneAI/pennylane/pull/2336) + +* The deprecated tape subclasses `QubitParamShiftTape`, `JacobianTape`, `CVParamShiftTape`, and + `ReversibleTape` have been removed. + [(#2336)](https://github.com/PennyLaneAI/pennylane/pull/2336) +

Bug fixes

* Fixes cases with `qml.measure` where unexpected operations were added to the circuit. [(#2328)](https://github.com/PennyLaneAI/pennylane/pull/2328) -

Deprecations

-

Documentation

Contributors

This release contains contributions from (in alphabetical order): -Karim Alaa El-Din, Guillermo Alonso-Linaje, Anthony Hayes +Karim Alaa El-Din, Guillermo Alonso-Linaje, Anthony Hayes, Josh Izaac From c198c3aa1c3c3a42dcf74c2b0d123c1e2d19e986 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 15:54:18 +0800 Subject: [PATCH 10/13] changelog --- doc/releases/changelog-dev.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index b26a0cab2e5..d3292e94a41 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -28,13 +28,15 @@ * The deprecated QNode, available via `qml.qnode_old.QNode`, has been removed. Please transition to using the standard `qml.QNode`. [(#2336)](https://github.com/PennyLaneAI/pennylane/pull/2336) + [(#2337)](https://github.com/PennyLaneAI/pennylane/pull/2337) + [(#2338)](https://github.com/PennyLaneAI/pennylane/pull/2338) -* The deprecated, non-batch compatible interfaces, have been removed. - [(#2336)](https://github.com/PennyLaneAI/pennylane/pull/2336) + In addition, several other components which powered the deprecated QNode have been removed: -* The deprecated tape subclasses `QubitParamShiftTape`, `JacobianTape`, `CVParamShiftTape`, and - `ReversibleTape` have been removed. - [(#2336)](https://github.com/PennyLaneAI/pennylane/pull/2336) + - The deprecated, non-batch compatible interfaces, have been removed. + + - The deprecated tape subclasses `QubitParamShiftTape`, `JacobianTape`, `CVParamShiftTape`, and + `ReversibleTape` have been removed.

Bug fixes

From abb1bce3c70c2669e3721f5f78133d10e6e23dbc Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 17:25:26 +0800 Subject: [PATCH 11/13] bugfix --- tests/interfaces/test_autograd.py | 7 +++++-- tests/interfaces/test_jax.py | 10 +++++++--- tests/interfaces/test_tensorflow.py | 7 +++++-- tests/interfaces/test_torch.py | 8 ++++++-- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/interfaces/test_autograd.py b/tests/interfaces/test_autograd.py index c20ef93c8b9..6ee7bfacf34 100644 --- a/tests/interfaces/test_autograd.py +++ b/tests/interfaces/test_autograd.py @@ -25,6 +25,9 @@ from pennylane.interfaces import execute +execute_module = sys.modules["pennylane.interfaces.execute"] + + class TestAutogradExecuteUnitTests: """Unit tests for autograd execution""" @@ -206,7 +209,7 @@ class TestCaching: def test_cache_maxsize(self, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces, "cache_execute") + spy = mocker.spy(execute_module, "cache_execute") def cost(a, cachesize): with qml.tape.QuantumTape() as tape: @@ -227,7 +230,7 @@ def cost(a, cachesize): def test_custom_cache(self, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces, "cache_execute") + spy = mocker.spy(execute_module, "cache_execute") def cost(a, cache): with qml.tape.QuantumTape() as tape: diff --git a/tests/interfaces/test_jax.py b/tests/interfaces/test_jax.py index 1eb330db655..0b629e9e4af 100644 --- a/tests/interfaces/test_jax.py +++ b/tests/interfaces/test_jax.py @@ -13,6 +13,7 @@ # limitations under the License. """Unit tests for the jax interface""" import functools +import sys import pytest @@ -26,6 +27,9 @@ from pennylane.interfaces import InterfaceUnsupportedError +execute_module = sys.modules["pennylane.interfaces.execute"] + + @pytest.mark.parametrize("interface", ["jax-jit", "jax-python"]) class TestJaxExecuteUnitTests: """Unit tests for jax execution""" @@ -198,7 +202,7 @@ class TestCaching: def test_cache_maxsize(self, interface, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces, "cache_execute") + spy = mocker.spy(execute_module, "cache_execute") def cost(a, cachesize): with qml.tape.QuantumTape() as tape: @@ -225,7 +229,7 @@ def cost(a, cachesize): def test_custom_cache(self, interface, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces, "cache_execute") + spy = mocker.spy(execute_module, "cache_execute") def cost(a, cache): with qml.tape.QuantumTape() as tape: @@ -247,7 +251,7 @@ def cost(a, cache): def test_custom_cache_multiple(self, interface, mocker): """Test the use of a custom cache object with multiple tapes""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces, "cache_execute") + spy = mocker.spy(execute_module, "cache_execute") a = jnp.array(0.1) b = jnp.array(0.2) diff --git a/tests/interfaces/test_tensorflow.py b/tests/interfaces/test_tensorflow.py index 4a78518d270..259b04da864 100644 --- a/tests/interfaces/test_tensorflow.py +++ b/tests/interfaces/test_tensorflow.py @@ -13,6 +13,7 @@ # limitations under the License. """Unit tests for the TensorFlow interface""" import functools +import sys import numpy as np import pytest @@ -21,6 +22,8 @@ from pennylane.gradients import finite_diff, param_shift from pennylane.interfaces import execute +execute_module = sys.modules["pennylane.interfaces.execute"] + tf = pytest.importorskip("tensorflow", minversion="2.1") @@ -133,7 +136,7 @@ class TestCaching: def test_cache_maxsize(self, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces, "cache_execute") + spy = mocker.spy(execute_module, "cache_execute") a = tf.Variable([0.1, 0.2]) with tf.GradientTape() as t: @@ -154,7 +157,7 @@ def test_cache_maxsize(self, mocker): def test_custom_cache(self, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces, "cache_execute") + spy = mocker.spy(execute_module, "cache_execute") a = tf.Variable([0.1, 0.2]) custom_cache = {} diff --git a/tests/interfaces/test_torch.py b/tests/interfaces/test_torch.py index 99bf4a7d32c..5d098187cfb 100644 --- a/tests/interfaces/test_torch.py +++ b/tests/interfaces/test_torch.py @@ -13,6 +13,7 @@ # limitations under the License. """Unit tests for the Torch interface""" import functools +import sys import numpy as np import pytest @@ -24,6 +25,9 @@ from pennylane.interfaces import execute +execute_module = sys.modules["pennylane.interfaces.execute"] + + class TestTorchExecuteUnitTests: """Unit tests for torch execution""" @@ -155,7 +159,7 @@ class TestCaching: def test_cache_maxsize(self, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces, "cache_execute") + spy = mocker.spy(execute_module, "cache_execute") def cost(a, cachesize): with qml.tape.QuantumTape() as tape: @@ -179,7 +183,7 @@ def cost(a, cachesize): def test_custom_cache(self, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.interfaces, "cache_execute") + spy = mocker.spy(execute_module, "cache_execute") def cost(a, cache): with qml.tape.QuantumTape() as tape: From 3b62d86f3958e5cb205acddff46e0928b0b5ca48 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 17:26:18 +0800 Subject: [PATCH 12/13] Revert "bugfix" This reverts commit abb1bce3c70c2669e3721f5f78133d10e6e23dbc. --- tests/interfaces/test_autograd.py | 7 ++----- tests/interfaces/test_jax.py | 10 +++------- tests/interfaces/test_tensorflow.py | 7 ++----- tests/interfaces/test_torch.py | 8 ++------ 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/tests/interfaces/test_autograd.py b/tests/interfaces/test_autograd.py index 6ee7bfacf34..c20ef93c8b9 100644 --- a/tests/interfaces/test_autograd.py +++ b/tests/interfaces/test_autograd.py @@ -25,9 +25,6 @@ from pennylane.interfaces import execute -execute_module = sys.modules["pennylane.interfaces.execute"] - - class TestAutogradExecuteUnitTests: """Unit tests for autograd execution""" @@ -209,7 +206,7 @@ class TestCaching: def test_cache_maxsize(self, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(execute_module, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cachesize): with qml.tape.QuantumTape() as tape: @@ -230,7 +227,7 @@ def cost(a, cachesize): def test_custom_cache(self, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(execute_module, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cache): with qml.tape.QuantumTape() as tape: diff --git a/tests/interfaces/test_jax.py b/tests/interfaces/test_jax.py index 0b629e9e4af..1eb330db655 100644 --- a/tests/interfaces/test_jax.py +++ b/tests/interfaces/test_jax.py @@ -13,7 +13,6 @@ # limitations under the License. """Unit tests for the jax interface""" import functools -import sys import pytest @@ -27,9 +26,6 @@ from pennylane.interfaces import InterfaceUnsupportedError -execute_module = sys.modules["pennylane.interfaces.execute"] - - @pytest.mark.parametrize("interface", ["jax-jit", "jax-python"]) class TestJaxExecuteUnitTests: """Unit tests for jax execution""" @@ -202,7 +198,7 @@ class TestCaching: def test_cache_maxsize(self, interface, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(execute_module, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cachesize): with qml.tape.QuantumTape() as tape: @@ -229,7 +225,7 @@ def cost(a, cachesize): def test_custom_cache(self, interface, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(execute_module, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cache): with qml.tape.QuantumTape() as tape: @@ -251,7 +247,7 @@ def cost(a, cache): def test_custom_cache_multiple(self, interface, mocker): """Test the use of a custom cache object with multiple tapes""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(execute_module, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") a = jnp.array(0.1) b = jnp.array(0.2) diff --git a/tests/interfaces/test_tensorflow.py b/tests/interfaces/test_tensorflow.py index 259b04da864..4a78518d270 100644 --- a/tests/interfaces/test_tensorflow.py +++ b/tests/interfaces/test_tensorflow.py @@ -13,7 +13,6 @@ # limitations under the License. """Unit tests for the TensorFlow interface""" import functools -import sys import numpy as np import pytest @@ -22,8 +21,6 @@ from pennylane.gradients import finite_diff, param_shift from pennylane.interfaces import execute -execute_module = sys.modules["pennylane.interfaces.execute"] - tf = pytest.importorskip("tensorflow", minversion="2.1") @@ -136,7 +133,7 @@ class TestCaching: def test_cache_maxsize(self, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(execute_module, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") a = tf.Variable([0.1, 0.2]) with tf.GradientTape() as t: @@ -157,7 +154,7 @@ def test_cache_maxsize(self, mocker): def test_custom_cache(self, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(execute_module, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") a = tf.Variable([0.1, 0.2]) custom_cache = {} diff --git a/tests/interfaces/test_torch.py b/tests/interfaces/test_torch.py index 5d098187cfb..99bf4a7d32c 100644 --- a/tests/interfaces/test_torch.py +++ b/tests/interfaces/test_torch.py @@ -13,7 +13,6 @@ # limitations under the License. """Unit tests for the Torch interface""" import functools -import sys import numpy as np import pytest @@ -25,9 +24,6 @@ from pennylane.interfaces import execute -execute_module = sys.modules["pennylane.interfaces.execute"] - - class TestTorchExecuteUnitTests: """Unit tests for torch execution""" @@ -159,7 +155,7 @@ class TestCaching: def test_cache_maxsize(self, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(execute_module, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cachesize): with qml.tape.QuantumTape() as tape: @@ -183,7 +179,7 @@ def cost(a, cachesize): def test_custom_cache(self, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(execute_module, "cache_execute") + spy = mocker.spy(qml.interfaces, "cache_execute") def cost(a, cache): with qml.tape.QuantumTape() as tape: From 4cf1e9c95cfe4c139afb6ae4d200a5745aeace1f Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 16 Mar 2022 17:28:19 +0800 Subject: [PATCH 13/13] fix --- pennylane/interfaces/__init__.py | 2 +- .../interfaces/{execute.py => execution.py} | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) rename pennylane/interfaces/{execute.py => execution.py} (93%) diff --git a/pennylane/interfaces/__init__.py b/pennylane/interfaces/__init__.py index bbd3ae5396c..8488070a76f 100644 --- a/pennylane/interfaces/__init__.py +++ b/pennylane/interfaces/__init__.py @@ -41,7 +41,7 @@ ~interfaces.torch """ -from .execute import cache_execute, execute, INTERFACE_NAMES, SUPPORTED_INTERFACES +from .execution import cache_execute, execute, INTERFACE_NAMES, SUPPORTED_INTERFACES from .set_shots import set_shots diff --git a/pennylane/interfaces/execute.py b/pennylane/interfaces/execution.py similarity index 93% rename from pennylane/interfaces/execute.py rename to pennylane/interfaces/execution.py index 15228d1c3c2..2758da47f86 100644 --- a/pennylane/interfaces/execute.py +++ b/pennylane/interfaces/execution.py @@ -290,22 +290,26 @@ def cost_fn(params, x): # don't unwrap if it's an interface device if "passthru_interface" in device.capabilities(): return batch_fn( - cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes) + qml.interfaces.cache_execute( + batch_execute, cache, return_tuple=False, expand_fn=expand_fn + )(tapes) ) with qml.tape.Unwrap(*tapes): - res = cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)( - tapes - ) + res = qml.interfaces.cache_execute( + batch_execute, cache, return_tuple=False, expand_fn=expand_fn + )(tapes) return batch_fn(res) if gradient_fn == "backprop" or interface is None: return batch_fn( - cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes) + qml.interfaces.cache_execute( + batch_execute, cache, return_tuple=False, expand_fn=expand_fn + )(tapes) ) # the default execution function is batch_execute - execute_fn = cache_execute(batch_execute, cache, expand_fn=expand_fn) + execute_fn = qml.interfaces.cache_execute(batch_execute, cache, expand_fn=expand_fn) _mode = "backward" if gradient_fn == "device": @@ -327,10 +331,10 @@ def cost_fn(params, x): elif mode == "backward": # disable caching on the forward pass - execute_fn = cache_execute(batch_execute, cache=None) + execute_fn = qml.interfaces.cache_execute(batch_execute, cache=None) # replace the backward gradient computation - gradient_fn = cache_execute( + gradient_fn = qml.interfaces.cache_execute( set_shots(device, override_shots)(device.gradients), cache, pass_kwargs=True,