diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index bd566c999e6..77c12483dde 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -4,6 +4,10 @@ ### Breaking changes +* Deprecated the old `QNode` such that only the new `QNode` and its syntax can be used, + moved all related files from the `pennylane/beta` folder to `pennylane`. + [#440](https://github.com/XanaduAI/pennylane/pull/440) + ### Improvements ### Documentation @@ -19,7 +23,7 @@ This release contains contributions from (in alphabetical order): -Ville Bergholm +Ville Bergholm, Josh Izaac, Antal Száva --- diff --git a/doc/code/qml_beta.rst b/doc/code/qml_beta.rst index 1dbd43289d4..5c55e9a9e3d 100644 --- a/doc/code/qml_beta.rst +++ b/doc/code/qml_beta.rst @@ -6,17 +6,10 @@ and features. .. currentmodule:: pennylane.beta -.. automodapi:: pennylane.beta.interfaces - :include-all-objects: - :no-inheritance-diagram: - .. automodapi:: pennylane.beta.plugins :include-all-objects: :no-inheritance-diagram: -.. automodapi:: pennylane.beta.qnodes - :include-all-objects: - .. automodapi:: pennylane.beta.vqe :include-all-objects: :no-inheritance-diagram: diff --git a/doc/code/qml_operation.rst b/doc/code/qml_operation.rst index 4631d4d3bd9..1c1974dabe6 100644 --- a/doc/code/qml_operation.rst +++ b/doc/code/qml_operation.rst @@ -14,4 +14,4 @@ qml.operation .. automodapi:: pennylane.operation :no-heading: :include-all-objects: - :skip: QNode, Sequence, Enum, IntEnum, Variable, ClassPropertyDescriptor + :skip: Sequence, Enum, IntEnum, Variable, ClassPropertyDescriptor diff --git a/doc/code/qml_qnodes.rst b/doc/code/qml_qnodes.rst new file mode 100644 index 00000000000..83c3892d6a2 --- /dev/null +++ b/doc/code/qml_qnodes.rst @@ -0,0 +1,16 @@ +qml.qnodes +========== + +.. warning:: + + Unless you are a PennyLane or plugin developer, you likely do not need + to use these classes directly. + + See the :doc:`quantum circuits <../introduction/circuits>` page for more + details on creating QNodes, as well as the :func:`~.qnode` decorator + and :func:`~.QNode` constructor. + +.. automodapi:: pennylane.qnodes + :no-heading: + :include-all-objects: + :skip: qnode, QNode, QuantumFunctionError diff --git a/doc/conf.py b/doc/conf.py index fa5df89d6d0..2fcd8c65563 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,7 +48,7 @@ ] autosummary_generate = True -autosummary_imported_members = True +autosummary_imported_members = False automodapi_toctreedirnm = "code/api" automodsumm_inherited_members = True diff --git a/doc/index.rst b/doc/index.rst index c30d1868449..67fe12026be 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -194,6 +194,7 @@ PennyLane is **free** and **open source**, released under the Apache License, Ve code/qml_interfaces code/qml_operation code/qml_plugins + code/qml_qnodes code/qml_templates code/qml_utils code/qml_variable diff --git a/doc/requirements.txt b/doc/requirements.txt index 5fc7387b9dc..77d738346dd 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,11 +1,12 @@ --find-links https://download.pytorch.org/whl/torch_stable.html +pip==19.3.1 appdirs autograd numpy pygments-github-lexers semantic_version==2.6 scipy -sphinx==1.8.5 +sphinx==2.2.2 sphinx-automodapi sphinx-copybutton sphinxcontrib-bibtex==0.4.2 diff --git a/doc/xanadu_theme/layout.html b/doc/xanadu_theme/layout.html index 41e88eebe22..d4118e57ebb 100644 --- a/doc/xanadu_theme/layout.html +++ b/doc/xanadu_theme/layout.html @@ -2,9 +2,6 @@ {# Do this so that bootstrap is included before the main css file #} {%- block htmltitle %} - {% set script_files = script_files + ["_static/myscript.js"] %} - - {% set script_files = script_files + ["_static/myscript.js"] %} diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 88ef51ececf..b41f0097669 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -36,12 +36,11 @@ from .measure import expval, var, sample from .ops import * from .optimize import * -from .qnode import QNode, QuantumFunctionError +from .qnodes import qnode, QNode, QuantumFunctionError from ._version import __version__ -# NOTE: this has to be imported last, -# otherwise it will clash with the .qnode import. -from .decorator import qnode + +_current_context = None # overwrite module docstrings diff --git a/pennylane/_device.py b/pennylane/_device.py index e139748fac3..4812b78ff71 100644 --- a/pennylane/_device.py +++ b/pennylane/_device.py @@ -20,7 +20,7 @@ import numpy as np from pennylane.operation import Operation, Observable, Sample, Variance, Expectation, Tensor -from .qnode import QuantumFunctionError +from pennylane.qnodes import QuantumFunctionError class DeviceError(Exception): diff --git a/pennylane/beta/__init__.py b/pennylane/beta/__init__.py index c685f7da101..0a97d1a5f26 100644 --- a/pennylane/beta/__init__.py +++ b/pennylane/beta/__init__.py @@ -14,6 +14,4 @@ """ This module contains experimental, contributed, and beta code. """ -import pennylane.beta.interfaces -import pennylane.beta.qnodes import pennylane.beta.vqe diff --git a/pennylane/beta/interfaces/__init__.py b/pennylane/beta/interfaces/__init__.py deleted file mode 100644 index 6354d099ba8..00000000000 --- a/pennylane/beta/interfaces/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2019 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. -r""" -This package contains experimental QNode interfaces for PennyLane. - -.. currentmodule:: pennylane.beta.interfaces -""" -from .autograd import to_autograd diff --git a/pennylane/beta/plugins/expt_tensornet_tf.py b/pennylane/beta/plugins/expt_tensornet_tf.py index 5b29dc6309f..5aa1ee07370 100644 --- a/pennylane/beta/plugins/expt_tensornet_tf.py +++ b/pennylane/beta/plugins/expt_tensornet_tf.py @@ -202,14 +202,10 @@ class TensorNetworkTF(TensorNetwork): pip install tensornetwork>=0.2 tensorflow>=2.0 - In addition, you will need to use the new-style QNode to enable - calculation of the quantum gradient using TensorFlow. - **Example:** - >>> from pennylane.beta.qnodes import qnode >>> dev = qml.device("expt.tensornet.tf", wires=1) - >>> @qnode(dev, interface="autograd", diff_method="best") + >>> @qml.qnode(dev, interface="autograd", diff_method="best") >>> def circuit(x): ... qml.RX(x[1], wires=0) ... qml.Rot(x[0], x[1], x[2], wires=0) diff --git a/pennylane/beta/vqe/vqe.py b/pennylane/beta/vqe/vqe.py index aa482a48e57..0549d0ed49e 100644 --- a/pennylane/beta/vqe/vqe.py +++ b/pennylane/beta/vqe/vqe.py @@ -18,7 +18,7 @@ import numpy as np from pennylane.ops import Observable from pennylane.measure import expval -from pennylane.qnode import QNode +from pennylane.qnodes import QNode class Hamiltonian: @@ -102,7 +102,7 @@ def circuit(params): ansatz (callable): the ansatz for the circuit before the final measurement step observables (Iterable[:class:`~.Observable`]): observables to measure during the final step of each circuit device (:class:`~.Device`): device where the circuits should be executed - interface (str): which interface to use for the :class:`~.QNode`s of the circuits + interface (str): which interface to use for the circuit QNodes Returns: tuple: callable functions which evaluate each observable @@ -122,12 +122,7 @@ def circuit(*params, obs=obs): ansatz(*params, wires=range(device.num_wires)) return expval(obs) - qnode = QNode(circuit, device) - - if interface == "tf": - qnode = qnode.to_tf() - elif interface == "torch": - qnode = qnode.to_torch() + qnode = QNode(circuit, device, interface=interface) qnodes.append(qnode) diff --git a/pennylane/decorator.py b/pennylane/decorator.py deleted file mode 100644 index 93f9ccb71d6..00000000000 --- a/pennylane/decorator.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2018-2019 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 :func:`qnode` decorator, which turns a quantum function into -a :class:`Qnode` object. -""" - -# pylint: disable=redefined-outer-name -from functools import wraps, lru_cache - -from .qnode import QNode - - -def qnode(device, interface="numpy", cache=False): - """QNode decorator. - - Args: - device (~pennylane._device.Device): a PennyLane-compatible device - interface (str): the interface that will be used for automatic - differentiation and classical processing. This affects - the types of objects that can be passed to/returned from the QNode: - - * ``interface='numpy'``: The QNode accepts default Python types - (floats, ints, lists) as well as NumPy array arguments, - and returns NumPy arrays. - - * ``interface='torch'``: The QNode accepts and returns Torch tensors. - - * ``interface='tfe'``: The QNode accepts and returns eager execution - TensorFlow ``tfe.Variable`` objects. - - cache (bool): If ``True``, the quantum function used to generate the QNode will - only be called to construct the quantum circuit once, on first execution, - and this circuit structure (i.e., the placement of templates, gates, measurements, etc.) will be cached for - all further executions. The circuit parameters can still change with every call. Only activate this - feature if your quantum circuit structure will never change. - """ - - @lru_cache() - def qfunc_decorator(func): - """The actual decorator""" - - qnode = QNode(func, device, cache=cache) - - if interface == "torch": - return qnode.to_torch() - - if interface == "tf": - return qnode.to_tf() - - @wraps(func) - def wrapper(*args, **kwargs): - """Wrapper function""" - return qnode(*args, **kwargs) - - # bind the jacobian method to the wrapped function - wrapper.jacobian = qnode.jacobian - wrapper.metric_tensor = qnode.metric_tensor - wrapper.interface = interface - wrapper.print_applied = qnode.print_applied - - # bind the qnode attributes to the wrapped function - wrapper.__dict__.update(qnode.__dict__) - - return wrapper - - return qfunc_decorator diff --git a/pennylane/interfaces/__init__.py b/pennylane/interfaces/__init__.py index 2b48018ff0c..a51cfca195e 100644 --- a/pennylane/interfaces/__init__.py +++ b/pennylane/interfaces/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This subpackage defines quantum nodes that are compatible with different machine +This subpackage defines functions convert QNodes to interface with different machine learning libraries. .. currentmodule:: pennylane.interfaces @@ -21,4 +21,5 @@ tf torch + autograd """ diff --git a/pennylane/beta/interfaces/autograd.py b/pennylane/interfaces/autograd.py similarity index 100% rename from pennylane/beta/interfaces/autograd.py rename to pennylane/interfaces/autograd.py diff --git a/pennylane/interfaces/tf.py b/pennylane/interfaces/tf.py index 157400ef985..65e2bfe7d7b 100644 --- a/pennylane/interfaces/tf.py +++ b/pennylane/interfaces/tf.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains the :func:`TFQNode` function to convert Numpy-interfacing quantum nodes to TensorFlow +This module contains the :func:`to_tf` function to convert Numpy-interfacing quantum nodes to TensorFlow compatible quantum nodes. """ # pylint: disable=redefined-outer-name @@ -31,7 +31,7 @@ from tensorflow import Variable # pylint: disable=unused-import,ungrouped-imports -def TFQNode(qnode): +def to_tf(qnode): """Function that accepts a :class:`~.QNode`, and returns a TensorFlow eager-execution-compatible QNode. Args: @@ -85,12 +85,7 @@ def grad(grad_output, **tfkwargs): # evaluate the Jacobian matrix of the QNode variables = tfkwargs.get('variables', None) - if hasattr(qnode, "to_autograd"): - # new style QNode.jacobian has a different signature - jacobian = qnode.jacobian(args, kwargs) - else: - jacobian = qnode.jacobian(args, **kwargs) - + jacobian = qnode.jacobian(args, kwargs) grad_output_np = grad_output.numpy() # perform the vector-Jacobian product diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py index 2db1c5f554c..65c833cd128 100644 --- a/pennylane/interfaces/torch.py +++ b/pennylane/interfaces/torch.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains the :func:`TorchQNode` function to convert Numpy-interfacing quantum nodes to PyTorch +This module contains the :func:`to_torch` function to convert Numpy-interfacing quantum nodes to PyTorch compatible quantum nodes. """ # pylint: disable=redefined-outer-name,arguments-differ @@ -95,7 +95,7 @@ def kwargs_to_numpy(kwargs): return res -def TorchQNode(qnode): +def to_torch(qnode): """Function that accepts a :class:`~.QNode`, and returns a PyTorch-compatible QNode. Args: @@ -140,11 +140,7 @@ def backward(ctx, grad_output): #pragma: no cover # the way in which the backward class is created on the fly # evaluate the Jacobian matrix of the QNode - if hasattr(qnode, "to_autograd"): - # new style QNode.jacobian has a different signature - jacobian = qnode.jacobian(ctx.args, ctx.kwargs) - else: - jacobian = qnode.jacobian(ctx.args, **ctx.kwargs) + jacobian = qnode.jacobian(ctx.args, ctx.kwargs) if grad_output.is_cuda: # pragma: no cover grad_output_np = grad_output.cpu().detach().numpy() diff --git a/pennylane/measure.py b/pennylane/measure.py index 9cb3b3d88cc..de2fbc80274 100644 --- a/pennylane/measure.py +++ b/pennylane/measure.py @@ -17,14 +17,15 @@ outcomes from quantum observables - expectation values, variances of expectations, and measurement samples. """ -from .qnode import QNode, QuantumFunctionError +import pennylane as qml from .operation import Observable, Sample, Variance, Expectation, Tensor +from .qnodes import QuantumFunctionError def _remove_if_in_queue(op): r"""Helper function to handle removing ops from the QNode queue""" - if op in QNode._current_context.queue: - QNode._current_context.queue.remove(op) + if op in qml._current_context.queue: + qml._current_context.queue.remove(op) def expval(op): @@ -41,7 +42,7 @@ def expval(op): "{} is not an observable: cannot be used with expval".format(op.name) ) - if QNode._current_context is not None: + if qml._current_context is not None: # delete observables from QNode operation queue if needed if isinstance(op, Tensor): for o in op.obs: @@ -52,9 +53,9 @@ def expval(op): # set return type to be an expectation value op.return_type = Expectation - if QNode._current_context is not None: + if qml._current_context is not None: # add observable to QNode observable queue - QNode._current_context._append_op(op) + qml._current_context._append_op(op) return op @@ -73,20 +74,20 @@ def var(op): "{} is not an observable: cannot be used with var".format(op.name) ) - if QNode._current_context is not None: + if qml._current_context is not None: # delete operations from QNode queue if isinstance(op, Tensor): for o in op.obs: - QNode._current_context.queue.remove(o) + qml._current_context.queue.remove(o) else: - QNode._current_context.queue.remove(op) + qml._current_context.queue.remove(op) # set return type to be a variance op.return_type = Variance - if QNode._current_context is not None: + if qml._current_context is not None: # add observable to QNode observable queue - QNode._current_context._append_op(op) + qml._current_context._append_op(op) return op @@ -106,19 +107,19 @@ def sample(op): "{} is not an observable: cannot be used with sample".format(op.name) ) - if QNode._current_context is not None: + if qml._current_context is not None: # delete operations from QNode queue if isinstance(op, Tensor): for o in op.obs: - QNode._current_context.queue.remove(o) + qml._current_context.queue.remove(o) else: - QNode._current_context.queue.remove(op) + qml._current_context.queue.remove(op) # set return type to be a sample op.return_type = Sample - if QNode._current_context is not None: + if qml._current_context is not None: # add observable to QNode observable queue - QNode._current_context._append_op(op) + qml._current_context._append_op(op) return op diff --git a/pennylane/operation.py b/pennylane/operation.py index 3012bc5cf08..14b2d2d8d55 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -20,7 +20,7 @@ ----------- Qubit Operations ----------------- +~~~~~~~~~~~~~~~~ The :class:`Operator` class serves as a base class for operators, and is inherited by both the :class:`Observable` class and the :class:`Operation` class. These classes are subclassed to implement quantum operations @@ -106,7 +106,8 @@ import numpy as np -from .qnode import QNode +import pennylane as qml + from .utils import _flatten, _unflatten from .variable import Variable @@ -215,9 +216,9 @@ class Operator(abc.ABC): wires (Sequence[int]): Subsystems it acts on. If not given, args[-1] is interpreted as wires. do_queue (bool): Indicates whether the operator should be - immediately pushed into a :class:`QNode` circuit queue. + immediately pushed into a :class:`BaseQNode` circuit queue. The circuit queue is determined by the presence of an - applicable `QNode._current_context`. If no context is + applicable `qml._current_context`. If no context is available, this argument is ignored. """ @property @@ -273,7 +274,7 @@ def __init__(self, *params, wires=None, do_queue=True): self._check_wires(wires) self._wires = wires #: tuple[int]: wires on which the operator acts - if do_queue and (QNode._current_context is not None): + if do_queue and (qml._current_context is not None): self.queue() def __str__(self): @@ -370,9 +371,9 @@ def parameters(self): return _unflatten(temp_val, self.params)[0] def queue(self): - """Append the operator to a QNode queue.""" + """Append the operator to a BaseQNode queue.""" - QNode._current_context._append_op(self) + qml._current_context._append_op(self) return self # so pre-constructed Observable instances can be queued and returned in a single statement #============================================================================= @@ -408,9 +409,9 @@ class Operation(Operator): wires (Sequence[int]): Subsystems it acts on. If not given, args[-1] is interpreted as wires. do_queue (bool): Indicates whether the operation should be - immediately pushed into a :class:`QNode` circuit queue. + immediately pushed into a :class:`BaseQNode` circuit queue. This flag is useful if there is some reason to run an Operation - outside of a QNode context. + outside of a BaseQNode context. """ # pylint: disable=abstract-method string_for_inverse = ".inv" @@ -576,9 +577,9 @@ class Observable(Operator): wires (Sequence[int]): subsystems it acts on. Currently, only one subsystem is supported. do_queue (bool): Indicates whether the operation should be - immediately pushed into a :class:`QNode` observable queue. + immediately pushed into a :class:`BaseQNode` observable queue. The observable queue is determined by the presence of an - applicable `QNode._current_context`. If no context is + applicable `qml._current_context`. If no context is available, this argument is ignored. """ # pylint: disable=abstract-method diff --git a/pennylane/optimize/qng.py b/pennylane/optimize/qng.py index aff3f529146..e156e87aeeb 100644 --- a/pennylane/optimize/qng.py +++ b/pennylane/optimize/qng.py @@ -126,7 +126,7 @@ def step(self, qnode, x, recompute_tensor=True): if recompute_tensor or self.metric_tensor is None: # pseudo-inverse metric tensor - self.metric_tensor = qnode.metric_tensor(x, diag_approx=self.diag_approx) + self.metric_tensor = qnode.metric_tensor([x], diag_approx=self.diag_approx) self.metric_tensor += self.lam * np.identity(self.metric_tensor.shape[0]) g = self.compute_grad(qnode, x) diff --git a/pennylane/qnode.py b/pennylane/qnode.py deleted file mode 100644 index ffb5615660e..00000000000 --- a/pennylane/qnode.py +++ /dev/null @@ -1,1249 +0,0 @@ -# Copyright 2018-2019 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. -# pylint: disable=cell-var-from-loop,attribute-defined-outside-init,too-many-branches,too-many-arguments - -""" -This module contains the :class:`QNode` class and its helper functions. -""" -from collections.abc import Sequence -from collections import namedtuple -import copy -import itertools -import numbers - -import numpy as np -import autograd.extend as ae -import autograd.builtins -from scipy import linalg - -import pennylane as qml -from pennylane.utils import _flatten, unflatten, _inv_dict, _get_default_args, expand -from pennylane.circuit_graph import CircuitGraph -from pennylane.variable import Variable - - -def pop_jacobian_kwargs(kwargs): - """Remove QNode.jacobian specific keyword arguments from a dictionary. - - This is required to correctly pass the user-defined - keyword arguments to the QNode quantum function. - - Args: - kwargs (dict): dictionary of keyword arguments - - Returns: - dict: keyword arguments with all QNode.jacobian - keyword arguments removed - """ - # TODO: refactor QNode.jacobian to pass all gradient - # specific options under a single `gradient_options` - # dictionary, allowing this function to be removed. - circuit_kwargs = {} - circuit_kwargs.update(kwargs) - - for k in ('h', 'order', 'shots', 'force_order2'): - circuit_kwargs.pop(k, None) - - return circuit_kwargs - - -def _decompose_queue(ops, device): - """Recursively loop through a queue and decompose - operations that are not supported by a device. - - Args: - ops (List[~.Operation]): operation queue - device (~.Device): a PennyLane device - """ - new_ops = [] - - for op in ops: - if device.supports_operation(op.name): - new_ops.append(op) - else: - decomposed_ops = op.decomposition(*op.params, wires=op.wires) - decomposition = _decompose_queue(decomposed_ops, device) - new_ops.extend(decomposition) - - return new_ops - - -def decompose_queue(ops, device): - """Decompose operations in a queue that are not supported by a device. - - This is a wrapper function for :func:`~._decompose_queue`, - which raises an error if an operation or its decomposition - is not supported by the device. - - Args: - ops (List[~.Operation]): operation queue - device (~.Device): a PennyLane device - """ - new_ops = [] - - for op in ops: - try: - new_ops.extend(_decompose_queue([op], device)) - except NotImplementedError: - raise qml.DeviceError("Gate {} not supported on device {}".format(op.name, device.short_name)) - - return new_ops - - -class QuantumFunctionError(Exception): - """Exception raised when an illegal operation is defined in a quantum function.""" - - -ParameterDependency = namedtuple("ParameterDependency", ["op", "par_idx"]) -"""Represents the dependence of an Operation on a free parameter. - -Args: - op (Operation): operation depending on the free parameter in question - par_idx (int): operation parameter index of the corresponding :class:`~.Variable` instance -""" - - -class QNode: - """Quantum node in the hybrid computational graph. - - Args: - func (callable): a Python function containing :class:`~.operation.Operation` - constructor calls, returning a tuple of measured :class:`~.operation.Observable` instances. - device (:class:`~pennylane._device.Device`): device to execute the function on - cache (bool): If ``True``, the quantum function used to generate the QNode will - only be called to construct the quantum circuit once, on first execution, - and this circuit structure (i.e., the placement of templates, gates, measurements, etc.) will be cached for all further executions. The circuit parameters can still change with every call. Only activate this - feature if your quantum circuit structure will never change. - """ - # pylint: disable=too-many-instance-attributes - _current_context = None #: QNode: for building Operation sequences by executing quantum circuit functions - - def __init__(self, func, device, cache=False): - self.func = func - self.device = device - self.num_wires = device.num_wires - self.num_variables = None - self.ops = [] - - self.cache = cache - - self.variable_deps = {} - """dict[int, list[ParameterDependency]]: Mapping from free parameter index to the list of - :class:`~pennylane.operation.Operation` instances (in this circuit) that depend on it. - """ - - self._metric_tensor_subcircuits = {} - - @property - def interface(self): - """String representing the QNode interface""" - return "numpy" - - def __str__(self): - """String representation""" - detail = "" - return detail.format(self.device.short_name, self.func.__name__, self.num_wires) - - def __repr__(self): - """REPL representation""" - return self.__str__() - - def _append_op(self, op): - """Appends a quantum operation into the circuit queue. - - Args: - op (:class:`~.operation.Operation`): quantum operation to be added to the circuit - - Raises: - ValueError: if `op` does not act on all wires - QuantumFunctionError: if state preparations and gates do not precede measured observables - """ - if op.num_wires == qml.operation.Wires.All: - if set(op.wires) != set(range(self.num_wires)): - raise ValueError("Operator {} must act on all wires".format(op.name)) - - # EVs go to their own, temporary queue - if isinstance(op, qml.operation.Observable): - if op.return_type is None: - self.queue.append(op) - else: - self.ev.append(op) - else: - if self.ev: - raise QuantumFunctionError('State preparations and gates must precede measured observables.') - self.queue.append(op) - - def print_applied(self): - """Prints the most recently applied operations from the QNode.""" - if not self.ops: - print("QNode has not yet been executed.") - return - - print("Operations") - print("==========") - for op in self.queue: - if op.parameters: - params = ", ".join([str(p) for p in op.parameters]) - print("{}({}, wires={})".format(op.name, params, op.wires)) - else: - print("{}(wires={})".format(op.name, op.wires)) - - return_map = { - qml.operation.Expectation: "expval", - qml.operation.Variance: "var", - qml.operation.Sample: "sample" - } - - print("\nObservables") - print("===========") - for op in self.ev: - return_type = return_map[op.return_type] - if op.parameters: - params = "".join([str(p) for p in op.parameters]) - print("{}({}({}, wires={}))".format(return_type, op.name, params, op.wires)) - else: - print("{}({}(wires={}))".format(return_type, op.name, op.wires)) - - def construct(self, args, kwargs=None): - """Constructs a representation of the quantum circuit. - - The user should never have to call this method. - - This method is called automatically the first time :meth:`QNode.evaluate` - or :meth:`QNode.jacobian` is called. It executes the quantum function, - stores the resulting sequence of :class:`~.operation.Operation` instances, - and creates the variable mapping. - - Args: - args (tuple): Represent the free parameters passed to the circuit. - Here we are not concerned with their values, but with their structure. - Each free param is replaced with a :class:`~.variable.Variable` instance. - kwargs (dict): Additional keyword arguments may be passed to the quantum circuit function, - however PennyLane does not support differentiating with respect to keyword arguments. - Instead, keyword arguments are useful for providing data or 'placeholders' - to the quantum circuit function. - - Raises: - QuantumFunctionError: if the :class:`pennylane.QNode`'s _current_context is attempted to be modified - inside of this method, the quantum function returns incorrect values or if - both continuous and discrete operations are specified in the same quantum circuit - """ - # pylint: disable=too-many-branches,too-many-statements - self.queue = [] - self.ev = [] # temporary queue for EVs - - if kwargs is None: - kwargs = {} - - # flatten the args, replace each with a Variable instance with a unique index - temp = [Variable(idx) for idx, val in enumerate(_flatten(args))] - self.num_variables = len(temp) - - # store the nested shape of the arguments for later unflattening - self.model = args - - # arrange the newly created Variables in the nested structure of args - variables = unflatten(temp, args) - - # get default kwargs that weren't passed - keyword_sig = _get_default_args(self.func) - self.keyword_defaults = {k: v[1] for k, v in keyword_sig.items()} - self.keyword_positions = {v[0]: k for k, v in keyword_sig.items()} - - keyword_values = {} - keyword_values.update(self.keyword_defaults) - keyword_values.update(kwargs) - - if self.cache: - # caching mode, must use variables for kwargs - # wrap each keyword argument as a Variable - kwarg_variables = {} - for key, val in keyword_values.items(): - temp = [Variable(idx, name=key) for idx, _ in enumerate(_flatten(val))] - kwarg_variables[key] = unflatten(temp, val) - - Variable.free_param_values = np.array(list(_flatten(args))) - Variable.kwarg_values = {k: np.array(list(_flatten(v))) for k, v in keyword_values.items()} - - # set up the context for Operation entry - if QNode._current_context is None: - QNode._current_context = self - else: - raise QuantumFunctionError('QNode._current_context must not be modified outside this method.') - # generate the program queue by executing the quantum circuit function - try: - if self.cache: - # caching mode, must use variables for kwargs - # so they can be updated without reconstructing - res = self.func(*variables, **kwarg_variables) - else: - # no caching, fine to directly pass kwarg values - res = self.func(*variables, **keyword_values) - finally: - # remove the context - QNode._current_context = None - - #---------------------------------------------------------- - # check the validity of the circuit - - # quantum circuit function return validation - if isinstance(res, qml.operation.Observable): - if res.return_type is qml.operation.Sample: - # Squeezing ensures that there is only one array of values returned - # when only a single-mode sample is requested - self.output_conversion = np.squeeze - else: - self.output_conversion = float - - self.output_dim = 1 - res = (res,) - elif isinstance(res, Sequence) and res and all(isinstance(x, qml.operation.Observable) for x in res): - # for multiple observables values, any valid Python sequence of observables - # (i.e., lists, tuples, etc) are supported in the QNode return statement. - - # Device already returns the correct numpy array, - # so no further conversion is required - self.output_conversion = np.asarray - self.output_dim = len(res) - - res = tuple(res) - else: - raise QuantumFunctionError("A quantum function must return either a single measured observable " - "or a nonempty sequence of measured observables.") - - # check that all returned observables have a return_type specified - for x in res: - if x.return_type is None: - raise QuantumFunctionError("Observable '{}' does not have the measurement " - "type specified.".format(x.name)) - - # check that all ev's are returned, in the correct order - if res != tuple(self.ev): - raise QuantumFunctionError("All measured observables must be returned in the " - "order they are measured.") - - self.ev = list(res) #: list[Observable]: returned observables - - # list all operations except for the identity - non_identity_ops = [op for op in self.queue + self.ev if not isinstance(op, qml.ops.Identity)] - - # contains True if op is a CV, False if it is a discrete variable - are_cvs = [isinstance(op, qml.operation.CV) for op in non_identity_ops] - - if not all(are_cvs) and any(are_cvs): - raise QuantumFunctionError("Continuous and discrete operations are not " - "allowed in the same quantum circuit.") - - # TODO: we should enforce plugins using the Device.capabilities dictionary to specify - # whether they are qubit or CV devices, and remove this logic here. - self.type = 'CV' if all(are_cvs) else 'qubit' - - if self.device.operations: - # replace operations in the queue with any decompositions if required - self.queue = decompose_queue(self.queue, self.device) - - self.ops = self.queue + self.ev #: list[Operation]: combined list of circuit operations - - # map each free variable to the operations which depend on it - self.variable_deps = {} - for k, op in enumerate(self.ops): - for j, p in enumerate(_flatten(op.params)): - if isinstance(p, Variable): - if p.name is None: # ignore keyword arguments - self.variable_deps.setdefault(p.idx, []).append(ParameterDependency(op, j)) - - # generate directed acyclic graph - self.circuit = CircuitGraph(self.ops, self.variable_deps) - - #: dict[int->str]: map from free parameter index to the gradient method to be used with that parameter - self.grad_method_for_par = {k: self._best_method(k) for k in self.variable_deps} - - def construct_metric_tensor(self, args, **kwargs): - """Create metric tensor subcircuits for each parameter. - - If the parameter appears in a gate :math:`G`, the subcircuit contains - all gates which precede :math:`G`, and :math:`G` is replaced by the variance - value of its generator. - - Args: - args (tuple): Represent the free parameters passed to the circuit. - Here we are not concerned with their values, but with their structure. - Each free param is replaced with a :class:`~.variable.Variable` instance. - - Keyword Args: - diag_approx (bool): If ``True``, forces the diagonal - approximation. Default is ``False``. - - Raises: - QuantumFunctionError: if a metric tensor cannot be generated because no generator was defined - - .. note:: - - Additional keyword arguments may be passed to the quantum circuit function, however PennyLane - does not support differentiating with respect to keyword arguments. Instead, - keyword arguments are useful for providing data or 'placeholders' to the quantum circuit function. - """ - # pylint: disable=too-many-statements - diag_approx = kwargs.pop("diag_approx", False) - - if not self.ops or not self.cache: - # construct the circuit - self.construct(args, kwargs) - - for queue, curr_ops, param_idx, _ in self.circuit.iterate_layers(): - obs = [] - scale = [] - - Ki_matrices = [] - KiKj_matrices = [] - Ki_ev = [] - KiKj_ev = [] - V = None - - # for each operator, get the generator - # and convert it to a variance - for n, op in enumerate(curr_ops): - gen, s = op.generator - w = op.wires - - if gen is None: - raise QuantumFunctionError("Can't generate metric tensor, operation {}" - "has no defined generator".format(op)) - - # get the observable corresponding - # to the generator of the current operation - if isinstance(gen, np.ndarray): - # generator is a Hermitian matrix - variance = qml.var(qml.Hermitian(gen, w)) - - if not diag_approx: - Ki_matrices.append((n, expand(gen, w, self.num_wires))) - - elif issubclass(gen, qml.operation.Observable): - # generator is an existing PennyLane operation - variance = qml.var(gen(w)) - - if not diag_approx: - if issubclass(gen, qml.ops.PauliX): - mat = np.array([[0, 1], [1, 0]]) - elif issubclass(gen, qml.ops.PauliY): - mat = np.array([[0, -1j], [1j, 0]]) - elif issubclass(gen, qml.ops.PauliZ): - mat = np.array([[1, 0], [0, -1]]) - - Ki_matrices.append((n, expand(mat, w, self.num_wires))) - - else: - raise QuantumFunctionError( - "Can't generate metric tensor, generator {}" - "has no corresponding observable".format(gen) - ) - - obs.append(variance) - scale.append(s) - - if not diag_approx: - # In order to compute the block diagonal portion of the metric tensor, - # we need to compute 'second order' terms. - - for i, j in itertools.product(range(len(Ki_matrices)), repeat=2): - # compute the matrices representing all K_i K_j terms - obs1 = Ki_matrices[i] - obs2 = Ki_matrices[j] - KiKj_matrices.append(((obs1[0], obs2[0]), obs1[1] @ obs2[1])) - - V = np.identity(2**self.num_wires, dtype=np.complex128) - - # generate the unitary operation to rotate to - # the shared eigenbasis of all observables - for _, term in Ki_matrices: - _, S = linalg.eigh(V.conj().T @ term @ V) - V = np.round(V @ S, 15) - - V = V.conj().T - - # calculate the eigenvalues for - # each observable in the shared eigenbasis - for idx, term in Ki_matrices: - eigs = np.diag(V @ term @ V.conj().T).real - Ki_ev.append((idx, eigs)) - - for idx, term in KiKj_matrices: - eigs = np.diag(V @ term @ V.conj().T).real - KiKj_ev.append((idx, eigs)) - - self._metric_tensor_subcircuits[tuple(param_idx)] = { - "queue": queue, - "observable": obs, - "Ki_expectations": Ki_ev, - "KiKj_expectations": KiKj_ev, - "eigenbasis_matrix": V, - "result": None, - "scale": scale, - } - - def _op_successors(self, op, only='G'): - """Successors of the given operation in the quantum circuit. - - Args: - op (Operation): operation in the quantum circuit - only (str): the type of successors to return. - - - ``'G'``: only return non-observables (default) - - ``'E'``: only return observables - - ``None``: return all successors - - Returns: - list[Operation]: successors in a topological order - """ - succ = self.circuit.descendants_in_order((op,)) - if only == 'E': - return [x for x in succ if isinstance(x, qml.operation.Observable)] - if only == 'G': - return [x for x in succ if not isinstance(x, qml.operation.Observable)] - return succ - - def _best_method(self, idx): - """Determine the correct gradient computation method for a free parameter. - - Use the analytic method iff every gate that depends on the parameter supports it. - If not, use the finite difference method. - - Note that If even one gate does not support differentiation, we cannot differentiate - with respect to this parameter at all. - - Args: - idx (int): free parameter index - Returns: - str: gradient method to be used - """ - # TODO: For CV circuits, when the circuit DAG is implemented, determining which gradient - # method to use for should work like this: - # - # 1. To check whether we can use the 'A' or 'A2' method, we need first to check for the - # presence of non-Gaussian ops and order-2 observables. - # - # 2. Starting from the measured observables (all leaf nodes under current limitations on - # observables, see :ref:`measurements`), walk through the DAG against the edges - # (upstream) in arbitrary order. - # - # 3. If the starting leaf is an order-2 EV, mark every Gaussian operation you hit with - # op.grad_method='A2' (instance variable, does not mess up the class variable!). - # - # 4. If you hit a non-Gaussian gate (grad_method != 'A'), from that gate upstream mark every - # gaussian operation with op.grad_method='F'. - # - # 5. Then run the standard discrete-case algorithm for determining the best gradient method - # for every free parameter. - - # pylint: disable=too-many-return-statements - def best_for_op(op): - "Returns the best gradient method for the Operation op." - # for discrete operations, other ops do not affect the choice - - if not isinstance(op, qml.operation.CV): - return op.grad_method - - # for CV ops it is more complicated - if op.grad_method == "A": - # op is Gaussian and has the heisenberg_* methods - - obs_successors = self._op_successors(op, 'E') - if not obs_successors: - # op is not succeeded by any observables, thus analytic method is OK - return 'A' - - # check that all successor ops are also Gaussian - successor_ops = self._op_successors(op, 'G') - if not all(x.supports_heisenberg for x in successor_ops): - non_gaussian_ops = [x for x in successor_ops if not x.supports_heisenberg] - # a non-Gaussian successor is OK if it isn't succeeded by any observables - for x in non_gaussian_ops: - if self._op_successors(x, 'E'): - return 'F' - - # check successor EVs, if any order-2 observables are found return 'A2', else return 'A' - for observable in obs_successors: - if observable.ev_order is None: - # ev_order of None corresponds to a non-Gaussian observable - return 'F' - if observable.ev_order == 2: - if observable.return_type is qml.operation.Variance: - # second order observables don't support - # analytic diff of variances - return 'F' - op.grad_method = 'A2' # bit of a hack - return 'A' - return op.grad_method - - # operations that depend on the free parameter idx - ops = [d.op for d in self.variable_deps[idx]] - methods = list(map(best_for_op, ops)) - - if all(k in ('A', 'A2') for k in methods): - return 'A' - - if None in methods: - return None - - return 'F' - - def __call__(self, *args, **kwargs): - """Wrapper for :meth:`~.QNode.evaluate`.""" - # pylint: disable=no-member - args = autograd.builtins.tuple(args) # prevents autograd boxed arguments from going through to evaluate - return self.evaluate(args, **kwargs) # args as one tuple - - @ae.primitive - def evaluate(self, args, **kwargs): - """Evaluates the quantum function on the specified device. - - Args: - args (tuple): input parameters to the quantum function - - Raises: - QuantumFunctionError: if there is an error while measuring - - Returns: - float, array[float]: output measured value(s) - """ - if not self.ops or not self.cache: - if self.num_variables is not None: - # circuit construction has previously been called - if len(list(_flatten(args))) == self.num_variables: - # only construct the circuit if the number - # of arguments matches the allowed number - # of variables. - # This avoids construction happening - # via self._pd_analytic, where temporary - # variables are appended to the argument list. - - # flatten and unflatten arguments - flat_args = list(_flatten(args)) - shaped_args = unflatten(flat_args, self.model) - - # construct the circuit - self.construct(shaped_args, kwargs) - else: - # circuit has not yet been constructed - # construct the circuit - self.construct(args, kwargs) - - # temporarily store keyword arguments - keyword_values = {} - keyword_values.update({k: np.array(list(_flatten(v))) for k, v in self.keyword_defaults.items()}) - keyword_values.update({k: np.array(list(_flatten(v))) for k, v in kwargs.items()}) - - # Try and insert kwargs-as-positional back into the kwargs dictionary. - # NOTE: this works, but the creation of new, temporary arguments - # by pd_analytic breaks this. - # positional = [] - # kwargs_as_position = {} - # for idx, v in enumerate(args): - # if idx not in self.keyword_positions: - # positional.append(v) - # else: - # kwargs_as_position[self.keyword_positions[idx]] = np.array(list(_flatten(v))) - # keyword_values.update(kwargs_as_position) - - # temporarily store the free parameter values in the Variable class - Variable.free_param_values = np.array(list(_flatten(args))) - Variable.kwarg_values = keyword_values - - self.device.reset() - - # check that no wires are measured more than once - m_wires = list(_flatten(list(w for ex in self.circuit.observables for w in ex.wires))) - if len(m_wires) != len(set(m_wires)): - raise QuantumFunctionError('Each wire in the quantum circuit can only be measured once.') - - def check_op(op): - """Make sure only existing wires are referenced.""" - for w in _flatten(op.wires): - if w < 0 or w >= self.num_wires: - raise QuantumFunctionError("Operation {} applied to invalid wire {} " - "on device with {} wires.".format(op.name, w, self.num_wires)) - - # check every gate/preparation and ev measurement - for op in self.ops: - check_op(op) - - ret = self.device.execute(self.circuit.operations, self.circuit.observables, self.variable_deps) - return self.output_conversion(ret) - - def metric_tensor(self, *args, **kwargs): - """Evaluate the value of the metric tensor. - - Args: - args : qfunc positional arguments - kwargs : qfunc keyword arguments - - Keyword Args: - diag_approx (bool): If ``True``, forces the diagonal - approximation. Default is ``False``. - - Returns: - array[float]: measured values - """ - diag_approx = kwargs.pop("diag_approx", False) - - if not self.ops or not self.cache: - # construct the circuit - self.construct(args, kwargs) - - # temporarily store keyword arguments - keyword_values = {} - keyword_values.update({k: np.array(list(_flatten(v))) for k, v in self.keyword_defaults.items()}) - keyword_values.update({k: np.array(list(_flatten(v))) for k, v in kwargs.items()}) - - # temporarily store the free parameter values in the Variable class - Variable.free_param_values = np.array(list(_flatten(args))) - Variable.kwarg_values = keyword_values - - if not self._metric_tensor_subcircuits: - self.construct_metric_tensor(args, diag_approx=diag_approx, **kwargs) - - tensor = np.zeros([self.num_variables, self.num_variables]) - - # execute any constructed metric tensor subcircuits - for params, circuit in self._metric_tensor_subcircuits.items(): - self.device.reset() - - s = np.array(circuit['scale']) - V = circuit['eigenbasis_matrix'] - - if not diag_approx: - # block diagonal approximation - - unitary_op = qml.ops.QubitUnitary(V, wires=list(range(self.num_wires))) - self.device.execute(circuit['queue'] + [unitary_op], circuit['observable']) - probs = list(self.device.probability().values()) - - first_order_ev = np.zeros([len(params)]) - second_order_ev = np.zeros([len(params), len(params)]) - - for idx, ev in circuit['Ki_expectations']: - first_order_ev[idx] = ev @ probs - - for idx, ev in circuit['KiKj_expectations']: - # idx is a 2-tuple (i, j), representing - # generators K_i, K_j - second_order_ev[idx] = ev @ probs - - # since K_i and K_j are assumed to commute, - # = , - # and thus the matrix of second-order expectations - # is symmetric - second_order_ev[idx[1], idx[0]] = second_order_ev[idx] - - g = np.zeros([len(params), len(params)]) - - for i, j in itertools.product(range(len(params)), repeat=2): - g[i, j] = s[i] * s[j] * (second_order_ev[i, j] - first_order_ev[i] * first_order_ev[j]) - - row = np.array(params).reshape(-1, 1) - col = np.array(params).reshape(1, -1) - circuit['result'] = np.diag(g) - tensor[row, col] = g - - else: - # diagonal approximation - circuit['result'] = s**2 * self.device.execute(circuit['queue'], circuit['observable']) - tensor[np.array(params), np.array(params)] = circuit['result'] - - return tensor - - def evaluate_obs(self, obs, args, **kwargs): - """Evaluate the value of the given observables. - - Assumes :meth:`construct` has already been called. - - Args: - obs (Iterable[Observable]): observables to measure - args (array[float]): circuit input parameters - - Returns: - array[float]: measured values - """ - # temporarily store keyword arguments - keyword_values = {} - keyword_values.update({k: np.array(list(_flatten(v))) for k, v in self.keyword_defaults.items()}) - keyword_values.update({k: np.array(list(_flatten(v))) for k, v in kwargs.items()}) - - # temporarily store the free parameter values in the Variable class - Variable.free_param_values = args - Variable.kwarg_values = keyword_values - - self.device.reset() - ret = self.device.execute(self.circuit.operations, obs, self.circuit.variable_deps) - return ret - - def jacobian(self, params, which=None, *, method='B', h=1e-7, order=1, **kwargs): - """Compute the Jacobian of the QNode. - - Returns the Jacobian of the parametrized quantum circuit encapsulated in the QNode. - - The Jacobian can be computed using several methods: - - * Finite differences (``'F'``). 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 = len(which)``. - - * Analytic method (``'A'``). Works for all one-parameter gates where the generator - only has two unique eigenvalues; this includes one-parameter qubit gates, as well as - Gaussian circuits of order one or two. Additionally, can be used in CV systems for Gaussian - circuits containing first- and second-order observables. - - The circuit is evaluated twice for each incidence of each parameter in the circuit. - - * Best known method for each parameter (``'B'``): uses the analytic method if - possible, otherwise finite difference. - - .. 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 requires exact expectation values, i.e., `analytic=True` in simulation plugins. - - Args: - params (nested Sequence[Number], Number): point in parameter space at which - to evaluate the gradient - which (Sequence[int], None): return the Jacobian with respect to these parameters. - None (the default) means with respect to all parameters. Note that keyword - arguments to the QNode are *always* treated as fixed values and not included - in the Jacobian calculation. - method (str): Jacobian computation method, see above. - - Keyword Args: - h (float): finite difference method step size - order (int): finite difference method order, 1 or 2 - shots (int): How many times the circuit should be evaluated (or sampled) to estimate - the expectation values. - - Raises: - QuantumFunctionError: if sampling is specified - ValueError: if `params` is incorrect, differentiation is not possible, or if `method` - is unknown - - Returns: - array[float]: Jacobian matrix, with shape ``(n_out, len(which))``, where ``len(which)`` is the - number of free parameters, and ``n_out`` is the number of expectation values returned - by the QNode. - """ - # pylint: disable=too-many-statements - - # in QNode.construct we need to be able to (essentially) apply the unpacking operator to params - if isinstance(params, numbers.Number): - params = (params,) - - circuit_kwargs = pop_jacobian_kwargs(kwargs) - - if not self.ops or not self.cache: - # construct the circuit - self.construct(params, circuit_kwargs) - - sample_ops = [ - e for e in self.circuit.observables if e.return_type is qml.operation.Sample] - - if sample_ops: - names = [str(e) for e in sample_ops] - raise QuantumFunctionError("Circuits that include sampling can not be differentiated. " - "The following observable include sampling: {}".format('; '.join(names))) - - flat_params = np.array(list(_flatten(params))) - - if which is None: - which = range(len(flat_params)) - else: - if min(which) < 0 or max(which) >= self.num_variables: - raise ValueError("Tried to compute the gradient wrt. free parameters {} " - "(this node has {} free parameters).".format(which, self.num_variables)) - if len(which) != len(set(which)): # set removes duplicates - raise ValueError("Parameter indices must be unique.") - - # check if the method can be used on the requested parameters - mmap = _inv_dict(self.grad_method_for_par) - def check_method(m): - """Intersection of ``which`` with free params whose best grad method is m.""" - return mmap.get(m, set()).intersection(which) - - bad = check_method(None) - if bad: - raise ValueError('Cannot differentiate wrt parameter(s) {}.'.format(bad)) - - if method in ('A', 'F'): - if method == 'A': - bad = check_method('F') - if bad: - raise ValueError("The analytic gradient method cannot be " - "used with the parameter(s) {}.".format(bad)) - method = {k: method for k in which} - elif method == 'B': - method = self.grad_method_for_par - else: - raise ValueError('Unknown gradient method.') - - if 'F' in method.values(): - if order == 1: - # the value of the circuit at params, computed only once here - y0 = np.asarray(self.evaluate(params, **circuit_kwargs)) - else: - y0 = None - - variances = any(e.return_type is qml.operation.Variance for e in self.circuit.observables) - - # compute the partial derivative w.r.t. each parameter using the proper method - grad = np.zeros((self.output_dim, len(which)), dtype=float) - - for i, k in enumerate(which): - if k not in self.variable_deps: - # unused parameter - continue - - par_method = method[k] - if par_method == 'A': - if variances: - grad[:, i] = self._pd_analytic_var(flat_params, k, **kwargs) - else: - grad[:, i] = self._pd_analytic(flat_params, k, **kwargs) - elif par_method == 'F': - grad[:, i] = self._pd_finite_diff(flat_params, k, h, order, y0, **kwargs) - else: - raise ValueError('Unknown gradient method.') - - return grad - - def _pd_finite_diff(self, params, idx, h=1e-7, order=1, y0=None, **kwargs): - """Partial derivative of the node using the finite difference method. - - Args: - params (array[float]): point in parameter space at which to evaluate - the partial derivative - idx (int): return the partial derivative with respect to this parameter - h (float): step size - order (int): finite difference method order, 1 or 2 - y0 (float): Value of the circuit at params. Should only be computed once. - - Raises: - ValueError: if the order specified is not equal to 1 or 2 - - Returns: - float: partial derivative of the node. - """ - circuit_kwargs = pop_jacobian_kwargs(kwargs) - - shift_params = params.copy() - if order == 1: - # shift one parameter by h - shift_params[idx] += h - y = np.asarray(self.evaluate(shift_params, **circuit_kwargs)) - return (y-y0) / h - elif order == 2: - # symmetric difference - # shift one parameter by +-h/2 - shift_params[idx] += 0.5*h - y2 = np.asarray(self.evaluate(shift_params, **circuit_kwargs)) - shift_params[idx] = params[idx] -0.5*h - y1 = np.asarray(self.evaluate(shift_params, **circuit_kwargs)) - return (y2-y1) / h - else: - raise ValueError('Order must be 1 or 2.') - - @staticmethod - def _transform_observable(observable, w, Z): - """Transform the observable - - Args: - observable (Observable): the observable to perform the transformation on - w (int): number of wires - Z (array[float]): the Heisenberg picture representation of the linear transformation - - Raises: - QuantumFunctionError: if there is a mismatch between polynomial order of observable - and heisenberg representation - - Returns: - float: expectation value - """ - q = observable.heisenberg_obs(w) - - if q.ndim != observable.ev_order: - raise QuantumFunctionError( - "Mismatch between polynomial order of observable and heisenberg representation") - - qp = q @ Z - if q.ndim == 2: - # 2nd order observable - qp = qp +qp.T - return qml.expval(qml.PolyXP(qp, wires=range(w))) - - - def _pd_analytic(self, params, idx, force_order2=False, **kwargs): - """Partial derivative of the node using the analytic method. - - 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. - - Args: - params (array[float]): point in free parameter space at which - to evaluate the partial derivative - idx (int): return the partial derivative with respect to this - free parameter - - Returns: - float: partial derivative of the node. - """ - # remove jacobian specific keyword arguments - circuit_kwargs = pop_jacobian_kwargs(kwargs) - - n = self.num_variables - w = self.num_wires - pd = 0.0 - # find the Commands in which the free parameter appears, use the product rule - for op, p_idx in self.variable_deps[idx]: - - # we temporarily edit the Operation such that parameter p_idx is replaced by a new one, - # which we can modify without affecting other Operations depending on the original. - orig = op.params[p_idx] - assert orig.idx == idx - - # reference to a new, temporary parameter with index n, otherwise identical with orig - temp_var = copy.copy(orig) - temp_var.idx = n - op.params[p_idx] = temp_var - - # get the gradient recipe for this parameter - recipe = op.grad_recipe[p_idx] - multiplier = 0.5 if recipe is None else recipe[0] - multiplier *= orig.mult - - # shift the temp parameter value by +- this amount - shift = np.pi / 2 if recipe is None else recipe[1] - shift /= orig.mult - - # shifted parameter values - shift_p1 = np.r_[params, params[idx] +shift] - shift_p2 = np.r_[params, params[idx] -shift] - - if not force_order2 and op.grad_method != 'A2': - # basic analytic method, for discrete gates and gaussian CV gates succeeded by order-1 observables - # evaluate the circuit in two points with shifted parameter values - y2 = np.asarray(self.evaluate(shift_p1, **circuit_kwargs)) - y1 = np.asarray(self.evaluate(shift_p2, **circuit_kwargs)) - pd += (y2-y1) * multiplier - else: - # order-2 method, for gaussian CV gates succeeded by order-2 observables - # evaluate transformed observables at the original parameter point - # first build the Z transformation matrix - Variable.free_param_values = shift_p1 - Z2 = op.heisenberg_tr(w) - Variable.free_param_values = shift_p2 - Z1 = op.heisenberg_tr(w) - Z = (Z2-Z1) * multiplier # derivative of the operation - - unshifted_params = np.r_[params, params[idx]] - Variable.free_param_values = unshifted_params - Z0 = op.heisenberg_tr(w, inverse=True) - Z = Z @ Z0 - - # conjugate Z with all the following operations - B = np.eye(1 +2*w) - B_inv = B.copy() - for BB in self._op_successors(op, 'G'): - if not BB.supports_heisenberg: - # if the successor gate is non-Gaussian in analytic differentiation - # mode, then there must be no observable following it. - continue - B = BB.heisenberg_tr(w) @ B - B_inv = B_inv @ BB.heisenberg_tr(w, inverse=True) - Z = B @ Z @ B_inv # conjugation - - # transform the observables - obs = [ - self._transform_observable(ob, w, Z) - for ob in self.circuit.observables] - - # measure transformed observables - pd += self.evaluate_obs(obs, unshifted_params, **circuit_kwargs) - - # restore the original parameter - op.params[p_idx] = orig - - return pd - - def _pd_analytic_var(self, param_values, param_idx, **kwargs): - """Partial derivative of variances of observables using the analytic method. - - Args: - param_values (array[float]): point in free parameter space at which - to evaluate the partial derivative - param_idx (int): return the partial derivative with respect to this - free parameter - - Returns: - float: partial derivative of the node. - """ - # boolean mask: elements are True where the return type is a variance, False for expectations - where_var = [e.return_type is qml.operation.Variance for e in self.circuit.observables] - applicable_nodes = [e for e in self.circuit.observables if e.return_type == qml.operation.Variance] - - for e in applicable_nodes: - # temporarily convert return type to expectation - e.return_type = qml.operation.Expectation - - # analytic derivative of - # For involutory observables (A^2 = I), - # then d/dp = 0 - pdA2 = 0 - - if self.type == 'qubit': - if e.__class__.__name__ == 'Hermitian': - # since arbitrary Hermitian observables - # are not guaranteed to be involutory, need to take them into - # account separately to calculate d/dp - - A = e.params[0] # Hermitian matrix - w = e.wires - - if not np.allclose(A @ A, np.identity(A.shape[0])): - # make a copy of the original variance - new = qml.expval(qml.ops.Hermitian(A @ A, w)) - - # replace the Hermitian variance with expectation - self.circuit.update_node(e, new) - - # calculate the analytic derivative of - pdA2 = np.asarray(self._pd_analytic(param_values, param_idx, **kwargs)) - - # restore the original Hermitian variance - self.circuit.update_node(new, e) - - elif self.type == 'CV': - # need to calculate d/dp - w = e.wires - - # get the heisenberg representation - # This will be a real 1D vector representing the - # first order observable in the basis [I, x, p] - A = e._heisenberg_rep(e.parameters) # pylint: disable=protected-access - - # make this a row vector by adding an extra dimension - A = np.expand_dims(A, axis=0) - - # take the outer product of the heisenberg representation - # with itself, to get a square symmetric matrix representing - # the square of the observable - A = np.kron(A, A.T) - - new = qml.expval(qml.ops.PolyXP(A, w)) - # replace the first order observable var(A) with - self.circuit.update_node(e, new) - - # calculate the analytic derivative of - pdA2 = np.asarray(self._pd_analytic(param_values, param_idx, force_order2=True, **kwargs)) - - # restore the original observable - self.circuit.update_node(new, e) - - # save original cache setting - cache = self.cache - # Make sure caching is on. If it is not on, - # the circuit will be reconstructed when self.evaluate is - # called, overwriting the temporary change we made to - # self.ev, where we set the return_type of every observable - # to :attr:`ObservableReturnTypes.Expectation`. - self.cache = True - - # evaluate circuit value at original parameters - evA = np.asarray(self.evaluate(param_values, **kwargs)) - # evaluate circuit gradient assuming all outputs are expectations - pdA = self._pd_analytic(param_values, param_idx, **kwargs) - - # restore the return type - for e in applicable_nodes: - e.return_type = qml.operation.Variance - - # restore original caching setting - self.cache = cache - - # return the variance shift rule where where_var==True, - # otherwise return the expectation parameter shift rule - return np.where(where_var, pdA2-2*evA*pdA, pdA) - - def to_torch(self): - """Convert the standard PennyLane QNode into a :func:`~.TorchQNode`. - - Raises: - QuantumFunctionError: if PyTorch is not installed - """ - # Placing slow imports here, in case the user does not use the Torch interface - try: # pragma: no cover - from .interfaces.torch import TorchQNode - except ImportError: # pragma: no cover - raise QuantumFunctionError("PyTorch not found. Please install " - "PyTorch to enable the TorchQNode interface.") from None - - return TorchQNode(self) - - def to_tf(self): - """Convert the standard PennyLane QNode into a :func:`~.TFQNode`. - - Raises: - QuantumFunctionError: if TensorFlow >= 1.12 is not installed - """ - # Placing slow imports here, in case the user does not use the TF interface - try: # pragma: no cover - from .interfaces.tf import TFQNode - except ImportError: # pragma: no cover - raise QuantumFunctionError("TensorFlow not found. Please install " - "the latest version of TensorFlow to enable the TFQNode interface.") from None - - return TFQNode(self) - - -#def QNode_vjp(ans, self, params, *args, **kwargs): -def QNode_vjp(ans, self, args, **kwargs): - """Returns the vector Jacobian product operator for a QNode, as a function - of the QNode evaluation for specific argnums at the specified parameter values. - - This function is required for integration with Autograd. - """ - # pylint: disable=unused-argument - def gradient_product(g): - """Vector Jacobian product operator. - - Args: - g (array): scalar or vector multiplying the Jacobian - from the left (output side). - - Returns: - nested Sequence[float]: vector-Jacobian product, arranged - into the nested structure of the QNode input arguments. - """ - # Jacobian matrix of the circuit - jac = self.jacobian(args, **kwargs) - if not g.shape: - temp = g * jac # numpy treats 0d arrays as scalars, hence @ cannot be used - else: - temp = g @ jac - - # restore the nested structure of the input args - temp = unflatten(temp.flat, args) - return temp - - return gradient_product - - -# define the vector-Jacobian product function for QNode.__call__() -ae.defvjp(QNode.evaluate, QNode_vjp, argnums=[1]) diff --git a/pennylane/beta/qnodes/__init__.py b/pennylane/qnodes/__init__.py similarity index 100% rename from pennylane/beta/qnodes/__init__.py rename to pennylane/qnodes/__init__.py diff --git a/pennylane/beta/qnodes/base.py b/pennylane/qnodes/base.py similarity index 90% rename from pennylane/beta/qnodes/base.py rename to pennylane/qnodes/base.py index 5b3f6d85a34..d8924de9161 100644 --- a/pennylane/beta/qnodes/base.py +++ b/pennylane/qnodes/base.py @@ -26,7 +26,6 @@ from pennylane.utils import _flatten, unflatten from pennylane.circuit_graph import CircuitGraph, _is_observable from pennylane.variable import Variable -from pennylane.qnode import QNode as QNode_old, QuantumFunctionError, decompose_queue _MARKER = inspect.Parameter.empty # singleton marker, could be any singleton class @@ -51,6 +50,10 @@ """ +class QuantumFunctionError(Exception): + """Exception raised when an illegal operation is defined in a quantum function.""" + + def _get_signature(func): """Introspect the parameter signature of a function. @@ -83,6 +86,49 @@ def _get_signature(func): func.n_pos = n_pos +def _decompose_queue(ops, device): + """Recursively loop through a queue and decompose + operations that are not supported by a device. + + Args: + ops (List[~.Operation]): operation queue + device (~.Device): a PennyLane device + """ + new_ops = [] + + for op in ops: + if device.supports_operation(op.name): + new_ops.append(op) + else: + decomposed_ops = op.decomposition(*op.params, wires=op.wires) + decomposition = _decompose_queue(decomposed_ops, device) + new_ops.extend(decomposition) + + return new_ops + + +def decompose_queue(ops, device): + """Decompose operations in a queue that are not supported by a device. + + This is a wrapper function for :func:`~._decompose_queue`, + which raises an error if an operation or its decomposition + is not supported by the device. + + Args: + ops (List[~.Operation]): operation queue + device (~.Device): a PennyLane device + """ + new_ops = [] + + for op in ops: + try: + new_ops.extend(_decompose_queue([op], device)) + except NotImplementedError: + raise qml.DeviceError("Gate {} not supported on device {}".format(op.name, device.short_name)) + + return new_ops + + class BaseQNode: """Base class for quantum nodes in the hybrid computational graph. @@ -282,7 +328,6 @@ def _construct(self, args, kwargs): inside of this method, the quantum function returns incorrect values or if both continuous and discrete operations are specified in the same quantum circuit """ - # pylint: disable=protected-access # remove when QNode_old is gone # pylint: disable=attribute-defined-outside-init, too-many-branches # flatten the args, replace each argument with a Variable instance carrying a unique index @@ -296,11 +341,11 @@ def _construct(self, args, kwargs): self.obs_queue = [] #: list[Observable]: applied observables # set up the context for Operator entry - if QNode_old._current_context is None: - QNode_old._current_context = self + if qml._current_context is None: + qml._current_context = self else: raise QuantumFunctionError( - "QNode._current_context must not be modified outside this method." + "qml._current_context must not be modified outside this method." ) try: # generate the program queue by executing the quantum circuit function @@ -317,7 +362,7 @@ def _construct(self, args, kwargs): res = self.func(*arg_vars, **kwarg_vars) finally: - QNode_old._current_context = None + qml._current_context = None # check the validity of the circuit self._check_circuit(res) @@ -467,7 +512,15 @@ def _default_args(self, kwargs): if self.func.var_keyword: continue # unknown parameter, but **kwargs will take it TODO should it? raise QuantumFunctionError("Unknown quantum function parameter '{}'.".format(name)) - if s.par.kind in forbidden_kinds or s.par.default == inspect.Parameter.empty: + + default_parameter = s.par.default + + # The following is a check of the default parameter which works for numpy + # arrays as well (if it is a numpy array, each element is checked separately). + correct_default_parameter = any(d == inspect.Parameter.empty for d in default_parameter)\ + if isinstance(default_parameter, np.ndarray)\ + else default_parameter == inspect.Parameter.empty + if s.par.kind in forbidden_kinds or correct_default_parameter: raise QuantumFunctionError( "Quantum function parameter '{}' cannot be given using the keyword syntax.".format( name @@ -477,7 +530,11 @@ def _default_args(self, kwargs): # apply defaults for name, s in self.func.sig.items(): default = s.par.default - if default != inspect.Parameter.empty: + correct_default = all(d != inspect.Parameter.empty for d in default)\ + if isinstance(default, np.ndarray)\ + else default != inspect.Parameter.empty + + if correct_default: # meant to be given using keyword syntax kwargs.setdefault(name, default) diff --git a/pennylane/beta/qnodes/cv.py b/pennylane/qnodes/cv.py similarity index 100% rename from pennylane/beta/qnodes/cv.py rename to pennylane/qnodes/cv.py diff --git a/pennylane/beta/qnodes/decorator.py b/pennylane/qnodes/decorator.py similarity index 100% rename from pennylane/beta/qnodes/decorator.py rename to pennylane/qnodes/decorator.py diff --git a/pennylane/beta/qnodes/device_jacobian.py b/pennylane/qnodes/device_jacobian.py similarity index 100% rename from pennylane/beta/qnodes/device_jacobian.py rename to pennylane/qnodes/device_jacobian.py diff --git a/pennylane/beta/qnodes/jacobian.py b/pennylane/qnodes/jacobian.py similarity index 98% rename from pennylane/beta/qnodes/jacobian.py rename to pennylane/qnodes/jacobian.py index 6fc6954cdc1..d6a7934669f 100644 --- a/pennylane/beta/qnodes/jacobian.py +++ b/pennylane/qnodes/jacobian.py @@ -353,13 +353,13 @@ def to_torch(self): # Placing slow imports here, in case the user does not use the Torch interface # pylint: disable=import-outside-toplevel try: # pragma: no cover - from pennylane.interfaces.torch import TorchQNode + from pennylane.interfaces.torch import to_torch as _to_torch except ImportError: # pragma: no cover raise QuantumFunctionError( "PyTorch not found. Please install " "PyTorch to enable the 'torch' interface." ) from None - return TorchQNode(self) + return _to_torch(self) def to_tf(self): """Attach the TensorFlow interface to the Jacobian QNode. @@ -370,14 +370,14 @@ def to_tf(self): # Placing slow imports here, in case the user does not use the TF interface # pylint: disable=import-outside-toplevel try: # pragma: no cover - from pennylane.interfaces.tf import TFQNode + from pennylane.interfaces.tf import to_tf as _to_tf except ImportError: # pragma: no cover raise QuantumFunctionError( "TensorFlow not found. Please install " "the latest version of TensorFlow to enable the 'tf' interface." ) from None - return TFQNode(self) + return _to_tf(self) def to_autograd(self): """Attach the TensorFlow interface to the Jacobian QNode. @@ -388,7 +388,7 @@ def to_autograd(self): # Placing slow imports here, in case the user does not use the TF interface # pylint: disable=import-outside-toplevel try: # pragma: no cover - from pennylane.beta.interfaces.autograd import to_autograd as _to_autograd + from pennylane.interfaces.autograd import to_autograd as _to_autograd except ImportError: # pragma: no cover raise QuantumFunctionError( "Autograd not found. Please install " diff --git a/pennylane/beta/qnodes/qubit.py b/pennylane/qnodes/qubit.py similarity index 100% rename from pennylane/beta/qnodes/qubit.py rename to pennylane/qnodes/qubit.py diff --git a/pennylane/utils.py b/pennylane/utils.py index f3afabb51d3..46d266447c2 100644 --- a/pennylane/utils.py +++ b/pennylane/utils.py @@ -272,13 +272,13 @@ def __init__(self): self.old_context = None def __enter__(self): - self.rec = Recorder(qml.QNode._current_context) + self.rec = Recorder(qml._current_context) # store the old context to be returned later - self.old_context = qml.QNode._current_context + self.old_context = qml._current_context # set the recorder as the QNode context - qml.QNode._current_context = self.rec + qml._current_context = self.rec self.queue = None self.operations = None @@ -304,7 +304,7 @@ def __exit__(self, *args, **kwargs): ) ) - qml.QNode._current_context = self.old_context + qml._current_context = self.old_context def __str__(self): output = "" diff --git a/setup.py b/setup.py index ab0bee6451d..9e224d1e336 100644 --- a/setup.py +++ b/setup.py @@ -42,9 +42,7 @@ 'pennylane.optimize', 'pennylane.interfaces', 'pennylane.beta', - 'pennylane.beta.interfaces', 'pennylane.beta.plugins', - 'pennylane.beta.qnodes', 'pennylane.beta.vqe', ], 'entry_points': { diff --git a/tests/beta/test_tensornet.py b/tests/beta/test_tensornet.py index 25843d9cd96..c69921a0cfb 100644 --- a/tests/beta/test_tensornet.py +++ b/tests/beta/test_tensornet.py @@ -20,7 +20,7 @@ import pytest import pennylane as qml -from pennylane import numpy as np +from pennylane import numpy as np, QuantumFunctionError tensornetwork = pytest.importorskip("tensornetwork", minversion="0.1") @@ -135,8 +135,7 @@ def circuit(*x): return qml.expval(qml.X(0)) with pytest.raises( - qml.DeviceError, - match="Gate {} not supported on device expt.tensornet".format(gate), + QuantumFunctionError, match="Device expt.tensornet is a qubit device; CV operations are not allowed." ): x = np.random.random([op.num_params]) circuit(*x) @@ -159,8 +158,7 @@ def circuit(*x): return qml.expval(op(*x, wires=wires)) with pytest.raises( - qml.DeviceError, - match="Observable {} not supported on device expt.tensornet".format(observable), + QuantumFunctionError, match="Device expt.tensornet is a qubit device; CV operations are not allowed." ): x = np.random.random([op.num_params]) circuit(*x) diff --git a/tests/beta/test_tensornet_tf.py b/tests/beta/test_tensornet_tf.py index ad370d4b7ac..17646a62c3c 100644 --- a/tests/beta/test_tensornet_tf.py +++ b/tests/beta/test_tensornet_tf.py @@ -24,9 +24,9 @@ import pennylane as qml from pennylane.beta.plugins.expt_tensornet_tf import TensorNetworkTF -from pennylane.beta.qnodes import qnode, QNode -from pennylane.beta.qnodes.decorator import ALLOWED_INTERFACES, ALLOWED_DIFF_METHODS from pennylane.plugins.default_qubit import I, X, Y, Z, H, CNOT, SWAP, CNOT, Toffoli, CSWAP +from pennylane.qnodes import qnode, QNode +from pennylane.qnodes.decorator import ALLOWED_INTERFACES, ALLOWED_DIFF_METHODS np.random.seed(42) diff --git a/tests/beta/test_vqe.py b/tests/beta/test_vqe.py index 986e6af5cdf..1cda8f98270 100644 --- a/tests/beta/test_vqe.py +++ b/tests/beta/test_vqe.py @@ -17,6 +17,7 @@ import pytest import pennylane as qml import numpy as np +import pennylane.beta.vqe try: @@ -162,7 +163,7 @@ def amp_embed_and_strong_ent_layer(*params, wires=None): def mock_device(monkeypatch): with monkeypatch.context() as m: m.setattr(qml.Device, "__abstractmethods__", frozenset()) - m.setattr(qml.Device, "_capabilities", {"tensor_observables": True}) + m.setattr(qml.Device, "_capabilities", {"tensor_observables": True, "model": "qubit"}) m.setattr(qml.Device, "operations", ["RX", "Rot", "CNOT", "Hadamard", "QubitStateVector"]) m.setattr( qml.Device, "observables", ["PauliX", "PauliY", "PauliZ", "Hadamard", "Hermitian"] @@ -293,21 +294,25 @@ def test_cost_invalid_ansatz(self, ansatz, mock_device): cost = qml.beta.vqe.cost([], 4, hamiltonian, mock_device) -class TestNumPyInterface: - """Tests for the NumPy interface""" +class TestAutogradInterface: + """Tests for the Autograd interface (and the NumPy interface for backward compatibility)""" @pytest.mark.parametrize("ansatz, params", CIRCUITS) @pytest.mark.parametrize("observables", OBSERVABLES) - def test_QNodes_have_right_interface(self, ansatz, observables, params, mock_device): - """Test that QNodes have the NumPy interface""" + @pytest.mark.parametrize("interface", ["autograd", "numpy"]) + def test_QNodes_have_right_interface(self, ansatz, observables, params, mock_device, interface): + """Test that QNodes have the Autograd interface""" mock_device.num_wires = 3 - circuits = qml.beta.vqe.circuits(ansatz, observables, device=mock_device, interface="numpy") - assert all(c.interface == "numpy" for c in circuits) + circuits = qml.beta.vqe.circuits(ansatz, observables, device=mock_device, interface=interface) + + assert all(c.interface == "autograd" for c in circuits) + assert all(c.__class__.__name__ == "AutogradQNode" for c in circuits) res = [c(*params) for c in circuits] assert all(isinstance(val, float) for val in res) - def test_gradient(self, tol): + @pytest.mark.parametrize("interface", ["autograd", "numpy"]) + def test_gradient(self, tol, interface): """Test differentiation works""" dev = qml.device("default.qubit", wires=1) @@ -322,9 +327,9 @@ def ansatz(*params, **kwargs): a, b = 0.54, 0.123 params = np.array([a, b]) - cost = qml.beta.vqe.cost(params, ansatz, H, dev, interface="numpy") + cost = qml.beta.vqe.cost(params, ansatz, H, dev, interface=interface) - cost2 = lambda params: qml.beta.vqe.cost(params, ansatz, H, dev, interface="numpy") + cost2 = lambda params: qml.beta.vqe.cost(params, ansatz, H, dev, interface=interface) dcost = qml.grad(cost2, argnum=[0]) res = dcost(params) diff --git a/tests/beta/test_interfaces_autograd.py b/tests/interfaces/test_interfaces_autograd.py similarity index 98% rename from tests/beta/test_interfaces_autograd.py rename to tests/interfaces/test_interfaces_autograd.py index 7dc5d2c2145..caea4054547 100644 --- a/tests/beta/test_interfaces_autograd.py +++ b/tests/interfaces/test_interfaces_autograd.py @@ -21,11 +21,11 @@ import numpy as np import pennylane as qml -from pennylane.beta.qnodes.base import QuantumFunctionError -from pennylane.beta.qnodes.qubit import QubitQNode -from pennylane.beta.qnodes.cv import CVQNode +from pennylane.qnodes.base import QuantumFunctionError +from pennylane.qnodes.qubit import QubitQNode +from pennylane.qnodes.cv import CVQNode -from pennylane.beta.interfaces.autograd import to_autograd +from pennylane.interfaces.autograd import to_autograd alpha = 0.5 # displacement in tests diff --git a/tests/test_tf.py b/tests/interfaces/test_tf.py similarity index 98% rename from tests/test_tf.py rename to tests/interfaces/test_tf.py index 9a207722553..ec529190da2 100644 --- a/tests/test_tf.py +++ b/tests/interfaces/test_tf.py @@ -34,7 +34,8 @@ import pennylane as qml -from pennylane.qnode import _flatten, unflatten, QNode, QuantumFunctionError +from pennylane.utils import _flatten, unflatten +from pennylane.qnodes import QNode, QuantumFunctionError from pennylane.plugins.default_qubit import CNOT, Rotx, Roty, Rotz, I, Y, Z from pennylane._device import DeviceError @@ -138,7 +139,7 @@ def qf(x): qml.Displacement(0.5, 0, wires=[0]) return qml.expval(qml.X(0)) - with pytest.raises(DeviceError, match='Gate [a-zA-Z]+ not supported on device'): + with pytest.raises(QuantumFunctionError, match='Device default.qubit is a qubit device; CV operations are not allowed.'): qf(Variable(0.5)) def test_qnode_fails_for_cv_observables_on_qubit_device(self, qubit_device_1_wire): @@ -148,7 +149,7 @@ def test_qnode_fails_for_cv_observables_on_qubit_device(self, qubit_device_1_wir def qf(x): return qml.expval(qml.X(0)) - with pytest.raises(DeviceError, match='Observable [a-zA-Z]+ not supported on device'): + with pytest.raises(QuantumFunctionError, match='Device default.qubit is a qubit device; CV operations are not allowed.'): qf(Variable(0.5)) diff --git a/tests/test_torch.py b/tests/interfaces/test_torch.py similarity index 98% rename from tests/test_torch.py rename to tests/interfaces/test_torch.py index dd29337974c..a02bd276c0a 100644 --- a/tests/test_torch.py +++ b/tests/interfaces/test_torch.py @@ -27,7 +27,8 @@ import pennylane as qml -from pennylane.qnode import _flatten, unflatten, QNode, QuantumFunctionError +from pennylane.utils import _flatten, unflatten +from pennylane.qnodes import QNode, QuantumFunctionError from pennylane.plugins.default_qubit import CNOT, Rotx, Roty, Rotz, I, Y, Z from pennylane._device import DeviceError @@ -131,7 +132,7 @@ def qf(x): qml.Displacement(0.5, 0, wires=[0]) return qml.expval(qml.X(0)) - with pytest.raises(DeviceError, match='Gate [a-zA-Z]+ not supported on device'): + with pytest.raises(QuantumFunctionError, match='Device default.qubit is a qubit device; CV operations are not allowed.'): qf(torch.tensor(0.5)) def test_qnode_fails_for_cv_observables_on_qubit_device(self, qubit_device_1_wire): @@ -141,7 +142,7 @@ def test_qnode_fails_for_cv_observables_on_qubit_device(self, qubit_device_1_wir def qf(x): return qml.expval(qml.X(0)) - with pytest.raises(DeviceError, match='Observable [a-zA-Z]+ not supported on device'): + with pytest.raises(QuantumFunctionError, match='Device default.qubit is a qubit device; CV operations are not allowed.'): qf(torch.tensor(0.5)) diff --git a/tests/beta/test_qnode_base.py b/tests/qnodes/test_qnode_base.py similarity index 99% rename from tests/beta/test_qnode_base.py rename to tests/qnodes/test_qnode_base.py index af9bb0130e5..d8cac5f442b 100644 --- a/tests/beta/test_qnode_base.py +++ b/tests/qnodes/test_qnode_base.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane._device import Device -from pennylane.beta.qnodes.base import BaseQNode, QuantumFunctionError, QNode_old, decompose_queue +from pennylane.qnodes.base import BaseQNode, QuantumFunctionError, decompose_queue @pytest.fixture(scope="function") @@ -209,10 +209,10 @@ def circuit(x): node = BaseQNode(circuit, operable_mock_device_2_wires) with monkeypatch.context() as m: - m.setattr(QNode_old, "_current_context", node) + m.setattr(qml, "_current_context", node) with pytest.raises( QuantumFunctionError, - match="QNode._current_context must not be modified outside this method.", + match="qml._current_context must not be modified outside this method.", ): node(0.5) diff --git a/tests/beta/test_qnode_cv.py b/tests/qnodes/test_qnode_cv.py similarity index 97% rename from tests/beta/test_qnode_cv.py rename to tests/qnodes/test_qnode_cv.py index 01af9627bb7..6f21ec1d931 100644 --- a/tests/beta/test_qnode_cv.py +++ b/tests/qnodes/test_qnode_cv.py @@ -20,8 +20,8 @@ import pennylane as qml from pennylane._device import Device from pennylane.operation import CVObservable -from pennylane.beta.qnodes.base import QuantumFunctionError -from pennylane.beta.qnodes.cv import CVQNode +from pennylane.qnodes.base import QuantumFunctionError +from pennylane.qnodes.cv import CVQNode class PolyN(qml.ops.PolyXP): diff --git a/tests/beta/test_qnode_decorator.py b/tests/qnodes/test_qnode_decorator.py similarity index 95% rename from tests/beta/test_qnode_decorator.py rename to tests/qnodes/test_qnode_decorator.py index bd6c64d4215..30233189999 100644 --- a/tests/beta/test_qnode_decorator.py +++ b/tests/qnodes/test_qnode_decorator.py @@ -19,7 +19,7 @@ import pytest import pennylane as qml -from pennylane.beta.qnodes import qnode, CVQNode, JacobianQNode, BaseQNode, QubitQNode +from pennylane.qnodes import qnode, CVQNode, JacobianQNode, BaseQNode, QubitQNode def test_create_qubit_qnode(): diff --git a/tests/beta/test_qnode_jacobian.py b/tests/qnodes/test_qnode_jacobian.py similarity index 98% rename from tests/beta/test_qnode_jacobian.py rename to tests/qnodes/test_qnode_jacobian.py index e1769391e1f..844a4f1a357 100644 --- a/tests/beta/test_qnode_jacobian.py +++ b/tests/qnodes/test_qnode_jacobian.py @@ -21,8 +21,8 @@ import pennylane as qml from pennylane._device import Device from pennylane.operation import CVObservable -from pennylane.beta.qnodes.base import QuantumFunctionError -from pennylane.beta.qnodes.jacobian import JacobianQNode +from pennylane.qnodes.base import QuantumFunctionError +from pennylane.qnodes.jacobian import JacobianQNode @pytest.fixture(scope="function") diff --git a/tests/beta/test_qnode_metric_tensor.py b/tests/qnodes/test_qnode_metric_tensor.py similarity index 99% rename from tests/beta/test_qnode_metric_tensor.py rename to tests/qnodes/test_qnode_metric_tensor.py index afd235a4b38..312029bf418 100644 --- a/tests/beta/test_qnode_metric_tensor.py +++ b/tests/qnodes/test_qnode_metric_tensor.py @@ -19,8 +19,8 @@ from scipy.linalg import block_diag import pennylane as qml -from pennylane.beta.qnodes.qubit import QubitQNode -from pennylane.beta.qnodes.base import QuantumFunctionError +from pennylane.qnodes.qubit import QubitQNode +from pennylane.qnodes.base import QuantumFunctionError from pennylane.plugins.default_qubit import Y, Z diff --git a/tests/beta/test_qnode_qubit.py b/tests/qnodes/test_qnode_qubit.py similarity index 96% rename from tests/beta/test_qnode_qubit.py rename to tests/qnodes/test_qnode_qubit.py index f6c15b6e613..48bab8efac9 100644 --- a/tests/beta/test_qnode_qubit.py +++ b/tests/qnodes/test_qnode_qubit.py @@ -20,8 +20,8 @@ import pennylane as qml from pennylane._device import Device from pennylane.operation import CVObservable -from pennylane.beta.qnodes.base import QuantumFunctionError -from pennylane.beta.qnodes.qubit import QubitQNode +from pennylane.qnodes.base import QuantumFunctionError +from pennylane.qnodes.qubit import QubitQNode thetas = np.linspace(-2*np.pi, 2*np.pi, 8) diff --git a/tests/test_circuit_graph.py b/tests/test_circuit_graph.py index c0bf99eff57..51ba91cb81f 100644 --- a/tests/test_circuit_graph.py +++ b/tests/test_circuit_graph.py @@ -168,7 +168,7 @@ def test_layers(self, parameterized_circuit): dev = qml.device("default.gaussian", wires=3) qnode = qml.QNode(parameterized_circuit, dev) - qnode.construct((0.1, 0.2, 0.3, 0.4, 0.5, 0.6)) + qnode._construct((0.1, 0.2, 0.3, 0.4, 0.5, 0.6), {}) circuit = qnode.circuit layers = circuit.layers ops = circuit.operations @@ -186,7 +186,7 @@ def test_iterate_layers(self, parameterized_circuit): dev = qml.device("default.gaussian", wires=3) qnode = qml.QNode(parameterized_circuit, dev) - qnode.construct((0.1, 0.2, 0.3, 0.4, 0.5, 0.6)) + qnode._construct((0.1, 0.2, 0.3, 0.4, 0.5, 0.6), {}) circuit = qnode.circuit result = list(circuit.iterate_layers()) diff --git a/tests/test_decorator.py b/tests/test_decorator.py deleted file mode 100644 index b5aee029fd7..00000000000 --- a/tests/test_decorator.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2018 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 :mod:`pennylane.qnode` decorator. -""" -# pylint: disable=protected-access,cell-var-from-loop -import numpy as np - -import pennylane as qml - - -class TestMethodBinding: - """Test QNode methods are correctly bound to - the wrapped function""" - - def test_jacobian(self): - """Test binding of jacobian method""" - dev = qml.device('default.qubit', wires=1) - - @qml.qnode(dev) - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert hasattr(circuit, 'jacobian') - - def circuit2(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - qnode = qml.QNode(circuit2, dev) - - assert qnode.jacobian(0.5) == circuit.jacobian(0.5) - - def test_metric_tensor(self, tol): - """Test binding of metric tensor methods""" - dev = qml.device('default.qubit', wires=1) - - a, b = 0.4, 0.1 - - @qml.qnode(dev) - def circuit(a, b): - qml.RX(a, wires=0) - qml.RY(b, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert hasattr(circuit, 'metric_tensor') - - def circuit2(a, b): - qml.RX(a, wires=0) - qml.RY(b, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - qnode = qml.QNode(circuit2, dev) - - # check that both QNode constructions agree - res = circuit.metric_tensor(a, b) - res2 = qnode.metric_tensor(a, b) - assert np.allclose(res, res2, atol=tol, rtol=0) - - # check metric tensor is correct - expected = np.diag(np.array([1, np.cos(a)**2])/4) - assert np.allclose(res, expected, atol=tol, rtol=0) diff --git a/tests/test_default_gaussian.py b/tests/test_default_gaussian.py index f20c29020d4..5c026567406 100644 --- a/tests/test_default_gaussian.py +++ b/tests/test_default_gaussian.py @@ -620,51 +620,6 @@ def test_args(self): with pytest.raises(TypeError, match="missing 1 required positional argument: 'wires'"): qml.device('default.gaussian') - @pytest.mark.parametrize("g", set(qml.ops.__all_ops__) - set(DefaultGaussian._operation_map.keys())) - def test_unsupported_gates(self, g, gaussian_device_3_wires): - """Test error is raised with unsupported gates""" - # TODO: Refactor this test to remove dynamic attribute loading, and instead - # have separate tests for gates with specific number of parameters/wires. - if g == "BasisState" or g == "QubitStateVector": - pytest.skip("Test not set up properly for gates with array parameters, needs refactoring.") - - op = getattr(qml.ops, g) - if op.num_wires <= 0: - wires = list(range(3)) - else: - wires = list(range(op.num_wires)) - - @qml.qnode(gaussian_device_3_wires) - def circuit(*x): - """Test quantum function""" - x = prep_par(x, op) - op(*x, wires=wires) - return qml.expval(qml.X(0)) if issubclass(op, qml.operation.CV) else qml.expval(qml.PauliZ(0)) - - x = np.random.random([op.num_params]) - with pytest.raises(qml.DeviceError, match="Gate {} not supported on device default.gaussian".format(g)): - circuit(*x) - - @pytest.mark.parametrize("g", set(qml.ops.__all_obs__) - set(DefaultGaussian._observable_map.keys())) - def test_unsupported_observables(self, g, gaussian_dev): - """Test error is raised with unsupported observables.""" - - op = getattr(qml.ops, g) - if op.num_wires <= 0: - wires = list(range(2)) - else: - wires = list(range(op.num_wires)) - - @qml.qnode(gaussian_dev) - def circuit(*x): - """Test quantum function""" - x = prep_par(x, op) - return qml.expval(op(*x, wires=wires)) - - x = np.random.random([op.num_params]) - with pytest.raises(qml.DeviceError, match="Observable {} not supported on device default.gaussian".format(g)): - circuit(*x) - def test_gaussian_circuit(self, tol): """Test that the default gaussian plugin provides correct result for simple circuit""" dev = qml.device('default.gaussian', wires=1) diff --git a/tests/test_default_qubit.py b/tests/test_default_qubit.py index 3c151d1b04b..1ef451485b3 100644 --- a/tests/test_default_qubit.py +++ b/tests/test_default_qubit.py @@ -839,56 +839,6 @@ def test_args(self): ): qml.device("default.qubit") - - @pytest.mark.parametrize("gate", set(qml.ops.cv.ops)) - def test_unsupported_gate_error(self, qubit_device_3_wires, gate): - """Tests that an error is raised if an unsupported gate is applied""" - op = getattr(qml.ops, gate) - - if op.num_wires is qml.operation.Wires.Any or qml.operation.Wires.All: - wires = [0] - else: - wires = list(range(op.num_wires)) - - @qml.qnode(qubit_device_3_wires) - def circuit(*x): - """Test quantum function""" - x = prep_par(x, op) - op(*x, wires=wires) - - return qml.expval(qml.X(0)) - - with pytest.raises( - qml.DeviceError, - match="Gate {} not supported on device default.qubit".format(gate), - ): - x = np.random.random([op.num_params]) - circuit(*x) - - @pytest.mark.parametrize("observable", set(qml.ops.cv.obs)) - def test_unsupported_observable_error(self, qubit_device_3_wires, observable): - """Test error is raised with unsupported observables""" - - op = getattr(qml.ops, observable) - - if op.num_wires is qml.operation.Wires.Any or qml.operation.Wires.All: - wires = [0] - else: - wires = list(range(op.num_wires)) - - @qml.qnode(qubit_device_3_wires) - def circuit(*x): - """Test quantum function""" - x = prep_par(x, op) - return qml.expval(op(*x, wires=wires)) - - with pytest.raises( - qml.DeviceError, - match="Observable {} not supported on device default.qubit".format(observable), - ): - x = np.random.random([op.num_params]) - circuit(*x) - def test_qubit_circuit(self, qubit_device_1_wire, tol): """Test that the default qubit plugin provides correct result for a simple circuit""" diff --git a/tests/test_device.py b/tests/test_device.py index 42cc87c0feb..20ad997669a 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -18,7 +18,7 @@ import pytest import pennylane as qml from pennylane import Device, DeviceError -from pennylane.qnode import QuantumFunctionError +from pennylane.qnodes import QuantumFunctionError mock_device_paulis = ["PauliX", "PauliY", "PauliZ"] diff --git a/tests/test_measure.py b/tests/test_measure.py index a9f4a77d7d1..4ba807692a0 100644 --- a/tests/test_measure.py +++ b/tests/test_measure.py @@ -16,7 +16,7 @@ import numpy as np import pennylane as qml -from pennylane.qnode import QuantumFunctionError +from pennylane.qnodes import QuantumFunctionError from pennylane.operation import Sample, Variance, Expectation diff --git a/tests/test_observable.py b/tests/test_observable.py index 677a9919686..e3fc01ef4b2 100644 --- a/tests/test_observable.py +++ b/tests/test_observable.py @@ -19,7 +19,7 @@ from scipy.linalg import block_diag import pennylane as qml -from pennylane.qnode import QuantumFunctionError +from pennylane.qnodes import QuantumFunctionError from pennylane.plugins import DefaultQubit import pytest diff --git a/tests/test_operation.py b/tests/test_operation.py index 4ba9e274914..939fc597a9c 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -187,7 +187,7 @@ def circuit(x): return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1)) node = qml.QNode(circuit, mock_device) - node.construct([1.0]) + node._construct([1.0], {}) return node @@ -209,7 +209,7 @@ def circuit(x): return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1)) node = qml.QNode(circuit, mock_device) - node.construct([1.0]) + node._construct([1.0], {}) return node @@ -525,7 +525,7 @@ def circuit(): DummyOp(wires=[0]) return qml.expval(qml.PauliZ(0)) - with pytest.raises(ValueError, match="Operator {} must act on all wires".format(DummyOp.__name__)): + with pytest.raises(qml.QuantumFunctionError, match="Operator {} must act on all wires".format(DummyOp.__name__)): circuit() diff --git a/tests/test_qnode.py b/tests/test_qnode.py deleted file mode 100644 index 3a8c6f15340..00000000000 --- a/tests/test_qnode.py +++ /dev/null @@ -1,2263 +0,0 @@ -# Copyright 2018 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 :mod:`pennylane` :class:`QNode` class. -""" -import contextlib -import io -import math -import textwrap - -import pytest -import numpy as np -from scipy.linalg import block_diag - -from pennylane.plugins.default_qubit import Y, Z - -import pennylane as qml -from pennylane._device import Device -from pennylane.qnode import QNode, QuantumFunctionError, decompose_queue - - -class TestQNodeOperationQueue: - """Tests that the QNode operation queue is properly filled and interacted with""" - - @pytest.fixture(scope="function") - def qnode(self, mock_device): - """Provides a circuit for the subsequent tests of the operation queue""" - - def circuit(x): - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.RY(0.4, wires=[0]) - qml.RZ(-0.2, wires=[1]) - return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1)) - - node = qml.QNode(circuit, mock_device) - node.construct([1.0]) - - return node - - def test_operation_ordering(self, qnode): - """Tests that the ordering of the operations is correct""" - - assert qnode.ops[0].name == "RX" - assert qnode.ops[1].name == "CNOT" - assert qnode.ops[2].name == "RY" - assert qnode.ops[3].name == "RZ" - assert qnode.ops[4].name == "PauliX" - assert qnode.ops[5].name == "PauliZ" - - def test_op_successors_operations_only(self, qnode): - """Tests that _op_successors properly extracts the successors that are operations""" - - operation_successors = qnode._op_successors(qnode.ops[0], only="G") - - assert qnode.ops[0] not in operation_successors - assert qnode.ops[1] in operation_successors - assert qnode.ops[4] not in operation_successors - - def test_op_successors_observables_only(self, qnode): - """Tests that _op_successors properly extracts the successors that are observables""" - - observable_successors = qnode._op_successors(qnode.ops[0], only="E") - - assert qnode.ops[0] not in observable_successors - assert qnode.ops[1] not in observable_successors - assert qnode.ops[4] in observable_successors - - def test_op_successors_both_operations_and_observables(self, qnode): - """Tests that _op_successors properly extracts all successors""" - - successors = qnode._op_successors(qnode.ops[0], only=None) - - assert qnode.ops[0] not in successors - assert qnode.ops[1] in successors - assert qnode.ops[4] in successors - - def test_op_successors_both_operations_and_observables_nodes(self, qnode): - """Tests that _op_successors properly extracts all successor nodes""" - - successors = qnode._op_successors(qnode.ops[0], only=None) - - assert qnode.circuit.operations[0] not in successors - assert qnode.circuit.operations[1] in successors - assert qnode.circuit.operations[2] in successors - assert qnode.circuit.operations[3] in successors - assert qnode.circuit.observables[0] in successors - - def test_op_successors_both_operations_and_observables_strict_ordering(self, qnode): - """Tests that _op_successors properly extracts all successors""" - - successors = qnode._op_successors(qnode.ops[2], only=None) - - assert qnode.circuit.operations[0] not in successors - assert qnode.circuit.operations[1] not in successors - assert qnode.circuit.operations[2] not in successors - assert qnode.circuit.operations[3] not in successors - assert qnode.circuit.observables[0] in successors - - def test_op_successors_extracts_all_successors(self, qnode): - """Tests that _op_successors properly extracts all successors""" - successors = qnode._op_successors(qnode.ops[2], only=None) - assert qnode.ops[4] in successors - assert qnode.ops[5] not in successors - - def test_print_applied(self, mock_device): - """Test that printing applied gates works correctly""" - - H = np.array([[0, 1], [1, 0]]) - - def circuit(x): - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.RY(0.4, wires=[0]) - qml.RZ(-0.2, wires=[1]) - return qml.expval(qml.PauliX(0)), qml.var(qml.Hermitian(H, wires=1)) - - expected_qnode_print = textwrap.dedent("""\ - Operations - ========== - RX({x}, wires=[0]) - CNOT(wires=[0, 1]) - RY(0.4, wires=[0]) - RZ(-0.2, wires=[1]) - - Observables - =========== - expval(PauliX(wires=[0])) - var(Hermitian([[0 1] - [1 0]], wires=[1]))""") - - node = qml.QNode(circuit, mock_device) - - # test before construction - f = io.StringIO() - - with contextlib.redirect_stdout(f): - node.print_applied() - out = f.getvalue().strip() - - assert out == "QNode has not yet been executed." - - # construct QNode - f = io.StringIO() - node.construct([0.1]) - - with contextlib.redirect_stdout(f): - node.print_applied() - out = f.getvalue().strip() - - assert out == expected_qnode_print.format(x=0.1) - - -@pytest.fixture(scope="function") -def operable_mock_device_2_wires(monkeypatch): - """A mock instance of the abstract Device class that can support qfuncs.""" - - dev = Device - with monkeypatch.context() as m: - m.setattr(dev, '__abstractmethods__', frozenset()) - m.setattr(dev, 'operations', ["BasisState", "RX", "RY", "CNOT", "Rot", "PhaseShift"]) - m.setattr(dev, 'observables', ["PauliX", "PauliY", "PauliZ"]) - m.setattr(dev, 'reset', lambda self: None) - m.setattr(dev, 'apply', lambda self, x, y, z: None) - m.setattr(dev, 'expval', lambda self, x, y, z: 1) - yield Device(wires=2) - - -@pytest.fixture(scope="function") -def operable_mock_CV_device_2_wires(monkeypatch): - """A mock instance of the abstract Device class that can support qfuncs.""" - - dev = Device - with monkeypatch.context() as m: - m.setattr(dev, '__abstractmethods__', frozenset()) - m.setattr(dev, 'operations', ["Displacement", "CubicPhase", "Squeezing", "Rotation", "Kerr", "Beamsplitter"]) - m.setattr(dev, 'observables', ["X", "NumberOperator"]) - m.setattr(dev, 'reset', lambda self: None) - m.setattr(dev, 'apply', lambda self, x, y, z: None) - m.setattr(dev, 'expval', lambda self, x, y, z: 1) - yield Device(wires=2) - - -class TestQNodeBestMethod: - """ - Test different flows of _best_method - """ - def test_best_method_with_non_gaussian_successors(self, tol, gaussian_device_2_wires): - """Tests that the analytic differentiation method is allowed and matches numerical - differentiation if a non-Gaussian gate is not succeeded by an observable.""" - - @qml.qnode(gaussian_device_2_wires) - def circuit(x): - qml.Squeezing(x, 0, wires=[0]) - qml.Beamsplitter(np.pi/4, 0, wires=[0, 1]) - qml.Kerr(0.54, wires=[1]) - return qml.expval(qml.NumberOperator(0)) - - res = circuit.jacobian([0.321], method='A') - expected = circuit.jacobian([0.321], method='F') - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_best_method_with_gaussian_successors_fails(self, gaussian_device_2_wires): - """Tests that the analytic differentiation method is not allowed - if a non-Gaussian gate is succeeded by an observable.""" - - @qml.qnode(gaussian_device_2_wires) - def circuit(x): - qml.Squeezing(x, 0, wires=[0]) - qml.Beamsplitter(np.pi/4, 0, wires=[0, 1]) - qml.Kerr(0.54, wires=[1]) - return qml.expval(qml.NumberOperator(1)) - - with pytest.raises(ValueError, match="analytic gradient method cannot be used with"): - circuit.jacobian([0.321], method='A') - - -class TestQNodeExceptions: - """Tests that QNode raises proper errors""" - - def test_current_context_modified_outside_construct(self, mock_device, monkeypatch): - """Tests that the QNode properly raises an error if the _current_context - was modified outside of construct""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(wires=0)) - - node = qml.QNode(circuit, mock_device) - - monkeypatch.setattr(QNode, "_current_context", node) - - with pytest.raises( - QuantumFunctionError, - match="QNode._current_context must not be modified outside this method.", - ): - node.construct([0.0]) - - def test_return_of_non_observable(self, operable_mock_device_2_wires): - """Tests that the QNode properly raises an error if the qfunc returns something - besides observables.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(wires=0)), 0.3 - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(QuantumFunctionError, match="must return either"): - node(0.5) - - def test_observable_not_returned(self, operable_mock_device_2_wires): - """Tests that the QNode properly raises an error if the qfunc does not - return all observables.""" - - def circuit(x): - qml.RX(x, wires=[0]) - ex = qml.expval(qml.PauliZ(wires=1)) - return qml.expval(qml.PauliZ(wires=0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(QuantumFunctionError, match="All measured observables"): - node(0.5) - - def test_observable_order_violated(self, operable_mock_device_2_wires): - """Tests that the QNode properly raises an error if the qfunc does not - return all observables in the correct order.""" - - def circuit(x): - qml.RX(x, wires=[0]) - ex = qml.expval(qml.PauliZ(wires=1)) - return qml.expval(qml.PauliZ(wires=0)), ex - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(QuantumFunctionError, match="All measured observables"): - node(0.5) - - def test_operations_after_observables(self, operable_mock_device_2_wires): - """Tests that the QNode properly raises an error if the qfunc contains - operations after observables.""" - - def circuit(x): - qml.RX(x, wires=[0]) - ex = qml.expval(qml.PauliZ(wires=1)) - qml.RY(0.5, wires=[0]) - return qml.expval(qml.PauliZ(wires=0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(QuantumFunctionError, match="gates must precede"): - node(0.5) - - def test_multiple_measurements_on_same_wire(self, operable_mock_device_2_wires): - """Tests that the QNode properly raises an error if the same wire - is measured multiple times.""" - - def circuit(x): - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)), qml.expval(qml.PauliX(0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(QuantumFunctionError, match="can only be measured once"): - node(0.5) - - def test_operation_on_nonexistant_wire(self, operable_mock_device_2_wires): - """Tests that the QNode properly raises an error if an operation - is applied to a non-existant wire.""" - - operable_mock_device_2_wires.num_wires = 2 - - def circuit(x): - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 2]) - return qml.expval(qml.PauliZ(0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(QuantumFunctionError, match="applied to invalid wire"): - node(0.5) - - def test_observable_on_nonexistant_wire(self, operable_mock_device_2_wires): - """Tests that the QNode properly raises an error if an observable - is measured on a non-existant wire.""" - - operable_mock_device_2_wires.num_wires = 2 - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(2)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(QuantumFunctionError, match="applied to invalid wire"): - node(0.5) - - def test_mixing_of_cv_and_qubit_operations(self, operable_mock_device_2_wires): - """Tests that the QNode properly raises an error if qubit and - CV operations are mixed in the same qfunc.""" - - def circuit(x): - qml.RX(x, wires=[0]) - qml.Displacement(0.5, 0, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(QuantumFunctionError, match="Continuous and discrete"): - node(0.5) - - def test_transform_observable_incorrect_heisenberg_size(self): - """Test that an exception is raised in the case that the - dimensions of a CV observable Heisenberg representation does not match - the ev_order attribute""" - - dev = qml.device("default.gaussian", wires=1) - - class P(qml.operation.CVObservable): - """Dummy CV observable with incorrect ev_order""" - num_wires = 1 - num_params = 0 - par_domain = None - ev_order = 2 - - @staticmethod - def _heisenberg_rep(p): - return np.array([0, 1, 0]) - - def circuit(x): - qml.Displacement(x, 0.1, wires=0) - return qml.expval(P(0)) - - node = qml.QNode(circuit, dev) - - with pytest.raises(QuantumFunctionError, match="Mismatch between polynomial order"): - node.jacobian([0.5]) - - -class TestQNodeJacobianExceptions: - """Tests that QNode.jacobian raises proper errors""" - - def test_undifferentiable_operation(self, operable_mock_device_2_wires): - """Tests that QNode.jacobian properly raises an error if the - qfunc contains an operation that is not differentiable.""" - - def circuit(x): - qml.BasisState(np.array([x, 0]), wires=[0, 1]) - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(ValueError, match="Cannot differentiate wrt parameter"): - node.jacobian(0.5) - - def test_operation_not_supporting_analytic_gradient(self, operable_mock_device_2_wires): - """Tests that QNode.jacobian properly raises an error if the - qfunc contains an operation that does not support analytic gradients.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.Hermitian(np.diag([x, 0]), 0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(ValueError, match="analytic gradient method cannot be used with"): - node.jacobian(0.5, method="A") - - def test_bogus_gradient_method_set(self, operable_mock_device_2_wires): - """Tests that QNode.jacobian properly raises an error if the - gradient method set is bogus.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - # in non-cached mode, the grad method would be - # recomputed and overwritten from the - # bogus value 'J'. Caching stops this from happening. - node = qml.QNode(circuit, operable_mock_device_2_wires, cache=True) - - node.evaluate([0.0]) - keys = node.grad_method_for_par.keys() - if keys: - k0 = [k for k in keys][0] - - node.grad_method_for_par[k0] = "J" - - with pytest.raises(ValueError, match="Unknown gradient method"): - node.jacobian(0.5) - - def test_indices_not_unique(self, operable_mock_device_2_wires): - """Tests that QNode.jacobian properly raises an error if the - jacobian is requested for non-unique indices.""" - - def circuit(x): - qml.Rot(0.3, x, -0.2, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(ValueError, match="Parameter indices must be unique."): - node.jacobian(0.5, which=[0, 0]) - - def test_indices_nonexistant(self, operable_mock_device_2_wires): - """Tests that QNode.jacobian properly raises an error if the - jacobian is requested for non-existant parameters.""" - - def circuit(x): - qml.Rot(0.3, x, -0.2, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(ValueError, match="Tried to compute the gradient wrt"): - node.jacobian(0.5, which=[0, 6]) - - with pytest.raises(ValueError, match="Tried to compute the gradient wrt"): - node.jacobian(0.5, which=[1, -1]) - - def test_unknown_method(self, operable_mock_device_2_wires): - """Tests that QNode.jacobian properly raises an error if the - gradient method is unknown.""" - - def circuit(x): - qml.Rot(0.3, x, -0.2, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(ValueError, match="Unknown gradient method"): - node.jacobian(0.5, method="unknown") - - def test_wrong_order_in_finite_difference(self, operable_mock_device_2_wires): - """Tests that QNode.jacobian properly raises an error if finite - differences are attempted with wrong order.""" - - def circuit(x): - qml.Rot(0.3, x, -0.2, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = qml.QNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(ValueError, match="Order must be 1 or 2"): - node.jacobian(0.5, method="F", order=3) - - -class TestQNodeParameters: - """Tests the handling of parameters in the QNode""" - - @pytest.mark.parametrize( - "x,y", - zip(np.linspace(-2 * np.pi, 2 * np.pi, 7), np.linspace(-2 * np.pi, 2 * np.pi, 7) ** 2 / 11), - ) - def test_fanout(self, qubit_device_1_wire, tol, x, y): - """Tests that qnodes can compute the correct function when the - same parameter is used in multiple gates.""" - - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RZ(y, wires=[0]) - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - def analytic_expval(x, y): - return math.cos(x) ** 2 - math.cos(y) * math.sin(x) ** 2 - - node = qml.QNode(circuit, qubit_device_1_wire) - - assert np.isclose(node(x, y), analytic_expval(x, y), atol=tol, rtol=0) - - def test_array_parameters_scalar_return(self, qubit_device_1_wire, tol): - """Test that QNode can take arrays as input arguments, and that they interact properly with Autograd. - Test case for a circuit that returns a scalar.""" - - def circuit(dummy1, array, dummy2): - qml.RY(0.5 * array[0, 1], wires=0) - qml.RY(-0.5 * array[1, 1], wires=0) - return qml.expval(qml.PauliX(0)) - - node = qml.QNode(circuit, qubit_device_1_wire) - - args = (0.46, np.array([[2.0, 3.0, 0.3], [7.0, 4.0, 2.1]]), -0.13) - grad_target = ( - np.array(1.0), - np.array([[0.5, 0.43879, 0], [0, -0.43879, 0]]), - np.array(-0.4), - ) - cost_target = 1.03257 - - def cost(x, array, y): - c = node(0.111, array, 4.5) - return c + 0.5 * array[0, 0] + x - 0.4 * y - - cost_grad = qml.grad(cost, argnum=[0, 1, 2]) - computed_grad = cost_grad(*args) - - assert np.isclose(cost(*args), cost_target, atol=tol, rtol=0) - - assert np.allclose(computed_grad[0], grad_target[0], atol=tol, rtol=0) - assert np.allclose(computed_grad[1], grad_target[1], atol=tol, rtol=0) - assert np.allclose(computed_grad[2], grad_target[2], atol=tol, rtol=0) - - def test_qnode_array_parameters_1_vector_return(self, qubit_device_1_wire, tol): - """Test that QNode can take arrays as input arguments, and that they interact properly with Autograd. - Test case for a circuit that returns a 1-vector.""" - - def circuit(dummy1, array, dummy2): - qml.RY(0.5 * array[0, 1], wires=0) - qml.RY(-0.5 * array[1, 1], wires=0) - return (qml.expval(qml.PauliX(0)),) - - node = qml.QNode(circuit, qubit_device_1_wire) - - args = (0.46, np.array([[2.0, 3.0, 0.3], [7.0, 4.0, 2.1]]), -0.13) - grad_target = ( - np.array(1.0), - np.array([[0.5, 0.43879, 0], [0, -0.43879, 0]]), - np.array(-0.4), - ) - cost_target = 1.03257 - - def cost(x, array, y): - c = node(0.111, array, 4.5)[0] - return c + 0.5 * array[0, 0] + x - 0.4 * y - - cost_grad = qml.grad(cost, argnum=[0, 1, 2]) - computed_grad = cost_grad(*args) - - assert np.isclose(cost(*args), cost_target, atol=tol, rtol=0) - - assert np.allclose(computed_grad[0], grad_target[0], atol=tol, rtol=0) - assert np.allclose(computed_grad[1], grad_target[1], atol=tol, rtol=0) - assert np.allclose(computed_grad[2], grad_target[2], atol=tol, rtol=0) - - def test_qnode_array_parameters_2_vector_return(self, qubit_device_2_wires, tol): - """Test that QNode can take arrays as input arguments, and that they interact properly with Autograd. - Test case for a circuit that returns a 2-vector.""" - - def circuit(dummy1, array, dummy2): - qml.RY(0.5 * array[0, 1], wires=0) - qml.RY(-0.5 * array[1, 1], wires=0) - qml.RY(array[1, 0], wires=1) - return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1)) - - node = qml.QNode(circuit, qubit_device_2_wires) - - args = (0.46, np.array([[2.0, 3.0, 0.3], [7.0, 4.0, 2.1]]), -0.13) - grad_target = ( - np.array(1.0), - np.array([[0.5, 0.43879, 0], [0, -0.43879, 0]]), - np.array(-0.4), - ) - cost_target = 1.03257 - - def cost(x, array, y): - c = node(0.111, array, 4.5)[0] - return c + 0.5 * array[0, 0] + x - 0.4 * y - - cost_grad = qml.grad(cost, argnum=[0, 1, 2]) - computed_grad = cost_grad(*args) - - assert np.isclose(cost(*args), cost_target, atol=tol, rtol=0) - - assert np.allclose(computed_grad[0], grad_target[0], atol=tol, rtol=0) - assert np.allclose(computed_grad[1], grad_target[1], atol=tol, rtol=0) - assert np.allclose(computed_grad[2], grad_target[2], atol=tol, rtol=0) - - def test_array_parameters_evaluate(self, qubit_device_2_wires, tol): - """Tests that array parameters gives same result as positional arguments.""" - a, b, c = 0.5, 0.54, 0.3 - - def ansatz(x, y, z): - qml.QubitStateVector(np.array([1, 0, 1, 1]) / np.sqrt(3), wires=[0, 1]) - qml.Rot(x, y, z, wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1)) - - @qml.qnode(qubit_device_2_wires) - def circuit1(x, y, z): - return ansatz(x, y, z) - - @qml.qnode(qubit_device_2_wires) - def circuit2(x, array): - return ansatz(x, array[0], array[1]) - - @qml.qnode(qubit_device_2_wires) - def circuit3(array): - return ansatz(*array) - - positional_res = circuit1(a, b, c) - positional_grad = circuit1.jacobian([a, b, c]) - - array_res = circuit2(a, np.array([b, c])) - array_grad = circuit2.jacobian([a, np.array([b, c])]) - - assert np.allclose(positional_res, array_res, atol=tol, rtol=0) - assert np.allclose(positional_grad, array_grad, atol=tol, rtol=0) - - list_res = circuit2(a, [b, c]) - list_grad = circuit2.jacobian([a, [b, c]]) - - assert np.allclose(positional_res, list_res, atol=tol, rtol=0) - assert np.allclose(positional_grad, list_grad, atol=tol, rtol=0) - - array_res = circuit3(np.array([a, b, c])) - array_grad = circuit3.jacobian([np.array([a, b, c])]) - - list_res = circuit3([a, b, c]) - list_grad = circuit3.jacobian([[a, b, c]]) - - assert np.allclose(positional_res, array_res, atol=tol, rtol=0) - assert np.allclose(positional_grad, array_grad, atol=tol, rtol=0) - - def test_multiple_expectation_different_wires(self, qubit_device_2_wires, tol): - """Tests that qnodes return multiple expectation values.""" - - a, b, c = 0.5, 0.54, 0.3 - - @qml.qnode(qubit_device_2_wires) - def circuit(x, y, z): - qml.RX(x, wires=[0]) - qml.RZ(y, wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.RY(y, wires=[0]) - qml.RX(z, wires=[0]) - return qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(1)) - - def analytic_expval(a, b, c): - return [-1 * math.cos(a) * math.cos(b) * math.sin(c), math.cos(a)] - - res = circuit(a, b, c) - analytic_res = analytic_expval(a, b, c) - - assert np.allclose(res, analytic_res, atol=tol, rtol=0) - - -class TestQNodeKeywordArguments: - """Tests that the qnode properly handles keyword arguments.""" - - def test_multiple_keywordargs_used(self, qubit_device_2_wires, tol): - """Tests that qnodes use multiple keyword arguments.""" - - def circuit(w, x=None, y=None): - qml.RX(x, wires=[0]) - qml.RX(y, wires=[1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - node = qml.QNode(circuit, qubit_device_2_wires) - - c = node(1.0, x=np.pi, y=np.pi) - - assert np.allclose(c, [-1.0, -1.0], atol=tol, rtol=0) - - def test_multidimensional_keywordargs_used(self, qubit_device_2_wires, tol): - """Tests that qnodes use multi-dimensional keyword arguments.""" - - def circuit(w, x=None): - qml.RX(x[0], wires=[0]) - qml.RX(x[1], wires=[1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - node = qml.QNode(circuit, qubit_device_2_wires) - - c = node(1.0, x=[np.pi, np.pi]) - - assert np.allclose(c, [-1.0, -1.0], atol=tol, rtol=0) - - def test_keywordargs_for_wires(self, qubit_device_2_wires, tol): - """Tests that wires can be passed as keyword arguments.""" - - default_q = 0 - - def circuit(x, q=default_q): - qml.RX(x, wires=[q]) - return qml.expval(qml.PauliZ(q)) - - node = qml.QNode(circuit, qubit_device_2_wires) - - c = node(np.pi, q=1) - - assert node.queue[0].wires == [1] - assert np.isclose(c, -1.0, atol=tol, rtol=0) - - c = node(np.pi) - - assert node.queue[0].wires == [default_q] - assert np.isclose(c, -1.0, atol=tol, rtol=0) - - def test_keywordargs_used(self, qubit_device_1_wire, tol): - """Tests that qnodes use keyword arguments.""" - - def circuit(w, x=None): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = qml.QNode(circuit, qubit_device_1_wire) - - c = node(1.0, x=np.pi) - - assert np.isclose(c, -1.0, atol=tol, rtol=0) - - def test_keywordarg_updated_in_multiple_calls(self, qubit_device_2_wires, tol): - """Tests that qnodes update keyword arguments in consecutive calls.""" - - def circuit(w, x=None): - qml.RX(w, wires=[0]) - qml.RX(x, wires=[1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - node = qml.QNode(circuit, qubit_device_2_wires) - - c1 = node(0.1, x=0.0) - c2 = node(0.1, x=np.pi) - - assert c1[1] != c2[1] - - def test_keywordarg_passes_through_classicalnode(self, qubit_device_2_wires, tol): - """Tests that qnodes' keyword arguments pass through classical nodes.""" - - def circuit(w, x=None): - qml.RX(w, wires=[0]) - qml.RX(x, wires=[1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - node = qml.QNode(circuit, qubit_device_2_wires) - - def classical_node(w, x=None): - return node(w, x=x) - - c = classical_node(0.0, x=np.pi) - - assert np.allclose(c, [1.0, -1.0], atol=tol, rtol=0) - - -class TestQNodeGradients: - """Qnode gradient tests.""" - - @pytest.mark.parametrize("shape", [(8,), (8, 1), (4, 2), (2, 2, 2), (2, 1, 2, 1, 2)]) - def test_multidim_array(self, shape, tol): - """Tests that arguments which are multidimensional arrays are - properly evaluated and differentiated in QNodes.""" - - base_array = np.linspace(-1.0, 1.0, 8) - multidim_array = np.reshape(base_array, shape) - - def circuit(w): - qml.RX(w[np.unravel_index(0, shape)], wires=0) # base_array[0] - qml.RX(w[np.unravel_index(1, shape)], wires=1) # base_array[1] - qml.RX(w[np.unravel_index(2, shape)], wires=2) # ... - qml.RX(w[np.unravel_index(3, shape)], wires=3) - qml.RX(w[np.unravel_index(4, shape)], wires=4) - qml.RX(w[np.unravel_index(5, shape)], wires=5) - qml.RX(w[np.unravel_index(6, shape)], wires=6) - qml.RX(w[np.unravel_index(7, shape)], wires=7) - return tuple(qml.expval(qml.PauliZ(idx)) for idx in range(len(base_array))) - - dev = qml.device("default.qubit", wires=8) - circuit = qml.QNode(circuit, dev) - - # circuit evaluations - circuit_output = circuit(multidim_array) - expected_output = np.cos(base_array) - assert np.allclose(circuit_output, expected_output, atol=tol, rtol=0) - - # circuit jacobians - circuit_jacobian = circuit.jacobian([multidim_array]) - expected_jacobian = -np.diag(np.sin(base_array)) - assert np.allclose(circuit_jacobian, expected_jacobian, atol=tol, rtol=0) - - def test_qnode_cv_gradient_methods(self, operable_mock_CV_device_2_wires): - """Tests the gradient computation methods on CV circuits.""" - # we can only use the 'A' method on parameters which only affect gaussian operations - # that are not succeeded by nongaussian operations - - par = [0.4, -2.3] - - def check_methods(qf, d): - q = qml.QNode(qf, operable_mock_CV_device_2_wires) - # NOTE: the default plugin is a discrete (qubit) simulator, it cannot - # execute CV gates, but the QNode can be constructed - q.construct(par) - assert q.grad_method_for_par == d - - def qf(x, y): - qml.Displacement(x, 0, wires=[0]) - qml.CubicPhase(0.2, wires=[0]) - qml.Squeezing(0.3, y, wires=[1]) - qml.Rotation(1.3, wires=[1]) - # nongaussian succeeding x but not y - qml.Kerr(0.4, wires=[0]) - return qml.expval(qml.X(0)), qml.expval(qml.X(1)) - - check_methods(qf, {0: "F", 1: "A"}) - - def qf(x, y): - qml.Displacement(x, 0, wires=[0]) - qml.CubicPhase(0.2, wires=[0]) # nongaussian succeeding x - qml.Squeezing(0.3, x, wires=[1]) # x affects gates on both wires, y unused - qml.Rotation(1.3, wires=[1]) - return qml.expval(qml.X(0)), qml.expval(qml.X(1)) - - check_methods(qf, {0: "F"}) - - def qf(x, y): - qml.Displacement(x, 0, wires=[0]) - qml.Displacement(1.2, y, wires=[0]) - qml.Beamsplitter(0.2, 1.7, wires=[0, 1]) - qml.Rotation(1.9, wires=[0]) - qml.Kerr(0.3, wires=[1]) # nongaussian succeeding both x and y due to the beamsplitter - return qml.expval(qml.X(0)), qml.expval(qml.X(1)) - - check_methods(qf, {0: "F", 1: "F"}) - - def qf(x, y): - qml.Kerr(y, wires=[1]) - qml.Displacement(x, 0, wires=[0]) - qml.Beamsplitter(0.2, 1.7, wires=[0, 1]) - return qml.expval(qml.X(0)), qml.expval(qml.X(1)) - - check_methods(qf, {0: "A", 1: "F"}) - - def test_qnode_gradient_multiple_gate_parameters(self, tol): - """Tests that gates with multiple free parameters yield correct gradients.""" - par = [0.5, 0.3, -0.7] - - def qf(x, y, z): - qml.RX(0.4, wires=[0]) - qml.Rot(x, y, z, wires=[0]) - qml.RY(-0.2, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=1) - q = qml.QNode(qf, dev) - value = q(*par) - grad_A = q.jacobian(par, method="A") - grad_F = q.jacobian(par, method="F") - - # analytic method works for every parameter - assert q.grad_method_for_par == {0: "A", 1: "A", 2: "A"} - # 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) - - def test_qnode_gradient_gate_with_two_parameters(self, tol): - """Test that a gate with two parameters yields - correct gradients""" - def qf(r0, phi0, r1, phi1): - qml.Squeezing(r0, phi0, wires=[0]) - qml.Squeezing(r1, phi1, wires=[0]) - return qml.expval(qml.NumberOperator(0)) - - dev = qml.device('default.gaussian', wires=2) - q = qml.QNode(qf, dev) - - par = [0.543, 0.123, 0.654, -0.629] - - grad_A = q.jacobian(par, method='A') - grad_F = q.jacobian(par, method='F') - - # the different methods agree - assert np.allclose(grad_A, grad_F, atol=tol, rtol=0) - - def test_second_order_obs_not_following_gate(self, tol): - """Parshift differentiation method matches finite diff and analytical result - when we have order-2 observables that do not follow the parametrized gate. - """ - num_wires = 2 - dev = qml.device("default.gaussian", wires=2) - def circuit(params): - for i in range(num_wires): - qml.Squeezing(params[i], 0, wires=i) - return [qml.expval(qml.NumberOperator(wires=i)) for i in range(num_wires)] - - node = qml.QNode(circuit, dev) - par = [0.321, -0.184] - - res = node(par) - res_true = np.sinh(np.abs(par)) ** 2 # analytical result - assert res == pytest.approx(res_true, abs=tol) - - grad_A = node.jacobian([par], method="A") - grad_F = node.jacobian([par], method="F") - grad_true = np.diag(np.sinh(2 * np.abs(par)) * np.sign(par)) # analytical gradient - assert grad_A == pytest.approx(grad_F, abs=tol) - assert grad_A == pytest.approx(grad_true, abs=tol) - - def test_qnode_gradient_repeated_gate_parameters(self, tol): - """Tests that repeated use of a free parameter in a - multi-parameter gate yield correct gradients.""" - par = [0.8, 1.3] - - def qf(x, y): - qml.RX(np.pi / 4, wires=[0]) - qml.Rot(y, x, 2 * x, wires=[0]) - return qml.expval(qml.PauliX(0)) - - dev = qml.device("default.qubit", wires=1) - q = qml.QNode(qf, dev) - grad_A = q.jacobian(par, method="A") - grad_F = q.jacobian(par, method="F") - - # the different methods agree - assert np.allclose(grad_A, grad_F, atol=tol, rtol=0) - - def test_qnode_gradient_parameters_inside_array(self, tol): - """Tests that free parameters inside an array passed to - an Operation yield correct gradients.""" - par = [0.8, 1.3] - - def qf(x, y): - qml.RX(x, wires=[0]) - qml.RY(x, wires=[0]) - return qml.expval(qml.Hermitian(np.diag([y, 1]), 0)) - - dev = qml.device("default.qubit", wires=1) - q = qml.QNode(qf, dev) - grad = q.jacobian(par) - grad_F = q.jacobian(par, method="F") - - # par[0] can use the 'A' method, par[1] cannot - assert q.grad_method_for_par == {0: "A", 1: "F"} - # the different methods agree - assert np.allclose(grad, grad_F, atol=tol, rtol=0) - - def test_array_parameters_autograd(self, tol): - """Test that gradients of array parameters give - same results as positional arguments.""" - - a, b, c = 0.5, 0.54, 0.3 - - def ansatz(x, y, z): - qml.QubitStateVector(np.array([1, 0, 1, 1]) / np.sqrt(3), wires=[0, 1]) - qml.Rot(x, y, z, wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - def circuit1(x, y, z): - return ansatz(x, y, z) - - def circuit2(x, array): - return ansatz(x, array[0], array[1]) - - def circuit3(array): - return ansatz(*array) - - dev = qml.device("default.qubit", wires=2) - circuit1 = qml.QNode(circuit1, dev) - grad1 = qml.grad(circuit1, argnum=[0, 1, 2]) - - positional_grad = circuit1.jacobian([a, b, c]) - positional_autograd = grad1(a, b, c) - assert np.allclose(positional_grad, positional_autograd, atol=tol, rtol=0) - - circuit2 = qml.QNode(circuit2, dev) - grad2 = qml.grad(circuit2, argnum=[0, 1]) - - circuit3 = qml.QNode(circuit3, dev) - grad3 = qml.grad(circuit3, argnum=0) - - array_grad = circuit3.jacobian([np.array([a, b, c])]) - array_autograd = grad3(np.array([a, b, c])) - assert np.allclose(array_grad, array_autograd, atol=tol, rtol=0) - - @staticmethod - def expected_jacobian(x, y, z): - dw0dx = 2 / 3 * np.sin(x) * np.sin(y) - dw0dy = 1 / 3 * (np.sin(y) - 2 * np.cos(x) * np.cos(y)) - dw0dz = 0 - - dw1dx = -2 / 3 * np.cos(x) * np.sin(y) - dw1dy = -2 / 3 * np.cos(y) * np.sin(x) - dw1dz = 0 - - return np.array([[dw0dx, dw0dy, dw0dz], [dw1dx, dw1dy, dw1dz]]) - - def test_multiple_expectation_jacobian_positional(self, tol): - """Tests that qnodes using positional arguments return - correct gradients for multiple expectation values.""" - a, b, c = 0.5, 0.54, 0.3 - - def circuit(x, y, z): - qml.QubitStateVector(np.array([1, 0, 1, 1]) / np.sqrt(3), wires=[0, 1]) - qml.Rot(x, y, z, wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1)) - - dev = qml.device("default.qubit", wires=2) - circuit = qml.QNode(circuit, dev) - - # compare our manual Jacobian computation to theoretical result - # Note: circuit.jacobian actually returns a full jacobian in this case - res = circuit.jacobian(np.array([a, b, c])) - assert np.allclose(self.expected_jacobian(a, b, c), res, atol=tol, rtol=0) - - # compare our manual Jacobian computation to autograd - # not sure if this is the intended usage of jacobian - jac0 = qml.jacobian(circuit, 0) - jac1 = qml.jacobian(circuit, 1) - jac2 = qml.jacobian(circuit, 2) - res = np.stack([jac0(a, b, c), jac1(a, b, c), jac2(a, b, c)]).T - - assert np.allclose(self.expected_jacobian(a, b, c), res, atol=tol, rtol=0) - - #compare with what we get if argnum is a list - res2 = qml.jacobian(circuit, argnum=[0, 1, 2])(a, b, c) - assert np.allclose(res, res2, atol=tol, rtol=0) - - def test_multiple_expectation_jacobian_array(self, tol): - """Tests that qnodes using an array argument return correct gradients - for multiple expectation values.""" - a, b, c = 0.5, 0.54, 0.3 - - def circuit(weights): - qml.QubitStateVector(np.array([1, 0, 1, 1]) / np.sqrt(3), wires=[0, 1]) - qml.Rot(weights[0], weights[1], weights[2], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1)) - - dev = qml.device("default.qubit", wires=2) - circuit = qml.QNode(circuit, dev) - - res = circuit.jacobian([np.array([a, b, c])]) - assert np.allclose(self.expected_jacobian(a, b, c), res, atol=tol, rtol=0) - - jac = qml.jacobian(circuit, 0) - res = jac(np.array([a, b, c])) - assert np.allclose(self.expected_jacobian(a, b, c), res, atol=tol, rtol=0) - - def test_keywordarg_not_differentiated(self, tol): - """Tests that qnodes do not differentiate w.r.t. keyword arguments.""" - a, b = 0.5, 0.54 - - def circuit1(weights, x=0.3): - qml.QubitStateVector(np.array([1, 0, 1, 1]) / np.sqrt(3), wires=[0, 1]) - qml.Rot(weights[0], weights[1], x, wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1)) - - dev = qml.device("default.qubit", wires=2) - circuit1 = qml.QNode(circuit1, dev) - - def circuit2(weights): - qml.QubitStateVector(np.array([1, 0, 1, 1]) / np.sqrt(3), wires=[0, 1]) - qml.Rot(weights[0], weights[1], 0.3, wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1)) - - circuit2 = qml.QNode(circuit2, dev) - - res1 = circuit1.jacobian([np.array([a, b])]) - res2 = circuit2.jacobian([np.array([a, b])]) - - assert np.allclose(res1, res2, atol=tol, rtol=0) - - def test_differentiate_all_positional(self, tol): - """Tests that all positional arguments are differentiated.""" - - def circuit1(a, b, c): - qml.RX(a, wires=0) - qml.RX(b, wires=1) - qml.RX(c, wires=2) - return tuple(qml.expval(qml.PauliZ(idx)) for idx in range(3)) - - dev = qml.device("default.qubit", wires=3) - circuit1 = qml.QNode(circuit1, dev) - - vals = np.array([np.pi, np.pi / 2, np.pi / 3]) - circuit_output = circuit1(*vals) - expected_output = np.cos(vals) - assert np.allclose(circuit_output, expected_output, atol=tol, rtol=0) - - # circuit jacobians - circuit_jacobian = circuit1.jacobian(vals) - expected_jacobian = -np.diag(np.sin(vals)) - assert np.allclose(circuit_jacobian, expected_jacobian, atol=tol, rtol=0) - - def test_differentiate_first_positional(self, tol): - """Tests that the first positional arguments are differentiated.""" - - def circuit2(a, b): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=2) - circuit2 = qml.QNode(circuit2, dev) - - a = 0.7418 - b = -5.0 - circuit_output = circuit2(a, b) - expected_output = np.cos(a) - assert np.allclose(circuit_output, expected_output, atol=tol, rtol=0) - - # circuit jacobians - circuit_jacobian = circuit2.jacobian([a, b]) - expected_jacobian = np.array([[-np.sin(a), 0]]) - assert np.allclose(circuit_jacobian, expected_jacobian, atol=tol, rtol=0) - - def test_differentiate_second_positional(self, tol): - """Tests that the second positional arguments are differentiated.""" - - def circuit3(a, b): - qml.RX(b, wires=0) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=2) - circuit3 = qml.QNode(circuit3, dev) - - a = 0.7418 - b = -5.0 - circuit_output = circuit3(a, b) - expected_output = np.cos(b) - assert np.allclose(circuit_output, expected_output, atol=tol, rtol=0) - - # circuit jacobians - circuit_jacobian = circuit3.jacobian([a, b]) - expected_jacobian = np.array([[0, -np.sin(b)]]) - assert np.allclose(circuit_jacobian, expected_jacobian, atol=tol, rtol=0) - - def test_differentiate_second_third_positional(self, tol): - """Tests that the second and third positional arguments are differentiated.""" - - def circuit4(a, b, c): - qml.RX(b, wires=0) - qml.RX(c, wires=1) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - dev = qml.device("default.qubit", wires=2) - circuit4 = qml.QNode(circuit4, dev) - - a = 0.7418 - b = -5.0 - c = np.pi / 7 - circuit_output = circuit4(a, b, c) - expected_output = np.array([[np.cos(b), np.cos(c)]]) - assert np.allclose(circuit_output, expected_output, atol=tol, rtol=0) - - # circuit jacobians - circuit_jacobian = circuit4.jacobian([a, b, c]) - expected_jacobian = np.array([[0.0, -np.sin(b), 0.0], [0.0, 0.0, -np.sin(c)]]) - assert np.allclose(circuit_jacobian, expected_jacobian, atol=tol, rtol=0) - - def test_differentiate_positional_multidim(self, tol): - """Tests that all positional arguments are differentiated - when they are multidimensional.""" - - def circuit(a, b): - qml.RX(a[0], wires=0) - qml.RX(a[1], wires=1) - qml.RX(b[2, 1], wires=2) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)), qml.expval(qml.PauliZ(2)) - - dev = qml.device("default.qubit", wires=3) - circuit = qml.QNode(circuit, dev) - - a = np.array([-np.sqrt(2), -0.54]) - b = np.array([np.pi / 7] * 6).reshape([3, 2]) - circuit_output = circuit(a, b) - expected_output = np.cos(np.array([[a[0], a[1], b[-1, 0]]])) - assert np.allclose(circuit_output, expected_output, atol=tol, rtol=0) - - # circuit jacobians - circuit_jacobian = circuit.jacobian([a, b]) - expected_jacobian = np.array( - [ - [-np.sin(a[0])] + [0.0] * 7, # expval 0 - [0.0, -np.sin(a[1])] + [0.0] * 6, # expval 1 - [0.0] * 2 + [0.0] * 5 + [-np.sin(b[2, 1])], - ] - ) # expval 2 - assert np.allclose(circuit_jacobian, expected_jacobian, atol=tol, rtol=0) - - def test_controlled_RX_gradient(self, tol): - """Test gradient of controlled RX gate""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(x): - qml.PauliX(wires=0) - qml.CRX(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - a = 0.542 # any value of a should give zero gradient - - # get the analytic gradient - gradA = circuit.jacobian([a], method="A") - # get the finite difference gradient - gradF = circuit.jacobian([a], method="F") - - # the expected gradient - expected = 0 - - assert np.allclose(gradF, expected, atol=tol, rtol=0) - assert np.allclose(gradA, expected, atol=tol, rtol=0) - - @qml.qnode(dev) - def circuit1(x): - qml.RX(x, wires=0) - qml.CRX(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - b = 0.123 # gradient is -sin(x) - - # get the analytic gradient - gradA = circuit1.jacobian([b], method="A") - # get the finite difference gradient - gradF = circuit1.jacobian([b], method="F") - - # the expected gradient - expected = -np.sin(b) - - assert np.allclose(gradF, expected, atol=tol, rtol=0) - assert np.allclose(gradA, expected, atol=tol, rtol=0) - - def test_controlled_RY_gradient(self, tol): - """Test gradient of controlled RY gate""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(x): - qml.PauliX(wires=0) - qml.CRY(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - a = 0.542 # any value of a should give zero gradient - - # get the analytic gradient - gradA = circuit.jacobian([a], method="A") - # get the finite difference gradient - gradF = circuit.jacobian([a], method="F") - - # the expected gradient - expected = 0 - - assert np.allclose(gradF, expected, atol=tol, rtol=0) - assert np.allclose(gradA, expected, atol=tol, rtol=0) - - @qml.qnode(dev) - def circuit1(x): - qml.RX(x, wires=0) - qml.CRY(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - b = 0.123 # gradient is -sin(x) - - # get the analytic gradient - gradA = circuit1.jacobian([b], method="A") - # get the finite difference gradient - gradF = circuit1.jacobian([b], method="F") - - # the expected gradient - expected = -np.sin(b) - - assert np.allclose(gradF, expected, atol=tol, rtol=0) - assert np.allclose(gradA, expected, atol=tol, rtol=0) - - def test_controlled_RZ_gradient(self, tol): - """Test gradient of controlled RZ gate""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(x): - qml.PauliX(wires=0) - qml.CRZ(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - a = 0.542 # any value of a should give zero gradient - - # get the analytic gradient - gradA = circuit.jacobian([a], method="A") - # get the finite difference gradient - gradF = circuit.jacobian([a], method="F") - - # the expected gradient - expected = 0 - - assert np.allclose(gradF, expected, atol=tol, rtol=0) - assert np.allclose(gradA, expected, atol=tol, rtol=0) - - @qml.qnode(dev) - def circuit1(x): - qml.RX(x, wires=0) - qml.CRZ(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - b = 0.123 # gradient is -sin(x) - - # get the analytic gradient - gradA = circuit1.jacobian([b], method="A") - # get the finite difference gradient - gradF = circuit1.jacobian([b], method="F") - - # the expected gradient - expected = -np.sin(b) - - assert np.allclose(gradF, expected, atol=tol, rtol=0) - assert np.allclose(gradA, expected, atol=tol, rtol=0) - - -gradient_test_data = [ - (0.5, -0.1), - (0.0, np.pi), - (-3.6, -3.6), - (1.0, 2.5), -] - - -dev = qml.device("default.qubit", wires=2) - - -@qml.qnode(dev) -def f(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - -@qml.qnode(dev) -def g(y): - qml.RY(y, wires=0) - return qml.expval(qml.PauliX(0)) - - -class TestMultiQNodeGradients: - """Multi Qnode gradient tests.""" - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_add_qnodes_gradient(self, x, y): - """Test the gradient of addition of two QNode circuits""" - - def add(a, b): - return a + b - - a = f(x) - b = g(y) - - # addition - assert qml.grad(add, argnum=0)(a, b) == 1.0 - assert qml.grad(add, argnum=1)(a, b) == 1.0 - - # same value added to itself; autograd doesn't distinguish inputs - assert qml.grad(add, argnum=0)(a, a) == 1.0 - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_subtract_qnodes_gradient(self, x, y): - """Test the gradient of subtraction of two QNode circuits""" - - def subtract(a, b): - return a - b - - a = f(x) - b = g(y) - - # subtraction - assert qml.grad(subtract, argnum=0)(a, b) == 1.0 - assert qml.grad(subtract, argnum=1)(a, b) == -1.0 - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_multiply_qnodes_gradient(self, x, y): - """Test the gradient of multiplication of two QNode circuits""" - - def mult(a, b): - return a * b - - a = f(x) - b = g(y) - - # multipication - assert qml.grad(mult, argnum=0)(a, b) == b - assert qml.grad(mult, argnum=1)(a, b) == a - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_division_qnodes_gradient(self, x, y): - """Test the gradient of division of two QNode circuits""" - - def div(a, b): - return a / b - - a = f(x) - b = g(y) - - # division - assert qml.grad(div, argnum=0)(a, b) == 1 / b - assert qml.grad(div, argnum=1)(a, b) == -a / b ** 2 - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_composing_qnodes_gradient(self, x, y): - """Test the gradient of composing of two QNode circuits""" - - def compose(f, x): - return f(x) - - a = f(x) - b = g(y) - - # composition - assert qml.grad(compose, argnum=1)(f, x) == qml.grad(f, argnum=0)(x) - assert qml.grad(compose, argnum=1)(f, a) == qml.grad(f, argnum=0)(a) - assert qml.grad(compose, argnum=1)(f, b) == qml.grad(f, argnum=0)(b) - -class TestQNodeVariance: - """Qnode variance tests.""" - - def test_involutory_variance(self, tol): - """Tests qubit observable that are involutory""" - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev) - def circuit(a): - qml.RX(a, wires=0) - return qml.var(qml.PauliZ(0)) - - a = 0.54 - var = circuit(a) - expected = 1 - np.cos(a) ** 2 - assert np.allclose(var, expected, atol=tol, rtol=0) - - # circuit jacobians - gradA = circuit.jacobian([a], method="A") - gradF = circuit.jacobian([a], method="F") - expected = 2 * np.sin(a) * np.cos(a) - assert np.allclose(gradF, expected, atol=tol, rtol=0) - assert np.allclose(gradA, expected, atol=tol, rtol=0) - - def test_non_involutory_variance(self, 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]]) - - @qml.qnode(dev) - def circuit(a): - qml.RX(a, wires=0) - return qml.var(qml.Hermitian(A, 0)) - - a = 0.54 - var = circuit(a) - expected = (39 / 2) - 6 * np.sin(2 * a) + (35 / 2) * np.cos(2 * a) - assert np.allclose(var, expected, atol=tol, rtol=0) - - # circuit jacobians - gradA = circuit.jacobian([a], method="A") - gradF = circuit.jacobian([a], method="F") - expected = -35 * np.sin(2 * a) - 12 * np.cos(2 * a) - assert np.allclose(gradA, expected, atol=tol, rtol=0) - assert np.allclose(gradF, expected, atol=tol, rtol=0) - - def test_fanout(self, tol): - """Tests qubit observable with repeated parameters""" - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev) - def circuit(a): - qml.RX(a, wires=0) - qml.RY(a, wires=0) - return qml.var(qml.PauliZ(0)) - - a = 0.54 - var = circuit(a) - expected = 0.5 * np.sin(a) ** 2 * (np.cos(2 * a) + 3) - assert np.allclose(var, expected, atol=tol, rtol=0) - - # circuit jacobians - gradA = circuit.jacobian([a], method="A") - gradF = circuit.jacobian([a], method="F") - expected = 4 * np.sin(a) * np.cos(a) ** 3 - assert np.allclose(gradA, expected, atol=tol, rtol=0) - assert np.allclose(gradF, expected, atol=tol, rtol=0) - - 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) - - @qml.qnode(dev) - def circuit(a, b, c): - 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.RZ(c, wires=2) - return qml.var(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)), qml.var(qml.PauliZ(2)) - - a = 0.54 - b = -0.423 - c = 0.123 - - var = circuit(a, b, c) - 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(var, expected, atol=tol, rtol=0) - - # # circuit jacobians - gradA = circuit.jacobian([a, b, c], method="A") - gradF = circuit.jacobian([a, b, c], method="F") - 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 np.allclose(gradF, expected, atol=tol, rtol=0) - assert np.allclose(gradA, expected, atol=tol, rtol=0) - - @pytest.mark.xfail(reason="Bug in the old QNode class: Jacobian of multiple CV variances", raises=AssertionError, strict=True) - def test_expval_and_variance_cv(self, tol): - """Test that the qnode works for a combination of CV expectation - values and variances""" - dev = qml.device("default.gaussian", wires=3) - - def circuit(a, b): - 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]) - return qml.var(qml.X(0)), qml.expval(qml.X(1)), qml.var(qml.X(2)) - - node = QNode(circuit, dev) - par = [0.54, -0.423] - - # jacobians must match - gradA = node.jacobian(par, method="A") - gradF = node.jacobian(par, method="F") - assert gradA == pytest.approx(gradF, abs=tol) - - def test_first_order_cv(self, tol): - """Test variance of a first order CV expectation value""" - dev = qml.device("default.gaussian", wires=1) - - @qml.qnode(dev) - def circuit(r, phi): - qml.Squeezing(r, 0, wires=0) - qml.Rotation(phi, wires=0) - return qml.var(qml.X(0)) - - r = 0.543 - phi = -0.654 - - var = circuit(r, phi) - expected = np.exp(2 * r) * np.sin(phi) ** 2 + np.exp(-2 * r) * np.cos(phi) ** 2 - assert np.allclose(var, expected, atol=tol, rtol=0) - - # circuit jacobians - gradA = circuit.jacobian([r, phi], method="A") - gradF = circuit.jacobian([r, phi], method="F") - 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(gradA, expected, atol=tol, rtol=0) - assert np.allclose(gradF, 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) - - @qml.qnode(dev) - def circuit(n, a): - qml.ThermalState(n, wires=0) - qml.Displacement(a, 0, wires=0) - return qml.var(qml.NumberOperator(0)) - - n = 0.12 - a = 0.765 - - var = circuit(n, a) - expected = n ** 2 + n + np.abs(a) ** 2 * (1 + 2 * n) - assert np.allclose(var, expected, atol=tol, rtol=0) - - # circuit jacobians - gradF = circuit.jacobian([n, a], method="F") - expected = np.array([2 * a ** 2 + 2 * n + 1, 2 * a * (2 * n + 1)]) - assert np.allclose(gradF, expected, atol=tol, rtol=0) - - def test_error_analytic_second_order_cv(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) - - @qml.qnode(dev) - def circuit(a): - qml.Displacement(a, 0, wires=0) - return qml.var(qml.NumberOperator(0)) - - with pytest.raises(ValueError, match=r"cannot be used with the parameter\(s\) \{0\}"): - circuit.jacobian([1.0], method="A") - - -class TestMetricTensor: - """Tests for metric tensor subcircuit construction and evaluation""" - - def test_no_generator(self): - """Test exception is raised if subcircuit contains an - operation with no generator""" - dev = qml.device('default.qubit', wires=1) - - def circuit(a): - qml.Rot(a, 0, 0, wires=0) - return qml.expval(qml.PauliX(0)) - - circuit = qml.QNode(circuit, dev) - - with pytest.raises(QuantumFunctionError, match="has no defined generator"): - circuit.construct_metric_tensor([1]) - - def test_generator_no_expval(self, monkeypatch): - """Test exception is raised if subcircuit contains an - operation with generator object that is not an observable""" - dev = qml.device('default.qubit', wires=1) - - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliX(0)) - - circuit = qml.QNode(circuit, dev) - - with monkeypatch.context() as m: - m.setattr('pennylane.RX.generator', [qml.RX, 1]) - - with pytest.raises(QuantumFunctionError, match="no corresponding observable"): - circuit.construct_metric_tensor([1]) - - def test_construct_subcircuit(self): - """Test correct subcircuits constructed""" - dev = qml.device('default.qubit', wires=2) - - def circuit(a, b, c): - qml.RX(a, wires=0) - qml.RY(b, wires=0) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(c, wires=1) - return qml.expval(qml.PauliX(0)) - - circuit = qml.QNode(circuit, dev) - - circuit.construct_metric_tensor([1, 1, 1]) - res = circuit._metric_tensor_subcircuits - - # first parameter subcircuit - assert len(res[(0,)]['queue']) == 0 - assert res[(0,)]['scale'] == [-0.5] - assert isinstance(res[(0,)]['observable'][0], qml.PauliX) - - # second parameter subcircuit - assert len(res[(1,)]['queue']) == 1 - assert res[(1,)]['scale'] == [-0.5] - assert isinstance(res[(1,)]['queue'][0], qml.RX) - assert isinstance(res[(1,)]['observable'][0], qml.PauliY) - - # third parameter subcircuit - assert len(res[(2,)]['queue']) == 3 - assert res[(2,)]['scale'] == [1] - assert isinstance(res[(2,)]['queue'][0], qml.RX) - assert isinstance(res[(2,)]['queue'][1], qml.RY) - assert isinstance(res[(2,)]['queue'][2], qml.CNOT) - assert isinstance(res[(2,)]['observable'][0], qml.Hermitian) - assert np.all(res[(2,)]['observable'][0].params[0] == qml.PhaseShift.generator[0]) - - def test_construct_subcircuit_layers(self): - """Test correct subcircuits constructed - when a layer structure exists""" - dev = qml.device('default.qubit', wires=3) - - def circuit(params): - # section 1 - qml.RX(params[0], wires=0) - # section 2 - qml.RY(params[1], wires=0) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - # section 3 - qml.RX(params[2], wires=0) - qml.RY(params[3], wires=1) - qml.RZ(params[4], wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - # section 4 - qml.RX(params[5], wires=0) - qml.RY(params[6], wires=1) - qml.RZ(params[7], wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - return qml.expval(qml.PauliX(0)) - - circuit = qml.QNode(circuit, dev) - - params = np.ones([8]) - circuit.construct_metric_tensor([params]) - res = circuit._metric_tensor_subcircuits - - # this circuit should split into 4 independent - # sections or layers when constructing subcircuits - assert len(res) == 4 - - # first layer subcircuit - layer = res[(0,)] - assert len(layer['queue']) == 0 - assert len(layer['observable']) == 1 - assert isinstance(layer['observable'][0], qml.PauliX) - - # second layer subcircuit - layer = res[(1,)] - assert len(layer['queue']) == 1 - assert len(layer['observable']) == 1 - assert isinstance(layer['queue'][0], qml.RX) - assert isinstance(layer['observable'][0], qml.PauliY) - - # third layer subcircuit - layer = res[(2, 3, 4)] - assert len(layer['queue']) == 4 - assert len(layer['observable']) == 3 - assert isinstance(layer['queue'][0], qml.RX) - assert isinstance(layer['queue'][1], qml.RY) - assert isinstance(layer['queue'][2], qml.CNOT) - assert isinstance(layer['queue'][3], qml.CNOT) - assert isinstance(layer['observable'][0], qml.PauliX) - assert isinstance(layer['observable'][1], qml.PauliY) - assert isinstance(layer['observable'][2], qml.PauliZ) - - # fourth layer subcircuit - layer = res[(5, 6, 7)] - assert len(layer['queue']) == 9 - assert len(layer['observable']) == 3 - assert isinstance(layer['queue'][0], qml.RX) - assert isinstance(layer['queue'][1], qml.RY) - assert isinstance(layer['queue'][2], qml.CNOT) - assert isinstance(layer['queue'][3], qml.CNOT) - assert isinstance(layer['queue'][4], qml.RX) - assert isinstance(layer['queue'][5], qml.RY) - assert isinstance(layer['queue'][6], qml.RZ) - assert isinstance(layer['queue'][7], qml.CNOT) - assert isinstance(layer['queue'][8], qml.CNOT) - assert isinstance(layer['observable'][0], qml.PauliX) - assert isinstance(layer['observable'][1], qml.PauliY) - assert isinstance(layer['observable'][2], qml.PauliZ) - - def test_evaluate_subcircuits(self, tol): - """Test subcircuits evaluate correctly""" - dev = qml.device('default.qubit', wires=2) - - def circuit(a, b, c): - qml.RX(a, wires=0) - qml.RY(b, wires=0) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(c, wires=1) - return qml.expval(qml.PauliX(0)) - - # construct subcircuits - circuit = qml.QNode(circuit, dev) - circuit.construct_metric_tensor([1, 1, 1]) - - a = 0.432 - b = 0.12 - c = -0.432 - - # evaluate subcircuits - circuit.metric_tensor(a, b, c) - - # first parameter subcircuit - res = circuit._metric_tensor_subcircuits[(0,)]['result'] - expected = 0.25 - assert np.allclose(res, expected, atol=tol, rtol=0) - - # second parameter subcircuit - res = circuit._metric_tensor_subcircuits[(1,)]['result'] - expected = np.cos(a)**2/4 - assert np.allclose(res, expected, atol=tol, rtol=0) - - # third parameter subcircuit - res = circuit._metric_tensor_subcircuits[(2,)]['result'] - expected = (3-2*np.cos(a)**2*np.cos(2*b)-np.cos(2*a))/16 - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_evaluate_diag_metric_tensor(self, tol): - """Test that a diagonal metric tensor evaluates correctly""" - dev = qml.device('default.qubit', wires=2) - - @qml.qnode(dev) - def circuit(a, b, c): - qml.RX(a, wires=0) - qml.RY(b, wires=0) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(c, wires=1) - return qml.expval(qml.PauliX(0)) - - a = 0.432 - b = 0.12 - c = -0.432 - - # evaluate metric tensor - g = circuit.metric_tensor(a, b, c) - - # check that the metric tensor is correct - expected = np.array([1, np.cos(a)**2, (3-2*np.cos(a)**2*np.cos(2*b)-np.cos(2*a))/4])/4 - assert np.allclose(g, np.diag(expected), atol=tol, rtol=0) - - @pytest.fixture - def sample_circuit(self): - """Sample variational circuit fixture used in the - next couple of tests""" - dev = qml.device('default.qubit', wires=3) - - def non_parametrized_layer(a, b, c): - qml.RX(a, wires=0) - qml.RX(b, wires=1) - qml.RX(c, wires=1) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - qml.RZ(a, wires=0) - qml.Hadamard(wires=1) - qml.CNOT(wires=[0, 1]) - qml.RZ(b, wires=1) - qml.Hadamard(wires=0) - - a = 0.5 - b = 0.1 - c = 0.5 - - @qml.qnode(dev) - def final(x, y, z, h, g, f): - non_parametrized_layer(a, b, c) - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.RZ(z, wires=2) - non_parametrized_layer(a, b, c) - qml.RY(f, wires=1) - qml.RZ(g, wires=2) - qml.RX(h, wires=1) - return qml.expval(qml.PauliX(0)) - - return dev, final, non_parametrized_layer, a, b, c - - def test_evaluate_block_diag_metric_tensor(self, sample_circuit, tol): - """Test that a block diagonal metric tensor evaluates correctly, - by comparing it to a known analytic result as well as numerical - computation.""" - dev, circuit, non_parametrized_layer, a, b, c = sample_circuit - - params = [-0.282203, 0.145554, 0.331624, -0.163907, 0.57662, 0.081272] - x, y, z, h, g, f = params - - G = circuit.metric_tensor(x, y, z, h, g, f) - - # ============================================ - # Test block diag metric tensor of first layer is correct. - # We do this by comparing against the known analytic result. - # First layer includes the non_parametrized_layer, - # followed by observables corresponding to generators of: - # qml.RX(x, wires=0) - # qml.RY(y, wires=1) - # qml.RZ(z, wires=2) - - G1 = np.zeros([3, 3]) - - # diag elements - G1[0, 0] = np.sin(a)**2/4 - G1[1, 1] = ( - 16 * np.cos(a) ** 2 * np.sin(b) ** 3 * np.cos(b) * np.sin(2 * c) - + np.cos(2 * b) * (2 - 8 * np.cos(a) ** 2 * np.sin(b) ** 2 * np.cos(2 * c)) - + np.cos(2 * (a - b)) - + np.cos(2 * (a + b)) - - 2 * np.cos(2 * a) - + 14 - ) / 64 - G1[2, 2] = (3-np.cos(2*a)-2*np.cos(a)**2*np.cos(2*(b+c)))/16 - - # off diag elements - G1[0, 1] = np.sin(a)**2 * np.sin(b) * np.cos(b+c)/4 - G1[0, 2] = np.sin(a)**2 * np.cos(b+c)/4 - G1[1, 2] = -np.sin(b) * ( - np.cos(2 * (a - b - c)) - + np.cos(2 * (a + b + c)) - + 2 * np.cos(2 * a) - + 2 * np.cos(2 * (b + c)) - - 6 - ) / 32 - - G1[1, 0] = G1[0, 1] - G1[2, 0] = G1[0, 2] - G1[2, 1] = G1[1, 2] - - assert np.allclose(G[:3, :3], G1, atol=tol, rtol=0) - - # ============================================= - # Test block diag metric tensor of second layer is correct. - # We do this by computing the required expectation values - # numerically. - # The second layer includes the non_parametrized_layer, - # RX, RY, RZ gates (x, y, z params), a 2nd non_parametrized_layer, - # followed by the qml.RY(f, wires=2) operation. - # - # Observable is simply generator of: - # qml.RY(f, wires=2) - # - # Note: since this layer only consists of a single parameter, - # only need to compute a single diagonal element. - - @qml.qnode(dev) - def layer2_diag(x, y, z, h, g, f): - non_parametrized_layer(a, b, c) - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.RZ(z, wires=2) - non_parametrized_layer(a, b, c) - qml.RY(f, wires=2) - return qml.var(qml.PauliX(1)) - - G2 = layer2_diag(x, y, z, h, g, f)/4 - assert np.allclose(G[3:4, 3:4], G2, atol=tol, rtol=0) - - # ============================================= - # Test block diag metric tensor of third layer is correct. - # We do this by computing the required expectation values - # numerically using multiple circuits. - # The second layer includes the non_parametrized_layer, - # RX, RY, RZ gates (x, y, z params), and a 2nd non_parametrized_layer. - # - # Observables are the generators of: - # qml.RY(f, wires=1) - # qml.RZ(g, wires=2) - G3 = np.zeros([2, 2]) - - @qml.qnode(dev) - def layer3_diag(x, y, z, h, g, f): - non_parametrized_layer(a, b, c) - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.RZ(z, wires=2) - non_parametrized_layer(a, b, c) - return qml.var(qml.PauliZ(2)), qml.var(qml.PauliY(1)) - - @qml.qnode(dev) - def layer3_off_diag_first_order(x, y, z, h, g, f): - non_parametrized_layer(a, b, c) - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.RZ(z, wires=2) - non_parametrized_layer(a, b, c) - return qml.expval(qml.PauliZ(2)), qml.expval(qml.PauliY(1)) - - @qml.qnode(dev) - def layer3_off_diag_second_order(x, y, z, h, g, f): - non_parametrized_layer(a, b, c) - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.RZ(z, wires=2) - non_parametrized_layer(a, b, c) - return qml.expval(qml.Hermitian(np.kron(Z, Y), wires=[2, 1])) - - # calculate the diagonal terms - varK0, varK1 = layer3_diag(x, y, z, h, g, f) - G3[0, 0] = varK0/4 - G3[1, 1] = varK1/4 - - # calculate the off-diagonal terms - exK0, exK1 = layer3_off_diag_first_order(x, y, z, h, g, f) - exK01 = layer3_off_diag_second_order(x, y, z, h, g, f) - - G3[0, 1] = (exK01 - exK0*exK1)/4 - G3[1, 0] = (exK01 - exK0*exK1)/4 - - assert np.allclose(G[4:6, 4:6], G3, atol=tol, rtol=0) - - # ============================================ - # Finally, double check that the entire metric - # tensor is as computed. - - G_expected = block_diag(G1, G2, G3) - assert np.allclose(G, G_expected, atol=tol, rtol=0) - - def test_evaluate_diag_approx_metric_tensor(self, sample_circuit, tol): - """Test that a metric tensor under the - diagonal approximation evaluates correctly.""" - dev, circuit, non_parametrized_layer, a, b, c = sample_circuit - params = [-0.282203, 0.145554, 0.331624, -0.163907, 0.57662, 0.081272] - x, y, z, h, g, f = params - - G = circuit.metric_tensor(x, y, z, h, g, f, diag_approx=True) - - # ============================================ - # Test block diag metric tensor of first layer is correct. - # We do this by comparing against the known analytic result. - # First layer includes the non_parametrized_layer, - # followed by observables corresponding to generators of: - # qml.RX(x, wires=0) - # qml.RY(y, wires=1) - # qml.RZ(z, wires=2) - - G1 = np.zeros([3, 3]) - - # diag elements - G1[0, 0] = np.sin(a)**2/4 - G1[1, 1] = ( - 16 * np.cos(a) ** 2 * np.sin(b) ** 3 * np.cos(b) * np.sin(2 * c) - + np.cos(2 * b) * (2 - 8 * np.cos(a) ** 2 * np.sin(b) ** 2 * np.cos(2 * c)) - + np.cos(2 * (a - b)) - + np.cos(2 * (a + b)) - - 2 * np.cos(2 * a) - + 14 - ) / 64 - G1[2, 2] = (3-np.cos(2*a)-2*np.cos(a)**2*np.cos(2*(b+c)))/16 - - assert np.allclose(G[:3, :3], G1, atol=tol, rtol=0) - - # ============================================= - # Test metric tensor of second layer is correct. - # We do this by computing the required expectation values - # numerically. - # The second layer includes the non_parametrized_layer, - # RX, RY, RZ gates (x, y, z params), a 2nd non_parametrized_layer, - # followed by the qml.RY(f, wires=2) operation. - # - # Observable is simply generator of: - # qml.RY(f, wires=2) - # - # Note: since this layer only consists of a single parameter, - # only need to compute a single diagonal element. - - @qml.qnode(dev) - def layer2_diag(x, y, z, h, g, f): - non_parametrized_layer(a, b, c) - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.RZ(z, wires=2) - non_parametrized_layer(a, b, c) - qml.RY(f, wires=2) - return qml.var(qml.PauliX(1)) - - G2 = layer2_diag(x, y, z, h, g, f)/4 - assert np.allclose(G[3:4, 3:4], G2, atol=tol, rtol=0) - - # ============================================= - # Test block diag metric tensor of third layer is correct. - # We do this by computing the required expectation values - # numerically using multiple circuits. - # The second layer includes the non_parametrized_layer, - # RX, RY, RZ gates (x, y, z params), and a 2nd non_parametrized_layer. - # - # Observables are the generators of: - # qml.RY(f, wires=1) - # qml.RZ(g, wires=2) - G3 = np.zeros([2, 2]) - - @qml.qnode(dev) - def layer3_diag(x, y, z, h, g, f): - non_parametrized_layer(a, b, c) - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.RZ(z, wires=2) - non_parametrized_layer(a, b, c) - return qml.var(qml.PauliZ(2)), qml.var(qml.PauliY(1)) - - # calculate the diagonal terms - varK0, varK1 = layer3_diag(x, y, z, h, g, f) - G3[0, 0] = varK0/4 - G3[1, 1] = varK1/4 - - assert np.allclose(G[4:6, 4:6], G3, atol=tol, rtol=0) - - # ============================================ - # Finally, double check that the entire metric - # tensor is as computed. - - G_expected = block_diag(G1, G2, G3) - assert np.allclose(G, G_expected, atol=tol, rtol=0) - - -class TestQNodeCacheing: - """Tests for the QNode construction caching""" - - def test_no_caching(self): - """Test that the circuit structure changes on - subsequent evalutions with caching turned off - """ - dev = qml.device("default.qubit", wires=2) - - def circuit(x, c=None): - qml.RX(x, wires=0) - - for i in range(c): - qml.RX(x, wires=i) - - return qml.expval(qml.PauliZ(0)) - - circuit = qml.QNode(circuit, dev, cache=False) - - # first evaluation - circuit(0, c=0) - # check structure - assert len(circuit.queue) == 1 - - # second evaluation - circuit(0, c=1) - # check structure - assert len(circuit.queue) == 2 - - def test_caching(self): - """Test that the circuit structure does not change on - subsequent evalutions with caching turned on - """ - dev = qml.device("default.qubit", wires=2) - - def circuit(x, c=None): - qml.RX(x, wires=0) - - for i in range(c.val): - qml.RX(x, wires=i) - - return qml.expval(qml.PauliZ(0)) - - circuit = qml.QNode(circuit, dev, cache=True) - - # first evaluation - circuit(0, c=0) - # check structure - assert len(circuit.queue) == 1 - - # second evaluation - circuit(0, c=1) - # check structure - assert len(circuit.queue) == 1 - - -class TestDecomposition: - """Test for queue decomposition""" - - def test_no_decomposition(self, operable_mock_device_2_wires): - """Test that decompose queue makes no changes - if there are no operations to be decomposed""" - - queue = [ - qml.Rot(0, 1, 2, wires=0), - qml.CNOT(wires=[0, 1]), - qml.RX(6, wires=0) - ] - - res = decompose_queue(queue, operable_mock_device_2_wires) - assert res == queue - - def test_decompose_queue(self, operable_mock_device_2_wires): - """Test that decompose queue works correctly - when an operation exists that can be decomposed""" - - queue = [ - qml.Rot(0, 1, 2, wires=0), - qml.U3(3, 4, 5, wires=0), - qml.RX(6, wires=0) - ] - - res = decompose_queue(queue, operable_mock_device_2_wires) - - assert len(res) == 5 - - assert res[0].name == "Rot" - assert res[0].parameters == [0, 1, 2] - - assert res[1].name == "Rot" - assert res[1].parameters == [5, 3, -5] - - assert res[2].name == "PhaseShift" - assert res[2].parameters == [5] - - assert res[3].name == "PhaseShift" - assert res[3].parameters == [4] - - assert res[4].name == "RX" - assert res[4].parameters == [6] - - def test_invalid_decompose(self, operable_mock_device_2_wires): - """Test that an error is raised if the device - does not support an operation arising from a - decomposition.""" - - class DummyOp(qml.operation.Operation): - """Dummy operation""" - num_params = 0 - num_wires = 1 - par_domain = "R" - grad_method = "A" - - @staticmethod - def decomposition(wires=None): - ops = [qml.Hadamard(wires=wires)] - return ops - - queue = [ - qml.Rot(0, 1, 2, wires=0), - DummyOp(wires=0), - qml.RX(6, wires=0) - ] - - with pytest.raises(qml.DeviceError, match="DummyOp not supported on device"): - decompose_queue(queue, operable_mock_device_2_wires) diff --git a/tests/test_quantum_gradients.py b/tests/test_quantum_gradients.py index 194adc49f64..72d819f9c5d 100644 --- a/tests/test_quantum_gradients.py +++ b/tests/test_quantum_gradients.py @@ -165,17 +165,17 @@ def circuit(x): return qml.expval(O(wires=0)) q = qml.QNode(circuit, gaussian_dev) - val = q.evaluate(par) + val = q.evaluate(par, {}) grad_F = q.jacobian(par, method='F') - grad_A2 = q.jacobian(par, method='A', force_order2=True) + grad_A2 = q.jacobian(par, method='A', options={"force_order2": True}) if O.ev_order == 1: grad_A = q.jacobian(par, method='A') # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) # analytic method works for every parameter - assert q.grad_method_for_par == {0:'A'} + assert q.par_to_grad_method == {0:'A'} # the different methods agree assert grad_A2 == pytest.approx(grad_F, abs=tol) @@ -192,10 +192,10 @@ def qf(r0, phi0, r1, phi1): q = qml.QNode(qf, gaussian_dev) grad_F = q.jacobian(par, method='F') grad_A = q.jacobian(par, method='A') - grad_A2 = q.jacobian(par, method='A', force_order2=True) + grad_A2 = q.jacobian(par, method='A', options={"force_order2": True}) # analytic method works for every parameter - assert q.grad_method_for_par == {i:'A' for i in range(4)} + assert q.par_to_grad_method == {i:'A' for i in range(4)} # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) assert grad_A2 == pytest.approx(grad_F, abs=tol) @@ -222,10 +222,10 @@ def qf(x, y): q = qml.QNode(qf, gaussian_dev) grad_F = q.jacobian(par, method='F') grad_A = q.jacobian(par, method='A') - grad_A2 = q.jacobian(par, method='A', force_order2=True) + grad_A2 = q.jacobian(par, method='A', options={"force_order2": True}) # analytic method works for every parameter - assert q.grad_method_for_par == {0:'A', 1:'A'} + assert q.par_to_grad_method == {0:'A', 1:'A'} # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) assert grad_A2 == pytest.approx(grad_F, abs=tol) @@ -247,11 +247,11 @@ def qf(x, y): q = qml.QNode(qf, gaussian_dev) grad = q.jacobian(par) grad_F = q.jacobian(par, method='F') - grad_A = q.jacobian(par, method='B') - grad_A2 = q.jacobian(par, method='B', force_order2=True) + grad_A = q.jacobian(par, method="best") + grad_A2 = q.jacobian(par, method="best", options={"force_order2": True}) # par[0] can use the 'A' method, par[1] cannot - assert q.grad_method_for_par == {0:'A', 1:'F'} + assert q.par_to_grad_method == {0:'A', 1:'F'} # the different methods agree assert grad == pytest.approx(grad_F, abs=tol) @@ -269,10 +269,10 @@ def circuit(x, y): q = qml.QNode(circuit, gaussian_dev) grad_F = q.jacobian(par, method='F') grad_A = q.jacobian(par, method='A') - grad_A2 = q.jacobian(par, method='A', force_order2=True) + grad_A2 = q.jacobian(par, method='A', options={"force_order2": True}) # analytic method works for every parameter - assert q.grad_method_for_par == {0:'A', 1:'A'} + assert q.par_to_grad_method == {0:'A', 1:'A'} # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) assert grad_A2 == pytest.approx(grad_F, abs=tol) @@ -310,10 +310,10 @@ def circuit(x): qnode = qml.QNode(circuit, gaussian_dev) grad_F = qnode.jacobian(0.5, method='F') grad_A = qnode.jacobian(0.5, method='A') - grad_A2 = qnode.jacobian(0.5, method='A', force_order2=True) + grad_A2 = qnode.jacobian(0.5, method='A', options={"force_order2": True}) # par[0] can use the 'A' method - assert qnode.grad_method_for_par == {0: 'A'} + assert qnode.par_to_grad_method == {0: 'A'} # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) @@ -458,12 +458,14 @@ def circuit(x, y, z): params = np.array([0.1, -1.6, np.pi / 5]) # manual gradients - grad_fd1 = qnode.jacobian(params, method='F', order=1) - grad_fd2 = qnode.jacobian(params, method='F', order=2) + grad_fd1 = qnode.jacobian(params, method='F', options={"order": 1}) + grad_fd2 = qnode.jacobian(params, method='F', options={"order": 2}) grad_angle = qnode.jacobian(params, method='A') # automatic gradient - grad_fn = autograd.grad(qnode.evaluate) + # Note: the lambda function is required as evaluate now receives a required `kwargs` argument + # that cannot be differentiated by autograd. + grad_fn = autograd.grad(lambda x: qnode.evaluate(x, {})) grad_auto = grad_fn(params)[np.newaxis, :] # so shapes will match # gradients computed with different methods must agree @@ -505,7 +507,7 @@ def d_error(p, grad_method): for d_in, d_out in zip(in_data, out_data): args = (d_in, p) diff = (classifier(*args) - d_out) - ret = ret + 2 * diff * classifier.jacobian(args, which=[1], method=grad_method) + ret = ret + 2 * diff * classifier.jacobian(args, wrt=[1], method=grad_method) return ret y0 = error(param) diff --git a/tests/test_templates_layers.py b/tests/test_templates_layers.py index 236ea5c945f..41f13fbc0d8 100644 --- a/tests/test_templates_layers.py +++ b/tests/test_templates_layers.py @@ -64,15 +64,8 @@ def weights(self): def test_cvneuralnet_uses_correct_weights(self, gaussian_device_4modes, weights): """Tests that the CVNeuralNetLayers template uses the weigh parameters correctly.""" - def circuit(weights): + with qml.utils.OperationRecorder() as rec: CVNeuralNetLayers(*weights, wires=range(4)) - return qml.expval(qml.X(wires=0)) - - qnode = qml.QNode(circuit, gaussian_device_4modes) - - # execution test - qnode(weights) - queue = qnode.queue # Test that gates appear in the right order for each layer: # BS-R-S-BS-R-D-K @@ -88,7 +81,7 @@ def circuit(weights): # loop through expected gates for idx, g in enumerate(gates): # loop through where these gates should be in the queue - for opidx, op in enumerate(queue[gc[idx, 0]:gc[idx, 1]]): + for opidx, op in enumerate(rec.queue[gc[idx, 0]:gc[idx, 1]]): # check that op in queue is correct gate assert isinstance(op, g) @@ -142,25 +135,20 @@ def test_strong_ent_layers_uses_correct_weights(self, n_subsystems): dev = qml.device('default.qubit', wires=num_wires) weights = np.random.randn(n_layers, num_wires, 3) - def circuit(weights): + with qml.utils.OperationRecorder() as rec: StronglyEntanglingLayers(weights, wires=range(num_wires)) - return qml.expval(qml.PauliZ(0)) - - qnode = qml.QNode(circuit, dev) - qnode(weights) - queue = qnode.queue # Test that gates appear in the right order exp_gates = [qml.Rot]*num_wires + [qml.CNOT]*num_wires exp_gates *= n_layers - res_gates = [op for op in queue] + res_gates = rec.queue for op1, op2 in zip(res_gates, exp_gates): assert isinstance(op1, op2) # test the device parameters for l in range(n_layers): - layer_ops = queue[2*l*num_wires:2*(l+1)*num_wires] + layer_ops = rec.queue[2*l*num_wires:2*(l+1)*num_wires] # check each rotation gate parameter for n in range(num_wires): @@ -174,14 +162,10 @@ def test_strong_ent_layers_uses_correct_number_of_imprimitives(self, n_layers, n dev = qml.device('default.qubit', wires=n_subsystems) weights = np.random.randn(n_layers, n_subsystems, 3) - def circuit(weights): + with qml.utils.OperationRecorder() as rec: StronglyEntanglingLayers(weights=weights, wires=range(n_subsystems), imprimitive=imprimitive) - return qml.expval(qml.PauliZ(0)) - qnode = qml.QNode(circuit, dev) - qnode(weights) - queue = qnode.queue - types = [type(q) for q in queue] + types = [type(q) for q in rec.queue] assert types.count(imprimitive) == n_subsystems*n_layers @pytest.mark.parametrize("n_wires, n_layers, ranges", [(2, 2, [2, 1]), @@ -323,14 +307,10 @@ def test_random_layers_nlayers(self, n_layers): dev = qml.device('default.qubit', wires=n_wires) weights = np.random.randn(n_layers, n_rots) - def circuit(weights): + with qml.utils.OperationRecorder() as rec: RandomLayers(weights=weights, wires=range(n_wires)) - return qml.expval(qml.PauliZ(0)) - qnode = qml.QNode(circuit, dev) - qnode(weights) - queue = qnode.queue - types = [type(q) for q in queue] + types = [type(q) for q in rec.queue] assert len(types) - types.count(impr) == n_layers def test_random_layer_ratio_imprimitive(self, ratio): @@ -341,15 +321,11 @@ def test_random_layer_ratio_imprimitive(self, ratio): dev = qml.device('default.qubit', wires=n_wires) weights = np.random.randn(n_rots) - def circuit(weights): + with qml.utils.OperationRecorder() as rec: _random_layer(weights=weights, wires=range(n_wires), ratio_imprim=ratio, imprimitive=CNOT, rotations=[RX, RY, RZ], seed=42) - return qml.expval(qml.PauliZ(0)) - qnode = qml.QNode(circuit, dev) - qnode(weights) - queue = qnode.queue - types = [type(q) for q in queue] + types = [type(q) for q in rec.queue] ratio_impr = types.count(impr) / len(types) assert np.isclose(ratio_impr, ratio, atol=0.05) @@ -359,15 +335,11 @@ def test_random_layer_gate_types(self, n_subsystems, impr, rots): dev = qml.device('default.qubit', wires=n_subsystems) weights = np.random.randn(n_rots) - def circuit(weights): + with qml.utils.OperationRecorder() as rec: _random_layer(weights=weights, wires=range(n_subsystems), ratio_imprim=0.3, imprimitive=impr, rotations=rots, seed=42) - return qml.expval(qml.PauliZ(0)) - qnode = qml.QNode(circuit, dev) - qnode(weights) - queue = qnode.queue - types = [type(q) for q in queue] + types = [type(q) for q in rec.queue] unique = set(types) gates = {impr, *rots} assert unique == gates @@ -378,15 +350,11 @@ def test_random_layer_numgates(self, n_subsystems): dev = qml.device('default.qubit', wires=n_subsystems) weights = np.random.randn(n_rots) - def circuit(weights): + with qml.utils.OperationRecorder() as rec: _random_layer(weights=weights, wires=range(n_subsystems), ratio_imprim=0.3, imprimitive=qml.CNOT, rotations=[RX, RY, RZ], seed=42) - return qml.expval(qml.PauliZ(0)) - qnode = qml.QNode(circuit, dev) - qnode(weights) - queue = qnode.queue - types = [type(q) for q in queue] + types = [type(q) for q in rec.queue] assert len(types) - types.count(qml.CNOT) == n_rots def test_random_layer_randomwires(self, n_subsystems): @@ -395,15 +363,11 @@ def test_random_layer_randomwires(self, n_subsystems): dev = qml.device('default.qubit', wires=n_subsystems) weights = np.random.randn(n_rots) - def circuit(weights): + with qml.utils.OperationRecorder() as rec: _random_layer(weights=weights, wires=range(n_subsystems), ratio_imprim=0.3, imprimitive=qml.CNOT, rotations=[RX, RY, RZ], seed=42) - return qml.expval(qml.PauliZ(0)) - qnode = qml.QNode(circuit, dev) - qnode(weights) - queue = qnode.queue - wires = [q._wires for q in queue] + wires = [q._wires for q in rec.queue] wires_flat = [item for w in wires for item in w] mean_wire = np.mean(wires_flat) assert np.isclose(mean_wire, (n_subsystems - 1) / 2, atol=0.05) @@ -415,15 +379,11 @@ def test_random_layer_weights(self, n_subsystems, tol): dev = qml.device('default.qubit', wires=n_subsystems) weights = np.random.randn(n_rots) - def circuit(weights): + with qml.utils.OperationRecorder() as rec: _random_layer(weights=weights, wires=range(n_subsystems), ratio_imprim=0.3, imprimitive=qml.CNOT, rotations=[RX, RY, RZ], seed=4) - return qml.expval(qml.PauliZ(0)) - qnode = qml.QNode(circuit, dev) - qnode(weights) - queue = qnode.queue - params = [q.parameters for q in queue] + params = [q.parameters for q in rec.queue] params_flat = [item for p in params for item in p] assert np.allclose(weights.flatten(), params_flat, atol=tol) diff --git a/tests/test_templates_subroutines.py b/tests/test_templates_subroutines.py index 2948320586a..d9615a2ca4f 100644 --- a/tests/test_templates_subroutines.py +++ b/tests/test_templates_subroutines.py @@ -61,49 +61,39 @@ def test_clements_beamsplitter_convention(self, tol): phi = [0.234] varphi = [0.42342, 0.1121] - def circuit_rect(varphi): + with qml.utils.OperationRecorder() as rec_rect: Interferometer(theta, phi, varphi, mesh='rectangular', beamsplitter='clements', wires=wires) - return [qml.expval(qml.NumberOperator(w)) for w in wires] - def circuit_tria(varphi): + with qml.utils.OperationRecorder() as rec_tria: Interferometer(theta, phi, varphi, mesh='triangular', beamsplitter='clements', wires=wires) - return [qml.expval(qml.NumberOperator(w)) for w in wires] - for c in [circuit_rect, circuit_tria]: - qnode = qml.QNode(c, dev) - assert np.allclose(qnode(varphi), [0, 0], atol=tol) + for rec in [rec_rect, rec_tria]: - queue = qnode.queue - assert len(queue) == 4 + assert len(rec.queue) == 4 - assert isinstance(qnode.queue[0], qml.Rotation) - assert qnode.queue[0].parameters == phi + assert isinstance(rec.queue[0], qml.Rotation) + assert rec.queue[0].parameters == phi - assert isinstance(qnode.queue[1], qml.Beamsplitter) - assert qnode.queue[1].parameters == [theta[0], 0] + assert isinstance(rec.queue[1], qml.Beamsplitter) + assert rec.queue[1].parameters == [theta[0], 0] - assert isinstance(qnode.queue[2], qml.Rotation) - assert qnode.queue[2].parameters == [varphi[0]] + assert isinstance(rec.queue[2], qml.Rotation) + assert rec.queue[2].parameters == [varphi[0]] - assert isinstance(qnode.queue[3], qml.Rotation) - assert qnode.queue[3].parameters == [varphi[1]] + assert isinstance(rec.queue[3], qml.Rotation) + assert rec.queue[3].parameters == [varphi[1]] def test_one_mode(self, tol): """Test that a one mode interferometer correctly gives a rotation gate""" dev = qml.device('default.gaussian', wires=1) varphi = [0.42342] - def circuit(varphi): + with qml.utils.OperationRecorder() as rec: Interferometer(theta=[], phi=[], varphi=varphi, wires=0) - return qml.expval(qml.NumberOperator(0)) - qnode = qml.QNode(circuit, dev) - assert np.allclose(qnode(varphi), 0, atol=tol) - - queue = qnode.queue - assert len(queue) == 1 - assert isinstance(qnode.queue[0], qml.Rotation) - assert np.allclose(qnode.queue[0].parameters, varphi, atol=tol) + assert len(rec.queue) == 1 + assert isinstance(rec.queue[0], qml.Rotation) + assert np.allclose(rec.queue[0].parameters, varphi, atol=tol) def test_two_mode_rect(self, tol): """Test that a two mode interferometer using the rectangular mesh @@ -115,24 +105,18 @@ def test_two_mode_rect(self, tol): theta = [0.321] phi = [0.234] varphi = [0.42342, 0.1121] - def circuit(varphi): - Interferometer(theta, phi, varphi, wires=wires) - return [qml.expval(qml.NumberOperator(w)) for w in wires] - - qnode = qml.QNode(circuit, dev) - assert np.allclose(qnode(varphi), [0, 0], atol=tol) - queue = qnode.queue - assert len(queue) == 3 + with qml.utils.OperationRecorder() as rec: + Interferometer(theta, phi, varphi, wires=wires) - assert isinstance(qnode.queue[0], qml.Beamsplitter) - assert qnode.queue[0].parameters == theta+phi + isinstance(rec.queue[0], qml.Beamsplitter) + assert rec.queue[0].parameters == theta+phi - assert isinstance(qnode.queue[1], qml.Rotation) - assert qnode.queue[1].parameters == [varphi[0]] + assert isinstance(rec.queue[1], qml.Rotation) + assert rec.queue[1].parameters == [varphi[0]] - assert isinstance(qnode.queue[2], qml.Rotation) - assert qnode.queue[2].parameters == [varphi[1]] + assert isinstance(rec.queue[2], qml.Rotation) + assert rec.queue[2].parameters == [varphi[1]] def test_two_mode_triangular(self, tol): """Test that a two mode interferometer using the triangular mesh @@ -144,25 +128,20 @@ def test_two_mode_triangular(self, tol): theta = [0.321] phi = [0.234] varphi = [0.42342, 0.1121] - def circuit(varphi): - Interferometer(theta, phi, varphi, mesh='triangular', wires=wires) - return [qml.expval(qml.NumberOperator(w)) for w in wires] - qnode = qml.QNode(circuit, dev) - assert np.allclose(qnode(varphi), [0, 0], atol=tol) - - queue = qnode.queue + with qml.utils.OperationRecorder() as rec: + Interferometer(theta, phi, varphi, mesh='triangular', wires=wires) - assert len(queue) == 3 + assert len(rec.queue) == 3 - assert isinstance(qnode.queue[0], qml.Beamsplitter) - assert qnode.queue[0].parameters == theta+phi + assert isinstance(rec.queue[0], qml.Beamsplitter) + assert rec.queue[0].parameters == theta+phi - assert isinstance(qnode.queue[1], qml.Rotation) - assert qnode.queue[1].parameters == [varphi[0]] + assert isinstance(rec.queue[1], qml.Rotation) + assert rec.queue[1].parameters == [varphi[0]] - assert isinstance(qnode.queue[2], qml.Rotation) - assert qnode.queue[2].parameters == [varphi[1]] + assert isinstance(rec.queue[2], qml.Rotation) + assert rec.queue[2].parameters == [varphi[1]] def test_three_mode(self, tol): """Test that a three mode interferometer using either mesh gives the correct gates""" @@ -174,30 +153,24 @@ def test_three_mode(self, tol): phi = [0.234, 0.324, 0.234] varphi = [0.42342, 0.234, 0.1121] - def circuit_rect(varphi): + with qml.utils.OperationRecorder() as rec_rect: Interferometer(theta, phi, varphi, wires=wires) - return [qml.expval(qml.NumberOperator(w)) for w in wires] - def circuit_tria(varphi): + with qml.utils.OperationRecorder() as rec_tria: Interferometer(theta, phi, varphi, wires=wires) - return [qml.expval(qml.NumberOperator(w)) for w in wires] - for c in [circuit_rect, circuit_tria]: + for rec in [rec_rect, rec_tria]: # test both meshes (both give identical results for the 3 mode case). - qnode = qml.QNode(c, dev) - assert np.allclose(qnode(varphi), [0]*N, atol=tol) - - queue = qnode.queue - assert len(queue) == 6 + assert len(rec.queue) == 6 expected_bs_wires = [[0, 1], [1, 2], [0, 1]] - for idx, op in enumerate(qnode.queue[:3]): + for idx, op in enumerate(rec_rect.queue[:3]): assert isinstance(op, qml.Beamsplitter) assert op.parameters == [theta[idx], phi[idx]] assert op.wires == expected_bs_wires[idx] - for idx, op in enumerate(qnode.queue[3:]): + for idx, op in enumerate(rec.queue[3:]): assert isinstance(op, qml.Rotation) assert op.parameters == [varphi[idx]] assert op.wires == [idx] @@ -212,24 +185,19 @@ def test_four_mode_rect(self, tol): phi = [0.234, 0.324, 0.234, 1.453, 1.42341, -0.534] varphi = [0.42342, 0.234, 0.4523, 0.1121] - def circuit_rect(varphi): + with qml.utils.OperationRecorder() as rec: Interferometer(theta, phi, varphi, wires=wires) - return [qml.expval(qml.NumberOperator(w)) for w in wires] - - qnode = qml.QNode(circuit_rect, dev) - assert np.allclose(qnode(varphi), [0]*N, atol=tol) - queue = qnode.queue - assert len(queue) == 10 + assert len(rec.queue) == 10 expected_bs_wires = [[0, 1], [2, 3], [1, 2], [0, 1], [2, 3], [1, 2]] - for idx, op in enumerate(qnode.queue[:6]): + for idx, op in enumerate(rec.queue[:6]): assert isinstance(op, qml.Beamsplitter) assert op.parameters == [theta[idx], phi[idx]] assert op.wires == expected_bs_wires[idx] - for idx, op in enumerate(qnode.queue[6:]): + for idx, op in enumerate(rec.queue[6:]): assert isinstance(op, qml.Rotation) assert op.parameters == [varphi[idx]] assert op.wires == [idx] @@ -244,24 +212,19 @@ def test_four_mode_triangular(self, tol): phi = [0.234, 0.324, 0.234, 1.453, 1.42341, -0.534] varphi = [0.42342, 0.234, 0.4523, 0.1121] - def circuit_tria(varphi): + with qml.utils.OperationRecorder() as rec: Interferometer(theta, phi, varphi, mesh='triangular', wires=wires) - return [qml.expval(qml.NumberOperator(w)) for w in wires] - - qnode = qml.QNode(circuit_tria, dev) - assert np.allclose(qnode(varphi), [0]*N, atol=tol) - queue = qnode.queue - assert len(queue) == 10 + assert len(rec.queue) == 10 expected_bs_wires = [[2, 3], [1, 2], [0, 1], [2, 3], [1, 2], [2, 3]] - for idx, op in enumerate(qnode.queue[:6]): + for idx, op in enumerate(rec.queue[:6]): assert isinstance(op, qml.Beamsplitter) assert op.parameters == [theta[idx], phi[idx]] assert op.wires == expected_bs_wires[idx] - for idx, op in enumerate(qnode.queue[6:]): + for idx, op in enumerate(rec.queue[6:]): assert isinstance(op, qml.Rotation) assert op.parameters == [varphi[idx]] assert op.wires == [idx] diff --git a/tests/test_utils.py b/tests/test_utils.py index b4e3301d137..9d3a798afb3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -347,15 +347,15 @@ class TestOperationRecorder: def test_context_switching(self, monkeypatch): """Test that the current QNode context is properly switched.""" - monkeypatch.setattr(qml.QNode, "_current_context", "Test") + monkeypatch.setattr(qml, "_current_context", "Test") - assert qml.QNode._current_context == "Test" + assert qml._current_context == "Test" with pu.OperationRecorder() as recorder: assert recorder.old_context == "Test" - assert qml.QNode._current_context == recorder.rec + assert qml._current_context == recorder.rec - assert qml.QNode._current_context == "Test" + assert qml._current_context == "Test" def test_circuit_integration(self): """Tests that the OperationRecorder integrates well with the