From 21e30b8a98824c78131c9dcec34a8b076c442c8b Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Fri, 29 Jan 2021 21:58:47 +0800 Subject: [PATCH 01/64] Remove old core --- pennylane/__init__.py | 15 +- pennylane/_device.py | 4 +- pennylane/_qubit_device.py | 5 +- pennylane/_queuing.py | 271 --- pennylane/circuit_drawer/circuit_drawer.py | 5 +- .../circuit_drawer/representation_resolver.py | 13 +- pennylane/circuit_graph.py | 123 +- pennylane/collections/map.py | 8 +- pennylane/devices/tests/conftest.py | 49 - .../tests/test_compare_default_qubit.py | 2 +- pennylane/devices/tests/test_gates.py | 2 +- pennylane/devices/tests/test_measurements.py | 2 +- pennylane/devices/tests/test_properties.py | 7 +- pennylane/devices/tests/test_wires.py | 1 - pennylane/interfaces/__init__.py | 42 +- pennylane/interfaces/autograd.py | 343 ++-- pennylane/interfaces/tf.py | 391 +++-- pennylane/interfaces/torch.py | 505 +++--- pennylane/measure.py | 300 +++- pennylane/operation.py | 190 +-- pennylane/ops/qubit.py | 31 +- pennylane/optimize/qng.py | 2 +- pennylane/qnn/keras.py | 76 +- pennylane/qnn/torch.py | 76 +- pennylane/{tape => }/qnode.py | 237 +-- pennylane/qnodes/__init__.py | 25 - pennylane/qnodes/base.py | 906 ---------- pennylane/qnodes/cv.py | 320 ---- pennylane/qnodes/decorator.py | 334 ---- pennylane/qnodes/device_jacobian.py | 53 - pennylane/qnodes/jacobian.py | 449 ----- pennylane/qnodes/passthru.py | 149 -- pennylane/qnodes/qubit.py | 434 ----- pennylane/qnodes/rev.py | 178 -- pennylane/{tape => }/queuing.py | 76 + pennylane/tape/__init__.py | 261 +-- pennylane/tape/circuit_graph.py | 135 -- pennylane/tape/{tapes => }/cv_param_shift.py | 4 +- pennylane/tape/interfaces/__init__.py | 17 - pennylane/tape/interfaces/autograd.py | 232 --- pennylane/tape/interfaces/tf.py | 192 --- pennylane/tape/interfaces/torch.py | 206 --- pennylane/tape/{tapes => }/jacobian_tape.py | 2 +- pennylane/tape/measure.py | 430 ----- pennylane/tape/operation.py | 182 -- .../tape/{tapes => }/qubit_param_shift.py | 4 +- pennylane/tape/{tapes => }/reversible.py | 2 +- pennylane/tape/{tapes => }/tape.py | 22 +- pennylane/tape/tapes/__init__.py | 22 - pennylane/templates/broadcast.py | 16 +- pennylane/templates/decorator.py | 10 +- pennylane/templates/embeddings/amplitude.py | 119 +- pennylane/templates/embeddings/angle.py | 29 +- pennylane/templates/embeddings/basis.py | 40 +- .../templates/embeddings/displacement.py | 51 +- pennylane/templates/embeddings/iqp.py | 59 +- pennylane/templates/embeddings/qaoa.py | 79 +- pennylane/templates/embeddings/squeezing.py | 52 +- pennylane/templates/layers/basic_entangler.py | 28 +- pennylane/templates/layers/cv_neural_net.py | 75 +- .../layers/particle_conserving_u1.py | 38 +- .../layers/particle_conserving_u2.py | 30 +- pennylane/templates/layers/random.py | 24 +- .../templates/layers/simplified_two_design.py | 51 +- .../templates/layers/strongly_entangling.py | 44 +- .../arbitrary_state_preparation.py | 22 +- .../templates/state_preparations/basis.py | 41 +- .../templates/state_preparations/mottonen.py | 49 +- .../subroutines/arbitrary_unitary.py | 20 +- .../subroutines/double_excitation_unitary.py | 18 +- .../templates/subroutines/interferometer.py | 27 +- .../subroutines/single_excitation_unitary.py | 16 +- pennylane/templates/subroutines/uccsd.py | 20 +- pennylane/templates/utils.py | 22 +- pennylane/{tape => }/transforms/__init__.py | 6 +- pennylane/transforms/classical_jacobian.py | 57 + pennylane/transforms/draw.py | 90 + pennylane/transforms/measurement_grouping.py | 40 + .../{tape => }/transforms/metric_tensor.py | 109 +- pennylane/utils.py | 45 +- pennylane/variable.py | 182 -- pennylane/vqe/vqe.py | 7 - tests/circuit_drawer/test_circuit_drawer.py | 65 +- tests/circuit_drawer/test_grid.py | 3 - .../test_representation_resolver.py | 72 +- tests/circuit_graph/test_circuit_graph.py | 70 +- .../circuit_graph/test_circuit_graph_hash.py | 177 +- .../circuit_graph/test_tape_circuit_graph.py | 203 --- tests/collections/test_collections.py | 3 - tests/collections/test_qnode_collection.py | 3 - tests/conftest.py | 51 - tests/devices/test_caching.py | 26 +- tests/devices/test_default_qubit.py | 9 +- tests/devices/test_default_qubit_jax.py | 24 +- tests/interfaces/test_autograd.py | 1064 ------------ .../interfaces/test_qnode_autograd.py | 0 tests/{tape => }/interfaces/test_qnode_tf.py | 0 .../{tape => }/interfaces/test_qnode_torch.py | 0 .../interfaces/test_tape_autograd.py | 0 tests/{tape => }/interfaces/test_tape_tf.py | 0 .../{tape => }/interfaces/test_tape_torch.py | 0 tests/interfaces/test_tf.py | 1334 --------------- tests/interfaces/test_torch.py | 1271 -------------- tests/math/test_functions.py | 10 +- tests/ops/test_channel_ops.py | 1 - tests/ops/test_qubit_ops.py | 8 +- tests/qnn/conftest.py | 2 +- tests/qnn/test_keras.py | 63 +- tests/qnn/test_qnn_torch.py | 58 +- tests/qnodes/test_qnode_base.py | 1520 ----------------- tests/qnodes/test_qnode_cv.py | 642 ------- tests/qnodes/test_qnode_decorator.py | 303 ---- tests/qnodes/test_qnode_jacobian.py | 241 --- tests/qnodes/test_qnode_metric_tensor.py | 515 ------ tests/qnodes/test_qnode_passthru.py | 230 --- tests/qnodes/test_qnode_qubit.py | 710 -------- tests/qnodes/test_qnode_reversible.py | 698 -------- tests/tape/{tapes => }/test_cv_param_shift.py | 0 tests/tape/{tapes => }/test_jacobian_tape.py | 0 tests/tape/{tapes => }/test_qnode.py | 0 .../{tapes => }/test_qubit_param_shift.py | 0 tests/tape/{tapes => }/test_reversible.py | 0 tests/tape/{tapes => }/test_tape.py | 0 tests/tape/test_tape_measure.py | 789 --------- tests/tape/test_tape_mode.py | 136 -- tests/tape/test_tape_queuing.py | 357 ---- tests/templates/test_broadcast.py | 3 - tests/templates/test_embeddings.py | 12 - tests/templates/test_integration.py | 2 - tests/templates/test_layers.py | 16 - tests/templates/test_state_preparations.py | 16 - tests/templates/test_subroutines.py | 9 - tests/templates/test_templ_utils.py | 26 +- tests/test_measure.py | 761 ++++++++- tests/test_optimize.py | 3 - tests/test_optimize_qng.py | 3 - tests/test_qaoa.py | 2 - tests/test_quantum_gradients.py | 1 - tests/test_qubit_device.py | 8 +- tests/test_queuing.py | 326 ++-- tests/test_variable.py | 103 -- tests/test_vqe.py | 56 +- .../transforms/test_metric_tensor.py | 3 - 143 files changed, 2781 insertions(+), 18078 deletions(-) delete mode 100644 pennylane/_queuing.py rename pennylane/{tape => }/qnode.py (79%) delete mode 100644 pennylane/qnodes/__init__.py delete mode 100644 pennylane/qnodes/base.py delete mode 100644 pennylane/qnodes/cv.py delete mode 100644 pennylane/qnodes/decorator.py delete mode 100644 pennylane/qnodes/device_jacobian.py delete mode 100644 pennylane/qnodes/jacobian.py delete mode 100644 pennylane/qnodes/passthru.py delete mode 100644 pennylane/qnodes/qubit.py delete mode 100644 pennylane/qnodes/rev.py rename pennylane/{tape => }/queuing.py (76%) delete mode 100644 pennylane/tape/circuit_graph.py rename pennylane/tape/{tapes => }/cv_param_shift.py (97%) delete mode 100644 pennylane/tape/interfaces/__init__.py delete mode 100644 pennylane/tape/interfaces/autograd.py delete mode 100644 pennylane/tape/interfaces/tf.py delete mode 100644 pennylane/tape/interfaces/torch.py rename pennylane/tape/{tapes => }/jacobian_tape.py (99%) delete mode 100644 pennylane/tape/measure.py delete mode 100644 pennylane/tape/operation.py rename pennylane/tape/{tapes => }/qubit_param_shift.py (97%) rename pennylane/tape/{tapes => }/reversible.py (97%) rename pennylane/tape/{tapes => }/tape.py (97%) delete mode 100644 pennylane/tape/tapes/__init__.py rename pennylane/{tape => }/transforms/__init__.py (76%) create mode 100644 pennylane/transforms/classical_jacobian.py create mode 100644 pennylane/transforms/draw.py create mode 100644 pennylane/transforms/measurement_grouping.py rename pennylane/{tape => }/transforms/metric_tensor.py (60%) delete mode 100644 pennylane/variable.py delete mode 100644 tests/circuit_graph/test_tape_circuit_graph.py delete mode 100644 tests/interfaces/test_autograd.py rename tests/{tape => }/interfaces/test_qnode_autograd.py (100%) rename tests/{tape => }/interfaces/test_qnode_tf.py (100%) rename tests/{tape => }/interfaces/test_qnode_torch.py (100%) rename tests/{tape => }/interfaces/test_tape_autograd.py (100%) rename tests/{tape => }/interfaces/test_tape_tf.py (100%) rename tests/{tape => }/interfaces/test_tape_torch.py (100%) delete mode 100644 tests/interfaces/test_tf.py delete mode 100644 tests/interfaces/test_torch.py delete mode 100644 tests/qnodes/test_qnode_base.py delete mode 100644 tests/qnodes/test_qnode_cv.py delete mode 100644 tests/qnodes/test_qnode_decorator.py delete mode 100644 tests/qnodes/test_qnode_jacobian.py delete mode 100644 tests/qnodes/test_qnode_metric_tensor.py delete mode 100644 tests/qnodes/test_qnode_passthru.py delete mode 100644 tests/qnodes/test_qnode_qubit.py delete mode 100644 tests/qnodes/test_qnode_reversible.py rename tests/tape/{tapes => }/test_cv_param_shift.py (100%) rename tests/tape/{tapes => }/test_jacobian_tape.py (100%) rename tests/tape/{tapes => }/test_qnode.py (100%) rename tests/tape/{tapes => }/test_qubit_param_shift.py (100%) rename tests/tape/{tapes => }/test_reversible.py (100%) rename tests/tape/{tapes => }/test_tape.py (100%) delete mode 100644 tests/tape/test_tape_measure.py delete mode 100644 tests/tape/test_tape_mode.py delete mode 100644 tests/tape/test_tape_queuing.py delete mode 100644 tests/test_variable.py rename tests/{tape => }/transforms/test_metric_tensor.py (97%) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 0911e56f787..cad288e9826 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -23,9 +23,11 @@ from semantic_version import Version, Spec # QueuingContext needs to be imported before all other pennylane imports -from ._queuing import QueuingContext # pylint: disable=wrong-import-order +from .queuing import QueuingContext # pylint: disable=wrong-import-order import pennylane.operation +import pennylane.math +import pennylane.tape import pennylane.init import pennylane.templates import pennylane.qnn @@ -33,13 +35,14 @@ from pennylane.templates import template, broadcast, layer from pennylane.about import about from pennylane.vqe import Hamiltonian, ExpvalCost, VQECost +from pennylane.transforms import draw, metric_tensor, measurement_grouping from .circuit_graph import CircuitGraph from .configuration import Configuration from ._device import Device, DeviceError from .collections import apply, map, sum, dot, QNodeCollection from ._qubit_device import QubitDevice -from .measure import expval, var, sample, probs +from .measure import expval, var, sample, state, density_matrix, probs from .ops import * from .optimize import * from .qnodes import qnode, QNode, QuantumFunctionError @@ -48,11 +51,6 @@ from .io import * from ._grad import jacobian, grad -import pennylane.math # pylint: disable=wrong-import-order -import pennylane.tape # pylint: disable=wrong-import-order -from .tape import enable_tape, disable_tape, tape_mode_active -from .tape.qnode import draw, metric_tensor - # Look for an existing configuration file default_config = Configuration("config.toml") @@ -230,6 +228,3 @@ def circuit(): def version(): """Returns the PennyLane version number.""" return __version__ - - -enable_tape() diff --git a/pennylane/_device.py b/pennylane/_device.py index 30936169b5b..355ffe3dd69 100644 --- a/pennylane/_device.py +++ b/pennylane/_device.py @@ -348,7 +348,7 @@ def batch_execute(self, circuits): circuits on a backend, for example using parallel and/or asynchronous executions. Args: - circuits (list[.tapes.QuantumTape]): circuits to execute on the device + circuits (list[.tape.QuantumTape]): circuits to execute on the device Returns: list[array[float]]: list of measured value(s) @@ -551,7 +551,7 @@ def check_validity(self, queue, observables): ) for o in observables: - if isinstance(o, qml.tape.MeasurementProcess) and o.obs is not None: + if isinstance(o, qml.measure.MeasurementProcess) and o.obs is not None: o = o.obs if isinstance(o, Tensor): diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index c21d0a14959..0a9d06e49b3 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -191,10 +191,7 @@ def execute(self, circuit, **kwargs): self._circuit_hash = circuit.hash if self._cache: - try: # TODO: Remove try/except when circuit is always QuantumTape - circuit_hash = circuit.graph.hash - except AttributeError as e: - raise ValueError("Caching is only available when using tape mode") from e + circuit_hash = circuit.graph.hash if circuit_hash in self._cache_execute: return self._cache_execute[circuit_hash] diff --git a/pennylane/_queuing.py b/pennylane/_queuing.py deleted file mode 100644 index eedc6407739..00000000000 --- a/pennylane/_queuing.py +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the :class:`QueuingContext` abstract base class. -""" -import abc -from collections import OrderedDict, deque - -import pennylane as qml - - -class QueuingContext(abc.ABC): - """Abstract base class for classes that exposes a queue for objects. - - In PennyLane, the construction of quantum gates is separated from the specific - quantum node (:class:`~.BaseQNode`) that they belong to. However, including - logic for this when creating an instance of :class:`~.Operator` does not align - with the current architecture. Therefore, there is a need to use a high level - object that holds information about the relationship between quantum gates and - a quantum node. - - The :class:`~.QueuingContext` class realizes this by providing access to - the current QNode. Furthermore, it provides the flexibility to have - multiple objects record the creation of quantum gates. - - The ``QueuingContext`` class both acts as the abstract base class for all - classes that expose a queue for Operations (so-called contexts), as well as the - interface to said queues. The active contexts contain maximally one QNode and - an arbitrary number of other contexts like the :class:`~.OperationRecorder`. - """ - - # TODO: update docstring - - _active_contexts = deque() - """The stack of contexts that are currently active.""" - - def __enter__(self): - """Adds this instance to the global list of active contexts. - - Returns: - QueuingContext: This instance - """ - QueuingContext._active_contexts.append(self) - return self - - def __exit__(self, exception_type, exception_value, traceback): - """Remove this instance from the global list of active contexts.""" - assert QueuingContext._active_contexts[-1] is self - QueuingContext._active_contexts.pop() - - @abc.abstractmethod - def _append(self, obj, **kwargs): - """Append an object to this QueuingContext instance. - - Args: - obj: The object to be appended - """ - - @classmethod - def active_context(cls): - """Returns the currently active queuing context.""" - if cls._active_contexts: - return cls._active_contexts[-1] - - return None - - @classmethod - def append(cls, obj, **kwargs): - """Append an object to the queue(s). - - Args: - obj: the object to be appended - """ - # TODO: this method should append only to `cls.active_context`, *not* - # all active contexts. However this will require a refactor in - # the template decorator and the operation recorder. - for context in cls._active_contexts: - context._append(obj, **kwargs) # pylint: disable=protected-access - - @abc.abstractmethod - def _remove(self, obj): - """Remove an object from this QueuingContext instance. - - Args: - obj: the object to be removed - """ - - @classmethod - def remove(cls, obj): - """Remove an object from the queue(s) if it is in the queue(s). - - Args: - obj: the object to be removed - """ - # TODO: this method should remove only from `cls.active_context`, *not* - # all active contexts. However this will require a refactor in - # the template decorator and the operation recorder. - for context in cls._active_contexts: - # We use the duck-typing approach to assume that the underlying remove - # behaves like `list.remove(obj)` or `del dict[key]` and throws a - # ValueError or KeyError if the operator is not present - try: - context._remove(obj) # pylint: disable=protected-access - except (ValueError, KeyError): - pass - - @classmethod - def update_info(cls, obj, **kwargs): - """Updates information of an object in the active queue.""" - cls.active_context()._update_info(obj, **kwargs) # pylint: disable=protected-access - - def _update_info(self, obj, **kwargs): - """Updates information of an object in the queue instance.""" - raise NotImplementedError - - @classmethod - def get_info(cls, obj): - """Returns information of an object in the queue.""" - return cls.active_context().get_info(obj) - - -class Queue(QueuingContext): - """Lightweight class that maintains a basic queue of operations and pre/post-processing steps - for representing quantum circuits.""" - - def __init__(self): - self.queue = [] - - def _append(self, obj, **kwargs): - self.queue.append(obj) - - def _remove(self, obj): - self.queue.remove(obj) - - -class AnnotatedQueue(QueuingContext): - """Lightweight class that maintains a basic queue of operations, in addition - to annotations.""" - - def __init__(self): - self._queue = OrderedDict() - - def _append(self, obj, **kwargs): - self._queue[obj] = kwargs - - def _remove(self, obj): - del self._queue[obj] - - def _update_info(self, obj, **kwargs): - """Updates the annotated information of an object in the queue. - - Args: - obj: the object to update - kwargs: Keyword arguments and values to add to the annotation. - If a particular keyword already exists in the annotation, - the value is updated. - """ - if obj not in self._queue: - raise ValueError(f"Object {obj} not in the queue.") - - self._queue[obj].update(kwargs) - - def get_info(self, obj): - """Returns the annotated information of an object in the queue. - - Args: - obj: the object to query - - Returns: - dict: the annotated information - """ - if obj not in self._queue: - raise ValueError(f"Object {obj} not in the queue.") - - return self._queue[obj] - - @property - def queue(self): - """Returns a list of objects in the annotated queue""" - return list(self._queue.keys()) - - -class OperationRecorder(Queue): - """A template and quantum function inspector, - allowing easy introspection of operators that have been - applied without requiring a QNode. - - **Example**: - - The OperationRecorder is a context manager. Executing templates - or quantum functions stores resulting applied operators in the - recorder, which can then be printed. - - >>> weights = qml.init.strong_ent_layers_normal(n_layers=1, n_wires=2) - >>> - >>> with qml.utils.OperationRecorder() as rec: - >>> qml.templates.layers.StronglyEntanglingLayers(*weights, wires=[0, 1]) - >>> - >>> print(rec) - Operations - ========== - Rot(-0.10832656163640327, 0.14429091013664083, -0.010835826725765343, wires=[0]) - Rot(-0.11254523669444501, 0.0947222564914006, -0.09139600968423377, wires=[1]) - CNOT(wires=[0, 1]) - CNOT(wires=[1, 0]) - - Alternatively, the :attr:`~.OperationRecorder.queue` attribute can be used - to directly access the applied :class:`~.Operation` and :class:`~.Observable` - objects. - - Attributes: - queue (List[Operator]): list of operators applied within - the OperatorRecorder context, includes operations and observables - operations (List[Operation]): list of operations applied within - the OperatorRecorder context - observables (List[Observable]): list of observables applied within - the OperatorRecorder context - """ - - def __init__(self): - super().__init__() - self.operations = None - self.observables = None - - def __exit__(self, exception_type, exception_value, traceback): - super().__exit__(exception_type, exception_value, traceback) - - # Remove duplicates that might have arisen from measurements. - # Code taken from https://stackoverflow.com/questions/480214 - seen = set() - self.queue = [x for x in self.queue if not (x in seen or seen.add(x))] - self.operations = list( - filter( - lambda op: not ( - isinstance(op, qml.operation.Observable) and not op.return_type is None - ), - self.queue, - ) - ) - self.observables = list( - filter( - lambda op: isinstance(op, qml.operation.Observable) and not op.return_type is None, - self.queue, - ) - ) - - def __str__(self): - output = "" - output += "Operations\n" - output += "==========\n" - for op in self.operations: - output += repr(op) + "\n" - - output += "\n" - output += "Observables\n" - output += "==========\n" - for op in self.observables: - output += repr(op) + "\n" - - return output diff --git a/pennylane/circuit_drawer/circuit_drawer.py b/pennylane/circuit_drawer/circuit_drawer.py index 214046bf5ec..1d6168d1e3d 100644 --- a/pennylane/circuit_drawer/circuit_drawer.py +++ b/pennylane/circuit_drawer/circuit_drawer.py @@ -47,7 +47,6 @@ class CircuitDrawer: raw_observable_grid (list[list[qml.operation.Observable]]): The CircuitGraph's observables wires (Wires): all wires on the device for which the circuit is drawn charset (pennylane.circuit_drawer.CharSet, optional): The CharSet that shall be used for drawing. - show_variable_names (bool, optional): Show variable names instead of variable values. show_all_wires (bool): If True, all wires, including empty wires, are printed. """ @@ -57,7 +56,6 @@ def __init__( raw_observable_grid, wires, charset=UnicodeCharSet, - show_variable_names=False, show_all_wires=False, ): self.operation_grid = Grid(raw_operation_grid) @@ -65,14 +63,13 @@ def __init__( self.wires = wires self.active_wires = self.extract_active_wires(raw_operation_grid, raw_observable_grid) self.charset = charset - self.show_variable_names = show_variable_names if show_all_wires: # if the provided wires include empty wires, make sure they are included # as active wires self.active_wires = wires.all_wires([wires, self.active_wires]) - self.representation_resolver = RepresentationResolver(charset, show_variable_names) + self.representation_resolver = RepresentationResolver(charset) self.operation_representation_grid = Grid() self.observable_representation_grid = Grid() self.operation_decoration_indices = [] diff --git a/pennylane/circuit_drawer/representation_resolver.py b/pennylane/circuit_drawer/representation_resolver.py index 02262e8223f..2c8e6a5a318 100644 --- a/pennylane/circuit_drawer/representation_resolver.py +++ b/pennylane/circuit_drawer/representation_resolver.py @@ -26,12 +26,10 @@ class RepresentationResolver: Args: charset (CharSet, optional): The CharSet to be used for representation resolution. - show_variable_names (bool, optional): Show variable names instead of variable values. """ - def __init__(self, charset=UnicodeCharSet, show_variable_names=False): + def __init__(self, charset=UnicodeCharSet): self.charset = charset - self.show_variable_names = show_variable_names self.matrix_cache = [] self.unitary_matrix_cache = [] self.hermitian_matrix_cache = [] @@ -108,14 +106,11 @@ def single_parameter_representation(self, par): """Resolve the representation of an Operator's parameter. Args: - par (Union[~.variable.Variable, int, float, str]): The parameter to be rendered + par (Union[int, float, str]): The parameter to be rendered Returns: str: String representation of the parameter """ - if isinstance(par, qml.variable.Variable): - return par.render(self.show_variable_names) - if isinstance(par, str): return par @@ -316,7 +311,7 @@ def operator_representation(self, op, wire): Returns: str: String representation of the Operator """ - if isinstance(op, qml.tape.MeasurementProcess) and op.obs is not None: + if isinstance(op, qml.measure.MeasurementProcess) and op.obs is not None: op = op.obs if isinstance(op, qml.operation.Tensor): @@ -453,7 +448,7 @@ def element_representation(self, element, wire): if isinstance(element, str): return element if ( - isinstance(element, (qml.operation.Observable, qml.tape.MeasurementProcess)) + isinstance(element, (qml.operation.Observable, qml.measure.MeasurementProcess)) and element.return_type is not None ): return self.output_representation(element, wire) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 1bf7dcd3d0b..ecad78fb9d8 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -21,10 +21,8 @@ import networkx as nx import pennylane as qml -from pennylane.operation import Sample from .circuit_drawer import CHARSETS, CircuitDrawer -from .variable import Variable OPENQASM_GATES = { @@ -138,16 +136,26 @@ class CircuitGraph: between arbitrary quantum channels and measurements, not just unitary gates. Args: - ops (Iterable[Operator]): quantum operators constituting the circuit, in temporal order - variable_deps (dict[int, list[ParameterDependency]]): Free parameters of the quantum circuit. - The dictionary key is the parameter index. - wires (Wires): the addressable wires on the device + ops (Iterable[.Operator]): quantum operators constituting the circuit, in temporal order + obs (Iterable[.MeasurementProcess]): terminal measurements, in temporal order + wires (.Wires): The addressable wire register of the device that will be executing this graph + par_info (dict[int, dict[str, .Operation or int]]): Parameter information. Keys are + parameter indices (in the order they appear on the tape), and values are a + dictionary containing the corresponding operation and operation parameter index. + trainable_params (set[int]): A set containing the indices of parameters that support + differentiability. The indices provided match the order of appearence in the + quantum circuit. """ # pylint: disable=too-many-public-methods - def __init__(self, ops, variable_deps, wires): - self.variable_deps = variable_deps + def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): + self._operations = ops + self._observables = obs + self.par_info = par_info + self.trainable_params = trainable_params + + self._depth = None self._grid = {} """dict[int, list[Operator]]: dictionary representing the quantum circuit as a grid. @@ -165,7 +173,7 @@ def __init__(self, ops, variable_deps, wires): # State measurements contain no wires by default, but wires are # required for the circuit drawer, so we recreate the state # measurement with all wires - op = qml.tape.MeasurementProcess(qml.operation.State, wires=wires) + op = qml.measure.MeasurementProcess(qml.operation.State, wires=wires) op.queue_idx = k for w in op.wires: @@ -193,6 +201,10 @@ def __init__(self, ops, variable_deps, wires): # Create an edge between this and the previous operator self._graph.add_edge(wire[i - 1], wire[i]) + # For computing depth; want only a graph with the operations, not + # including the observables + self._operation_graph = None + def print_contents(self): """Prints the contents of the quantum circuit.""" @@ -224,16 +236,9 @@ def serialize(self): serialization_string += op.name for param in op.data: - if isinstance(param, Variable): - serialization_string += delimiter - serialization_string += variable_delimiter - serialization_string += str(param.idx) - serialization_string += delimiter - - else: - serialization_string += delimiter - serialization_string += str(param) - serialization_string += delimiter + serialization_string += delimiter + serialization_string += str(param) + serialization_string += delimiter serialization_string += str(op.wires.tolist()) @@ -357,7 +362,10 @@ def observables_in_order(self): nodes = [node for node in self._graph.nodes if _is_observable(node)] return sorted(nodes, key=_by_idx) - observables = observables_in_order + @property + def observables(self): + """Observables in the circuit.""" + return self._observables @property def operations_in_order(self): @@ -374,7 +382,10 @@ def operations_in_order(self): nodes = [node for node in self._graph.nodes if not _is_observable(node)] return sorted(nodes, key=_by_idx) - operations = operations_in_order + @property + def operations(self): + """Operations in the circuit.""" + return self._operations @property def graph(self): @@ -501,18 +512,15 @@ def parametrized_layers(self): Returns: list[Layer]: layers of the circuit """ - # FIXME maybe layering should be greedier, for example [a0 b0 c1 d1] should layer as [a0 c1], [b0, d1] and not [a0], [b0 c1], [d1] - # keep track of the current layer + # FIXME maybe layering should be greedier, for example [a0 b0 c1 d1] should layer as [a0 + # c1], [b0, d1] and not [a0], [b0 c1], [d1] keep track of the current layer current = Layer([], []) layers = [current] - # sort vars by first occurrence of the var in the ops queue - variable_ops_sorted = sorted(self.variable_deps.items(), key=lambda x: x[1][0].op.queue_idx) + for idx, info in self.par_info.items(): + if idx in self.trainable_params: + op = info["op"] - # iterate over all parameters - for param_idx, gate_param_tuple in variable_ops_sorted: - # iterate over ops depending on that param - for op, _ in gate_param_tuple: # get all predecessor ops of the op sub = self.ancestors((op,)) @@ -525,7 +533,7 @@ def parametrized_layers(self): # store the parameters and ops indices for the layer current.ops.append(op) - current.param_inds.append(param_idx) + current.param_inds.append(idx) return layers @@ -566,7 +574,7 @@ def greedy_layers(self, wire_order=None, show_all_wires=False): operations[wire] = list( filter( lambda op: not ( - isinstance(op, (qml.operation.Observable, qml.tape.MeasurementProcess)) + isinstance(op, (qml.operation.Observable, qml.measure.MeasurementProcess)) and op.return_type is not None ), operations[wire], @@ -596,7 +604,7 @@ def greedy_layers(self, wire_order=None, show_all_wires=False): observables[wire] = list( filter( lambda op: isinstance( - op, (qml.operation.Observable, qml.tape.MeasurementProcess) + op, (qml.operation.Observable, qml.measure.MeasurementProcess) ) and op.return_type is not None, self._grid[wire], @@ -654,15 +662,16 @@ def update_node(self, old, new): raise ValueError("The new Operator must act on the same wires as the old one.") new.queue_idx = old.queue_idx nx.relabel_nodes(self._graph, {old: new}, copy=False) # change the graph in place + self._operations = self.operations_in_order + self._observables = self.observables_in_order def draw( - self, charset="unicode", show_variable_names=False, wire_order=None, show_all_wires=False + self, charset="unicode", wire_order=None, show_all_wires=False ): """Draw the CircuitGraph as a circuit diagram. Args: charset (str, optional): The charset that should be used. Currently, "unicode" and "ascii" are supported. - show_variable_names (bool, optional): Show variable names instead of variable values. wire_order (Wires or None): the order (from top to bottom) to print the wires of the circuit show_all_wires (bool): If True, all wires, including empty wires, are printed. @@ -689,37 +698,35 @@ def draw( obs, wires=wire_order or self.wires, charset=CHARSETS[charset], - show_variable_names=show_variable_names, show_all_wires=show_all_wires, ) return drawer.draw() - @property - def diagonalizing_gates(self): - """Returns the gates that diagonalize the measured wires such that they - are in the eigenbasis of the circuit observables. + def get_depth(self): + """Depth of the quantum circuit (longest path in the DAG).""" + # If there are no operations in the circuit, the depth is 0 + if not self.operations: + self._depth = 0 - Returns: - List[~.Operation]: the operations that diagonalize the observables - """ - rotation_gates = [] + # If there are operations but depth is uncomputed, compute the truncated graph + # with only the operations, and return the longest path + 1 (since the path is + # expressed in terms of edges, and we want it in terms of nodes). + if self._depth is None and self.operations: + if self._operation_graph is None: + self._operation_graph = self.graph.subgraph(self.operations) + self._depth = nx.dag_longest_path_length(self._operation_graph) + 1 - for observable in self.observables_in_order: - rotation_gates.extend(observable.diagonalizing_gates()) + return self._depth - return rotation_gates + def has_path(self, a, b): + """Checks if a path exists between the two given nodes. - @property - def is_sampled(self): - """Returns ``True`` if the circuit graph contains observables - which are sampled.""" - # TODO: remove when tape is core - return any(obs.return_type == Sample for obs in self.observables_in_order) + Args: + a (Operator): initial node + b (Operator): final node - @property - def all_sampled(self): - """Returns ``True`` if the circuit graph contains observables - which are sampled.""" - # TODO: remove when tape is core - return all(obs.return_type == Sample for obs in self.observables_in_order) + Returns: + bool: returns ``True`` if a path exists + """ + return nx.has_path(self._graph, a, b) diff --git a/pennylane/collections/map.py b/pennylane/collections/map.py index f4e1436c54f..96f9f2819fb 100644 --- a/pennylane/collections/map.py +++ b/pennylane/collections/map.py @@ -23,6 +23,9 @@ from .qnode_collection import QNodeCollection +MEASURE_MAP = {"expval": qml.expval, "var": qml.var, "sample": qml.sample} + + def map( template, observables, @@ -100,9 +103,6 @@ def my_template(params, wires, **kwargs): >>> qnodes(params) array([-0.06154835 0.99280864]) """ - # TODO: return measure_map to a module variable when tape-mode is default - measure_map = {"expval": qml.expval, "var": qml.var, "sample": qml.sample} - if not callable(template): raise ValueError("Could not create QNodes. The template is not a callable function.") @@ -134,7 +134,7 @@ def circuit( params, _obs=obs, _m=m, _wires=wires, **circuit_kwargs ): # pylint: disable=dangerous-default-value, function-redefined template(params, wires=_wires, **circuit_kwargs) - return measure_map[_m](_obs) + return MEASURE_MAP[_m](_obs) qnode = qml.QNode(circuit, dev, interface=interface, diff_method=diff_method, **kwargs) qnodes.append(qnode) diff --git a/pennylane/devices/tests/conftest.py b/pennylane/devices/tests/conftest.py index 3c33e3a7f4b..b171f19447b 100755 --- a/pennylane/devices/tests/conftest.py +++ b/pennylane/devices/tests/conftest.py @@ -262,52 +262,3 @@ def pytest_runtest_makereport(item, call): tr.outcome = "skipped" return tr - - -@pytest.fixture(params=[False, True]) -def tape_mode(request, mocker): - """Tests using this fixture will be run twice, once in tape mode and once without.""" - - if request.param: - # Several attributes and methods on the old QNode have a new location on the new QNode/tape. - # Here, we dynamically mock so that the tests do not have to be modified to support both - # tape and non-tape mode. Once tape mode is default, we can make the equivalent - # changes directly in the tests. - mocker.patch( - "pennylane.tape.QNode.ops", - property(lambda self: self.qtape.operations + self.qtape.observables), - create=True, - ) - mocker.patch( - "pennylane.tape.QNode.h", property(lambda self: self.diff_options["h"]), create=True - ) - mocker.patch( - "pennylane.tape.QNode.order", - property(lambda self: self.diff_options["order"]), - create=True, - ) - mocker.patch( - "pennylane.tape.QNode.circuit", property(lambda self: self.qtape.graph), create=True - ) - - def patched_jacobian(self, args, **kwargs): # pylint: disable=unused-argument - method = kwargs.get("method", "best") - - if method == "A": - method = "analytic" - elif method == "F": - method = "numeric" - - kwargs["method"] = method - dev = kwargs["options"]["device"] - - return self.qtape.jacobian(dev, **kwargs) - - mocker.patch("pennylane.tape.QNode.jacobian", patched_jacobian, create=True) - - qml.enable_tape() - - yield - - if request.param: - qml.disable_tape() diff --git a/pennylane/devices/tests/test_compare_default_qubit.py b/pennylane/devices/tests/test_compare_default_qubit.py index 525d5a3c718..33d014a3c28 100755 --- a/pennylane/devices/tests/test_compare_default_qubit.py +++ b/pennylane/devices/tests/test_compare_default_qubit.py @@ -20,7 +20,7 @@ from pennylane import numpy as np # Import from PennyLane to mirror the standard approach in demos from pennylane.templates.layers import RandomLayers -pytestmark = [pytest.mark.skip_unsupported, pytest.mark.usefixtures("tape_mode")] +pytestmark = pytest.mark.skip_unsupported @flaky(max_runs=10) diff --git a/pennylane/devices/tests/test_gates.py b/pennylane/devices/tests/test_gates.py index 4b59530385b..8128dcd3947 100755 --- a/pennylane/devices/tests/test_gates.py +++ b/pennylane/devices/tests/test_gates.py @@ -28,7 +28,7 @@ from scipy.linalg import block_diag from flaky import flaky -pytestmark = [pytest.mark.skip_unsupported, pytest.mark.usefixtures("tape_mode")] +pytestmark = pytest.mark.skip_unsupported np.random.seed(42) diff --git a/pennylane/devices/tests/test_measurements.py b/pennylane/devices/tests/test_measurements.py index 5d7e3e1f3ad..00bcb23ff08 100755 --- a/pennylane/devices/tests/test_measurements.py +++ b/pennylane/devices/tests/test_measurements.py @@ -20,7 +20,7 @@ import pennylane as qml -pytestmark = [pytest.mark.skip_unsupported, pytest.mark.usefixtures("tape_mode")] +pytestmark = pytest.mark.skip_unsupported # ========================================================== # Some useful global variables diff --git a/pennylane/devices/tests/test_properties.py b/pennylane/devices/tests/test_properties.py index 85746e368bc..af70af48155 100755 --- a/pennylane/devices/tests/test_properties.py +++ b/pennylane/devices/tests/test_properties.py @@ -18,7 +18,6 @@ import pennylane as qml from pennylane._device import DeviceError -pytestmark = pytest.mark.usefixtures("tape_mode") try: import tensorflow as tf @@ -135,11 +134,7 @@ def test_passthru_interface_is_correct(self, device_kwargs): assert interface in ["tf", "autograd", "jax"] # for new interface, add test case qfunc = qfunc_with_scalar_input(cap["model"]) - qnode = ( - qml.QNode(qfunc, dev) - if qml.tape_mode_active() - else qml.qnodes.passthru.PassthruQNode(qfunc, dev) - ) + qnode = qml.QNode(qfunc, dev) qnode.interface = interface # assert that we can do a simple gradient computation in the passthru interface diff --git a/pennylane/devices/tests/test_wires.py b/pennylane/devices/tests/test_wires.py index e251b60eac8..debc5a76e5f 100644 --- a/pennylane/devices/tests/test_wires.py +++ b/pennylane/devices/tests/test_wires.py @@ -17,7 +17,6 @@ import pytest from pennylane import numpy as np -pytestmark = pytest.mark.usefixtures("tape_mode") # ===== Factories for circuits using arbitrary wire labels and numbers diff --git a/pennylane/interfaces/__init__.py b/pennylane/interfaces/__init__.py index 11811f00c80..ae9ca6d41d1 100644 --- a/pennylane/interfaces/__init__.py +++ b/pennylane/interfaces/__init__.py @@ -1,25 +1,17 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This subpackage defines functions convert QNodes to interface with different machine -learning libraries. - -.. currentmodule:: pennylane.interfaces -.. autosummary:: - :toctree: api - - tf - torch - autograd -""" +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This subpackage defines functions convert quantum tapes to interface with different machine +learning libraries. +""" diff --git a/pennylane/interfaces/autograd.py b/pennylane/interfaces/autograd.py index 20197ec87f6..a2d1fa3206c 100644 --- a/pennylane/interfaces/autograd.py +++ b/pennylane/interfaces/autograd.py @@ -1,111 +1,232 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Differentiable quantum nodes with Autograd interface. -""" -import autograd.extend -import autograd.builtins - -from pennylane.utils import unflatten - - -def to_autograd(qnode): - """Function that accepts a :class:`~.QNode`, and returns an Autograd-compatible QNode. - - Args: - qnode (~pennylane.qnode.QNode): a PennyLane QNode - - Returns: - AutogradQNode: an Autograd-compatible QNode - """ - qnode_interface = getattr(qnode, "interface", None) - - if qnode_interface == "autograd": - return qnode - - if qnode_interface is not None: - qnode = qnode._qnode # pylint: disable=protected-access - - class AutogradQNode(qnode.__class__): - """QNode that works with Autograd.""" - - @property - def interface(self): - """str, None: automatic differentiation interface used by the node, if any""" - return "autograd" - - # mark the evaluate method as an Autograd primitive - evaluate = autograd.extend.primitive(qnode.__class__.evaluate) - - def set_trainable(self, args): - """Given input arguments to the AutogradQNode, determine which arguments - are trainable and which aren't. - - This method calls the underlying :meth:`set_trainable_args` method of the QNode. - """ - trainable_args = set() - - for idx, arg in enumerate(args): - if getattr(arg, "requires_grad", True): - trainable_args.add(idx) - - self.set_trainable_args(trainable_args) - - def __call__(self, *args, **kwargs): - # prevents autograd boxed arguments from going through to evaluate - self.set_trainable(args) - args = autograd.builtins.tuple(args) # pylint: disable=no-member - return self.evaluate(args, kwargs) - - @staticmethod - def QNode_vjp(ans, self, args, kwargs): - """Returns the vector-Jacobian product operator for the QNode. - - Takes the same arguments as :meth:`evaluate`, plus ``ans``. - - Returns: - function[array[float], array[float]]: vector-Jacobian product operator - """ - # pylint: disable=unused-argument - def gradient_product(g): - """Vector-Jacobian product operator. - - Args: - g (array[float]): 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 input arguments in ``args`` - """ - # Jacobian matrix of the circuit - self.set_trainable(args) - jac = self.jacobian(args, kwargs) - - if not g.shape: - vjp = g * jac # numpy treats 0d arrays as scalars, hence @ cannot be used - else: - vjp = g @ jac - - # Restore the nested structure of the input args. - vjp = unflatten(vjp.flat, args) - return vjp - - return gradient_product - - # define the vector-Jacobian product function for AutogradQNode.evaluate - autograd.extend.defvjp(AutogradQNode.evaluate, AutogradQNode.QNode_vjp, argnums=[1]) - qnode._qnode = qnode # pylint: disable=protected-access - qnode.__class__ = AutogradQNode - return qnode +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains the mixin interface class for creating differentiable quantum tapes with +Autograd. +""" +# pylint: disable=protected-access +import autograd.extend +import autograd.builtins +from autograd.numpy.numpy_boxes import ArrayBox + +from pennylane import numpy as np +from pennylane.queuing import AnnotatedQueue + + +class AutogradInterface(AnnotatedQueue): + """Mixin class for applying an autograd interface to a :class:`~.JacobianTape`. + + Autograd-compatible quantum tape classes can be created via subclassing: + + .. code-block:: python + + class MyAutogradQuantumTape(AutogradInterface, JacobianTape): + + Alternatively, the autograd interface can be dynamically applied to existing + quantum tapes via the :meth:`~.apply` class method. This modifies the + tape **in place**. + + Once created, the autograd interface can be used to perform quantum-classical + differentiable programming. + + .. note:: + + If using a device that supports native autograd computation and backpropagation, such as + :class:`~.DefaultQubitAutograd`, the Autograd interface **does not need to be applied**. It + is only applied to tapes executed on non-Autograd compatible devices. + + **Example** + + Once an autograd quantum tape has been created, it can be differentiated using autograd: + + .. code-block:: python + + tape = AutogradInterface.apply(JacobianTape()) + + with tape: + qml.Rot(0, 0, 0, wires=0) + expval(qml.PauliX(0)) + + def cost_fn(x, y, z, device): + tape.set_parameters([x, y ** 2, y * np.sin(z)], trainable_only=False) + return tape.execute(device=device) + + >>> x = np.array(0.1, requires_grad=False) + >>> y = np.array(0.2, requires_grad=True) + >>> z = np.array(0.3, requires_grad=True) + >>> dev = qml.device("default.qubit", wires=2) + >>> cost_fn(x, y, z, device=dev) + [0.03991951] + >>> jac_fn = qml.jacobian(cost_fn) + >>> jac_fn(x, y, z, device=dev) + [[ 0.39828408, -0.00045133]] + """ + + # pylint: disable=attribute-defined-outside-init + dtype = np.float64 + + @property + def interface(self): # pylint: disable=missing-function-docstring + return "autograd" + + def _update_trainable_params(self): + """Set the trainable parameters. + + Unlike in :class:`~.JacobianTape`, we also set the private attribute + ``self._all_parameter_values``. + """ + params = self.get_parameters(trainable_only=False, return_arraybox=True) + trainable_params = set() + + for idx, p in enumerate(params): + if getattr(p, "requires_grad", False) or isinstance(p, ArrayBox): + trainable_params.add(idx) + + self.trainable_params = trainable_params + self._all_parameter_values = params + + def get_parameters(self, trainable_only=True, return_arraybox=False): + """Return the parameters incident on the tape operations. + + The returned parameters are provided in order of appearance + on the tape. By default, the returned parameters are wrapped in + an ``autograd.builtins.list`` container. + + Args: + trainable_only (bool): if True, returns only trainable parameters + return_arraybox (bool): if True, the returned parameters are not + wrapped in an ``autograd.builtins.list`` container + Returns: + autograd.builtins.list or list: the corresponding parameter values + + **Example** + + .. code-block:: python + + with JacobianTape() as tape: + qml.RX(0.432, wires=0) + qml.RY(0.543, wires=0) + qml.CNOT(wires=[0, 'a']) + qml.RX(0.133, wires='a') + expval(qml.PauliZ(wires=[0])) + + By default, all parameters are trainable and will be returned: + + >>> tape.get_parameters() + [0.432, 0.543, 0.133] + + Setting the trainable parameter indices will result in only the specified + parameters being returned: + + >>> tape.trainable_params = {1} # set the second parameter as free + >>> tape.get_parameters() + [0.543] + + The ``trainable_only`` argument can be set to ``False`` to instead return + all parameters: + + >>> tape.get_parameters(trainable_only=False) + [0.432, 0.543, 0.133] + """ + params = [] + iterator = self.trainable_params if trainable_only else self._par_info + + for p_idx in iterator: + op = self._par_info[p_idx]["op"] + op_idx = self._par_info[p_idx]["p_idx"] + params.append(op.data[op_idx]) + + return params if return_arraybox else autograd.builtins.list(params) + + @autograd.extend.primitive + def _execute(self, params, device): + # unwrap all NumPy scalar arrays to Python literals + params = [p.item() if p.shape == tuple() else p for p in params] + params = autograd.builtins.tuple(params) + + # unwrap constant parameters + self._all_params_unwrapped = [ + p.numpy() if isinstance(p, np.tensor) else p for p in self._all_parameter_values + ] + + # evaluate the tape + self.set_parameters(self._all_params_unwrapped, trainable_only=False) + res = self.execute_device(params, device=device) + self.set_parameters(self._all_parameter_values, trainable_only=False) + + if self.is_sampled: + return res + + if res.dtype == np.dtype("object"): + return np.hstack(res) + + requires_grad = False + + if self.trainable_params: + requires_grad = True + + return np.array(res, requires_grad=requires_grad) + + @staticmethod + def vjp(ans, self, params, device): # pylint: disable=unused-argument + """Returns the vector-Jacobian product operator for the quantum tape. + The returned function takes the arguments as :meth:`~.JacobianTape.execute`. + + Args: + ans (array): the result of the tape execution + self (.AutogradQuantumTape): the tape instance + params (list[Any]): the quantum tape operation parameters + device (.Device): a PennyLane device that can execute quantum + operations and return measurement statistics + + Returns: + function: this function accepts the backpropagation + gradient output vector, and computes the vector-Jacobian product + """ + + def gradient_product(g): + # In autograd, the forward pass is always performed prior to the backwards + # pass, so we do not need to re-unwrap the parameters. + self.set_parameters(self._all_params_unwrapped, trainable_only=False) + jac = self.jacobian(device, params=params, **self.jacobian_options) + self.set_parameters(self._all_parameter_values, trainable_only=False) + vjp = g.flatten() @ jac + return vjp + + return gradient_product + + @classmethod + def apply(cls, tape): + """Apply the autograd interface to an existing tape in-place. + + Args: + tape (.JacobianTape): a quantum tape to apply the Autograd interface to + + **Example** + + >>> with JacobianTape() as tape: + ... qml.RX(0.5, wires=0) + ... expval(qml.PauliZ(0)) + >>> AutogradInterface.apply(tape) + >>> tape + , params=1> + """ + tape_class = getattr(tape, "__bare__", tape.__class__) + tape.__bare__ = tape_class + tape.__class__ = type("AutogradQuantumTape", (cls, tape_class), {}) + tape._update_trainable_params() + return tape + + +autograd.extend.defvjp(AutogradInterface._execute, AutogradInterface.vjp, argnums=[1]) diff --git a/pennylane/interfaces/tf.py b/pennylane/interfaces/tf.py index efcc18a0acc..457f23296ab 100644 --- a/pennylane/interfaces/tf.py +++ b/pennylane/interfaces/tf.py @@ -1,199 +1,192 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the :func:`to_tf` function to convert Numpy-interfacing quantum nodes to TensorFlow -compatible quantum nodes. -""" -# pylint: disable=redefined-outer-name -import numbers -from collections.abc import Iterable -from functools import partial - -import numpy as np -import tensorflow as tf - -from tensorflow import Variable # pylint: disable=unused-import,ungrouped-imports - -try: - from tensorflow.python.eager.tape import should_record_backprop -except ImportError: - from tensorflow.python.eager.tape import should_record as should_record_backprop - - -def unflatten_tf(flat, model): - """Restores an arbitrary nested structure to a flattened TF tensor. - - See also :func:`~.unflatten`. - - Args: - flat (tf.Tensor): 1D tensor of items - model (array, Iterable, Number): model nested structure - - Returns: - Union[tf.Tensor, list], array: first elements of flat arranged into the nested - structure of model, unused elements of flat - - Raises: - TypeError: if ``model`` contains an object of unsupported type - """ - if isinstance(model, (numbers.Number, str)): - return flat[0], flat[1:] - - if isinstance(model, (tf.Tensor, tf.Variable)): - idx = tf.size(model) - res = tf.reshape(flat[:idx], model.shape) - return res, flat[idx:] - - if isinstance(model, Iterable): - res = [] - for x in model: - val, flat = unflatten_tf(flat, x) - res.append(val) - return res, flat - - raise TypeError("Unsupported type in the model: {}".format(type(model))) - - -def to_tf(qnode, dtype=None): - """Function that accepts a :class:`~.QNode`, and returns a TensorFlow eager-execution-compatible QNode. - - Args: - qnode (~pennylane.qnode.QNode): a PennyLane QNode - dtype (tf.DType): target output type of QNode; uses the TensorFlow equivalent of the - QNode output type if ``dtype`` is not specified - - Returns: - function: the QNode as a TensorFlow function - """ - qnode_interface = getattr(qnode, "interface", None) - - if qnode_interface == "tf" and dtype == qnode._dtype: # pylint: disable=protected-access - return qnode - - if qnode_interface is not None: - qnode = qnode._qnode # pylint: disable=protected-access - - class TFQNode(partial): - """TensorFlow QNode""" - - # pylint: disable=too-few-public-methods - - # Here, we are making use of functools.partial to dynamically add - # methods and attributes to the custom gradient function defined below. - # This allows us to provide more useful __str__ and __repr__ methods - # for the decorated function (so it would still look like a QNode to end-users), - # as well as making QNode attributes and methods available. - - @property - def interface(self): - """String representing the QNode interface""" - return "tf" - - def __str__(self): - """String representation""" - detail = "" - return detail.format( - qnode.device.short_name, qnode.func.__name__, qnode.num_wires, self.interface - ) - - def __repr__(self): - """REPL representation""" - return self.__str__() - - # Bind QNode methods - print_applied = qnode.print_applied - jacobian = qnode.jacobian - metric_tensor = qnode.metric_tensor - draw = qnode.draw - func = qnode.func - set_trainable_args = qnode.set_trainable_args - get_trainable_args = qnode.get_trainable_args - _qnode = qnode - _dtype = dtype - - # Bind QNode attributes. Note that attributes must be - # bound as properties; by making use of closure, we ensure - # that updates to the wrapped QNode attributes are reflected - # by the wrapper class. - num_variables = property(lambda self: qnode.num_variables) - arg_vars = property(lambda self: qnode.arg_vars) - par_to_grad_method = property(lambda self: qnode.par_to_grad_method) - - @TFQNode - @tf.custom_gradient - def _TFQNode(*input_, **input_kwargs): - # Determine which input tensors/Variables are being recorded for backpropagation. - # The function should_record_backprop, documented here: - # https://github.com/tensorflow/tensorflow/tree/master/tensorflow/python/eager/tape.py#L163 - # accepts lists of *tensors* (not Variables), returning True if all are being watched by one or more - # existing gradient tape, False if not. - trainable_args = { - idx - for idx, i in enumerate(input_) - if isinstance(i, (Variable, tf.Tensor)) - and should_record_backprop([tf.convert_to_tensor(i)]) - } - - # detach all input Tensors, convert to NumPy array - args = [i.numpy() if isinstance(i, (Variable, tf.Tensor)) else i for i in input_] - kwargs = { - k: v.numpy() if isinstance(v, (Variable, tf.Tensor)) else v - for k, v in input_kwargs.items() - } - - # if NumPy array is scalar, convert to a Python float - args = [i.tolist() if (isinstance(i, np.ndarray) and not i.shape) else i for i in args] - kwargs = { - k: v.tolist() if (isinstance(v, np.ndarray) and not v.shape) else v - for k, v in kwargs.items() - } - - # evaluate the QNode - qnode.set_trainable_args(trainable_args) - res = qnode(*args, **kwargs) - - if not isinstance(res, np.ndarray): - # scalar result, cast to NumPy scalar - res = np.array(res) - - def grad(grad_output, **tfkwargs): - """Returns the vector-Jacobian product""" - # evaluate the Jacobian matrix of the QNode - variables = tfkwargs.get("variables", None) - qnode.set_trainable_args(trainable_args) - jacobian = qnode.jacobian(args, kwargs) - jacobian = tf.constant(jacobian, dtype=dtype) - - # Reshape gradient output array as a 2D row-vector. - grad_output_row = tf.transpose(tf.reshape(grad_output, [-1, 1])) - - # Calculate the vector-Jacobian matrix product, and flatten the output. - grad_input = tf.matmul(grad_output_row, jacobian) - grad_input = tf.reshape(grad_input, [-1]) - - grad_input_unflattened = unflatten_tf(grad_input, input_)[0] - - for idx in set(range(len(args))) - trainable_args: - # If a particular input argument is non-differentiable, - # replace the corresponding position in the gradient with None. - grad_input_unflattened[idx] = None - - if variables is not None: - return grad_input_unflattened, variables - - return grad_input_unflattened - - return tf.convert_to_tensor(res, dtype=dtype), grad - - return _TFQNode +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains the mixin interface class for creating differentiable quantum tapes with +TensorFlow. +""" +# pylint: disable=protected-access, attribute-defined-outside-init +import numpy as np +import tensorflow as tf + +try: + from tensorflow.python.eager.tape import should_record_backprop +except ImportError: + from tensorflow.python.eager.tape import should_record as should_record_backprop + + +from pennylane.queuing import AnnotatedQueue + + +class TFInterface(AnnotatedQueue): + """Mixin class for applying an TensorFlow interface to a :class:`~.JacobianTape`. + + TensorFlow-compatible quantum tape classes can be created via subclassing: + + .. code-block:: python + + class MyTFQuantumTape(TFInterface, JacobianTape): + + Alternatively, the TensorFlow interface can be dynamically applied to existing + quantum tapes via the :meth:`~.apply` class method. This modifies the + tape **in place**. + + Once created, the TensorFlow interface can be used to perform quantum-classical + differentiable programming. + + .. note:: + + If using a device that supports native TensorFlow computation and backpropagation, such as + :class:`~.DefaultQubitTF`, the TensorFlow interface **does not need to be applied**. It is + only applied to tapes executed on non-TensorFlow compatible devices. + + **Example** + + Once a TensorFlow quantum tape has been created, it can be differentiated using the gradient tape: + + .. code-block:: python + + dev = qml.device("default.qubit", wires=1) + p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float64) + + with tf.GradientTape() as tape: + with TFInterface.apply(JacobianTape()) as qtape: + qml.Rot(p[0], p[1] ** 2 + p[0] * p[2], p[1] * tf.sin(p[2]), wires=0) + expval(qml.PauliX(0)) + + result = qtape.execute(dev) + + >>> print(result) + tf.Tensor([0.06982072], shape=(1,), dtype=float64) + >>> grad = tape.gradient(result, p) + >>> print(grad) + tf.Tensor([0.29874274 0.39710271 0.09958091], shape=(3,), dtype=float64) + + The TensorFlow interface defaults to ``tf.float64`` output. This can be modified by + providing the ``dtype`` argument when applying the interface: + + >>> p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float32) + >>> with tf.GradientTape() as tape: + ... TFInterface.apply(qtape, dtype=tf.float32) # reusing the previous qtape + ... result = qtape.execute(dev) + >>> print(result) + tf.Tensor([0.06982072], shape=(1,), dtype=float32) + >>> grad = tape.gradient(result, p) + >>> print(grad) + tf.Tensor([0.2895088 0.38464668 0.09645163], shape=(3,), dtype=float32) + """ + + dtype = tf.float64 + + @property + def interface(self): # pylint: disable=missing-function-docstring + return "tf" + + def _update_trainable_params(self): + params = self.get_parameters(trainable_only=False) + + trainable_params = set() + + for idx, p in enumerate(params): + # Determine which input tensors/Variables are being recorded for backpropagation. + # The function should_record_backprop, documented here: + # https://github.com/tensorflow/tensorflow/tree/master/tensorflow/python/eager/tape.py#L167 + # accepts lists of *Tensors* (not Variables), returning True if all are being watched by one or more + # existing gradient tapes, False if not. + + if isinstance(p, (tf.Variable, tf.Tensor)) and should_record_backprop( + # we need to convert any Variable objects to Tensors here, otherwise + # should_record_backprop will raise an error + [tf.convert_to_tensor(p)] + ): + trainable_params.add(idx) + + self.trainable_params = trainable_params + + @staticmethod + def convert_to_numpy(tensors): + """Converts any TensorFlow tensors in a sequence to NumPy arrays. + + Args: + tensors (Sequence[Any, tf.Variable, tf.Tensor]): input sequence + + Returns: + list[Any, array]: list with all tensors converted to NumPy arrays + """ + return [i.numpy() if isinstance(i, (tf.Variable, tf.Tensor)) else i for i in tensors] + + @tf.custom_gradient + def _execute(self, params, **input_kwargs): + # unwrap free parameters + args = self.convert_to_numpy(params) + + # unwrap constant parameters + all_params = self.get_parameters(trainable_only=False) + all_params_unwrapped = self.convert_to_numpy(all_params) + + self.set_parameters(all_params_unwrapped, trainable_only=False) + res = self.execute_device(args, input_kwargs["device"]) + self.set_parameters(all_params, trainable_only=False) + + def grad(grad_output, **tfkwargs): + variables = tfkwargs.get("variables", None) + + self.set_parameters(all_params_unwrapped, trainable_only=False) + jacobian = self.jacobian(input_kwargs["device"], params=args, **self.jacobian_options) + self.set_parameters(all_params, trainable_only=False) + + jacobian = tf.constant(jacobian, dtype=self.dtype) + + # Reshape gradient output array as a 2D row-vector. + grad_output_row = tf.reshape(grad_output, [1, -1]) + + # Calculate the vector-Jacobian matrix product, and unstack the output. + grad_input = tf.matmul(grad_output_row, jacobian) + grad_input = tf.unstack(tf.reshape(grad_input, [-1])) + + if variables is not None: + return grad_input, variables + + return grad_input + + if self.is_sampled: + return res, grad + + if res.dtype == np.dtype("object"): + res = np.hstack(res) + + return tf.convert_to_tensor(res, dtype=self.dtype), grad + + @classmethod + def apply(cls, tape, dtype=tf.float64): + """Apply the TensorFlow interface to an existing tape in-place. + + Args: + tape (.JacobianTape): a quantum tape to apply the TF interface to + dtype (tf.dtype): the dtype that the returned quantum tape should + output + + **Example** + + >>> with JacobianTape() as tape: + ... qml.RX(0.5, wires=0) + ... expval(qml.PauliZ(0)) + >>> TFInterface.apply(tape) + >>> tape + , params=1> + """ + tape_class = getattr(tape, "__bare__", tape.__class__) + tape.__bare__ = tape_class + tape.__class__ = type("TFQuantumTape", (cls, tape_class), {"dtype": dtype}) + tape._update_trainable_params() + return tape diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py index e62b26d520b..92479879239 100644 --- a/pennylane/interfaces/torch.py +++ b/pennylane/interfaces/torch.py @@ -1,299 +1,206 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -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 -from collections.abc import Iterable -import inspect -from functools import partial -import numbers - -import numpy as np -import torch -from torch.autograd.function import once_differentiable - - -def unflatten_torch(flat, model): - """Restores an arbitrary nested structure to a flattened Torch tensor. - - Args: - flat (torch.Tensor): 1D tensor of items - model (array, Iterable, Number): model nested structure - - Returns: - Tuple[list[torch.Tensor], torch.Tensor]: tuple containing elements of ``flat`` arranged - into the nested structure of model, as well as the unused elements of ``flat``. - - Raises: - TypeError: if ``model`` contains an object of unsupported type - """ - if isinstance(model, (numbers.Number, str)): - return flat[0], flat[1:] - - if isinstance(model, (torch.Tensor, np.ndarray)): - try: - idx = model.numel() - except AttributeError: - idx = model.size - - res = flat[:idx].reshape(model.shape) - return res, flat[idx:] - - if isinstance(model, Iterable): - res = [] - for x in model: - val, flat = unflatten_torch(flat, x) - res.append(val) - return res, flat - - raise TypeError("Unsupported type in the model: {}".format(type(model))) - - -def _get_default_args(func): - """Get the default arguments of a function. - - Args: - func (function): a valid Python function - - Returns: - dict: dictionary containing the argument name and tuple - (positional idx, default value) - """ - signature = inspect.signature(func) - return { - k: (idx, v.default) - for idx, (k, v) in enumerate(signature.parameters.items()) - if v.default is not inspect.Parameter.empty - } - - -def args_to_numpy(args): - """Converts all Torch tensors in a list to NumPy arrays - - Args: - args (list): list containing QNode arguments, including Torch tensors - - Returns: - list: returns the same list, with all Torch tensors converted to NumPy arrays - """ - res = [] - - for i in args: - if isinstance(i, torch.Tensor): - if i.is_cuda: # pragma: no cover - res.append(i.cpu().detach().numpy()) - else: - res.append(i.detach().numpy()) - else: - res.append(i) - - # if NumPy array is scalar, convert to a Python float - res = [i.tolist() if (isinstance(i, np.ndarray) and not i.shape) else i for i in res] - - return res - - -def kwargs_to_numpy(kwargs): - """Converts all Torch tensors in a dictionary to NumPy arrays - - Args: - args (dict): dictionary containing QNode keyword arguments, including Torch tensors - - Returns: - dict: returns the same dictionary, with all Torch tensors converted to NumPy arrays - """ - res = {} - - for key, val in kwargs.items(): - if isinstance(val, torch.Tensor): - if val.is_cuda: # pragma: no cover - res[key] = val.cpu().detach().numpy() - else: - res[key] = val.detach().numpy() - else: - res[key] = val - - # if NumPy array is scalar, convert to a Python float - res = { - k: v.tolist() if (isinstance(v, np.ndarray) and not v.shape) else v for k, v in res.items() - } - - return res - - -def to_torch(qnode): - """Function that accepts a :class:`~.QNode`, and returns a PyTorch-compatible QNode. - - Args: - qnode (~pennylane.qnode.QNode): a PennyLane QNode - - Returns: - torch.autograd.Function: the QNode as a PyTorch autograd function - """ - qnode_interface = getattr(qnode, "interface", None) - - if qnode_interface == "torch": - return qnode - - if qnode_interface is not None: - qnode = qnode._qnode # pylint: disable=protected-access - - class _TorchQNode(torch.autograd.Function): - """The TorchQNode""" - - @staticmethod - def set_trainable(args): - """Given input arguments to the TorchQNode, determine which arguments - are trainable and which aren't. - - Currently, all arguments are assumed to be nondifferentiable by default, - unless the ``torch.tensor`` attribute ``requires_grad`` is set to True. - - This method calls the underlying :meth:`set_trainable_args` method of the QNode. - """ - trainable_args = set() - - for idx, arg in enumerate(args): - if getattr(arg, "requires_grad", False): - trainable_args.add(idx) - - qnode.set_trainable_args(trainable_args) - - @staticmethod - def forward(ctx, input_kwargs, *input_): - """Implements the forward pass QNode evaluation""" - # detach all input tensors, convert to NumPy array - ctx.args = args_to_numpy(input_) - ctx.kwargs = kwargs_to_numpy(input_kwargs) - ctx.save_for_backward(*input_) - - # Determine which QNode input tensors require gradients, - # and thus communicate to the QNode which ones must - # be wrapped as PennyLane variables. - _TorchQNode.set_trainable(input_) - - # evaluate the QNode - res = qnode(*ctx.args, **ctx.kwargs) - - if not isinstance(res, np.ndarray): - # scalar result, cast to NumPy scalar - res = np.array(res) - - # if any input tensor uses the GPU, the output should as well - for i in input_: - if isinstance(i, torch.Tensor): - if i.is_cuda: # pragma: no cover - cuda_device = i.get_device() - return torch.as_tensor(torch.from_numpy(res), device=cuda_device) - - return torch.from_numpy(res) - - @staticmethod - @once_differentiable - def backward(ctx, grad_output): # pragma: no cover - """Implements the backwards pass QNode vector-Jacobian product""" - # NOTE: This method is definitely tested by the `test_torch.py` test suite, - # however does not show up in the coverage. This is likely due to - # subtleties in the torch.autograd.FunctionMeta metaclass, specifically - # the way in which the backward class is created on the fly - - # evaluate the Jacobian matrix of the QNode - jacobian = qnode.jacobian(ctx.args, ctx.kwargs) - jacobian = torch.as_tensor(jacobian, dtype=grad_output.dtype).to(grad_output) - - vjp = torch.transpose(grad_output.view(-1, 1), 0, 1) @ jacobian - vjp = vjp.flatten() - - # restore the nested structure of the input args - grad_input_list = unflatten_torch(vjp, ctx.saved_tensors)[0] - grad_input = [] - - # match the type and device of the input tensors - for i, j in zip(grad_input_list, ctx.saved_tensors): - res = torch.as_tensor(i, dtype=j.dtype) - if j.is_cuda: # pragma: no cover - cuda_device = j.get_device() - res = torch.as_tensor(res, device=cuda_device) - grad_input.append(res) - - return (None,) + tuple(grad_input) - - class TorchQNode(partial): - """Torch QNode""" - - # pylint: disable=too-few-public-methods - - # Here, we are making use of functools.partial to dynamically add - # methods and attributes to the custom gradient method defined below. - # This allows us to provide more useful __str__ and __repr__ methods - # for the decorated function (so it would still look like a QNode to end-users), - # as well as making QNode attributes and methods available. - - @property - def interface(self): - """String representing the QNode interface""" - return "torch" - - def __str__(self): - """String representation""" - detail = "" - return detail.format( - qnode.device.short_name, qnode.func.__name__, qnode.num_wires, self.interface - ) - - def __repr__(self): - """REPL representation""" - return self.__str__() - - # Bind QNode methods - print_applied = qnode.print_applied - jacobian = qnode.jacobian - metric_tensor = qnode.metric_tensor - draw = qnode.draw - func = qnode.func - set_trainable_args = qnode.set_trainable_args - get_trainable_args = qnode.get_trainable_args - _qnode = qnode - - # Bind QNode attributes. Note that attributes must be - # bound as properties; by making use of closure, we ensure - # that updates to the wrapped QNode attributes are reflected - # by the wrapper class. - arg_vars = property(lambda self: qnode.arg_vars) - num_variables = property(lambda self: qnode.num_variables) - par_to_grad_method = property(lambda self: qnode.par_to_grad_method) - - @TorchQNode - def custom_apply(*args, **kwargs): - """Custom apply wrapper, to allow passing kwargs to the TorchQNode""" - - # get default kwargs that weren't passed - keyword_sig = _get_default_args(qnode.func) - keyword_defaults = {k: v[1] for k, v in keyword_sig.items()} - # keyword_positions = {v[0]: k for k, v in keyword_sig.items()} - - # create a keyword_values dict, that contains defaults - # and any user-passed kwargs - keyword_values = {} - keyword_values.update(keyword_defaults) - keyword_values.update(kwargs) - - # sort keyword values into a list of args, using their position - # [keyword_values[k] for k in sorted(keyword_positions, key=keyword_positions.get)] - - return _TorchQNode.apply(keyword_values, *args) - - return custom_apply +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains the mixin interface class for creating differentiable quantum tapes with +PyTorch. +""" +# pylint: disable=protected-access, attribute-defined-outside-init, arguments-differ, no-member, import-self +import numpy as np +import semantic_version +import torch + +from pennylane import QuantumFunctionError +from pennylane.interfaces.torch import args_to_numpy + +from pennylane.queuing import AnnotatedQueue + +COMPLEX_SUPPORT = semantic_version.match(">=1.6.0", torch.__version__) + + +class _TorchInterface(torch.autograd.Function): + @staticmethod + def forward(ctx, input_kwargs, *input_): + """Implements the forward pass QNode evaluation""" + # detach all input tensors, convert to NumPy array + ctx.args = args_to_numpy(input_) + ctx.kwargs = input_kwargs + ctx.save_for_backward(*input_) + + tape = ctx.kwargs["tape"] + device = ctx.kwargs["device"] + + # unwrap constant parameters + ctx.all_params = tape.get_parameters(trainable_only=False) + ctx.all_params_unwrapped = args_to_numpy(ctx.all_params) + + # evaluate the tape + tape.set_parameters(ctx.all_params_unwrapped, trainable_only=False) + res = tape.execute_device(ctx.args, device) + tape.set_parameters(ctx.all_params, trainable_only=False) + + if hasattr(res, "numpy"): + res = res.numpy() + + # if any input tensor uses the GPU, the output should as well + for i in input_: + if isinstance(i, torch.Tensor): + if i.is_cuda: # pragma: no cover + cuda_device = i.get_device() + return torch.as_tensor( + torch.from_numpy(res), device=cuda_device, dtype=tape.dtype + ) + + if tape.is_sampled and not tape.all_sampled: + return tuple([torch.as_tensor(t, dtype=tape.dtype) for t in res]) + + if res.dtype == np.dtype("object"): + res = np.hstack(res) + + return torch.as_tensor(torch.from_numpy(res), dtype=tape.dtype) + + @staticmethod + def backward(ctx, grad_output): # pragma: no cover + """Implements the backwards pass QNode vector-Jacobian product""" + tape = ctx.kwargs["tape"] + device = ctx.kwargs["device"] + + tape.set_parameters(ctx.all_params_unwrapped, trainable_only=False) + jacobian = tape.jacobian(device, params=ctx.args, **tape.jacobian_options) + tape.set_parameters(ctx.all_params, trainable_only=False) + + jacobian = torch.as_tensor(jacobian, dtype=grad_output.dtype).to(grad_output) + + vjp = grad_output.view(1, -1) @ jacobian + grad_input_list = torch.unbind(vjp.flatten()) + grad_input = [] + + # match the type and device of the input tensors + for i, j in zip(grad_input_list, ctx.saved_tensors): + res = torch.as_tensor(i, dtype=tape.dtype) + if j.is_cuda: # pragma: no cover + cuda_device = j.get_device() + res = torch.as_tensor(res, device=cuda_device) + grad_input.append(res) + + return (None,) + tuple(grad_input) + + +class TorchInterface(AnnotatedQueue): + """Mixin class for applying an Torch interface to a :class:`~.JacobianTape`. + + Torch-compatible quantum tape classes can be created via subclassing: + + .. code-block:: python + + class MyTorchQuantumTape(TorchInterface, JacobianTape): + + Alternatively, the Torch interface can be dynamically applied to existing + quantum tapes via the :meth:`~.apply` class method. This modifies the + tape **in place**. + + Once created, the Torch interface can be used to perform quantum-classical + differentiable programming. + + **Example** + + Once a Torch quantum tape has been created, it can be evaluated and differentiated: + + .. code-block:: python + + dev = qml.device("default.qubit", wires=1) + p = torch.tensor([0.1, 0.2, 0.3], requires_grad=True) + + with TorchInterface.apply(JacobianTape()) as qtape: + qml.Rot(p[0], p[1] ** 2 + p[0] * p[2], p[1] * torch.sin(p[2]), wires=0) + expval(qml.PauliX(0)) + + result = qtape.execute(dev) + + >>> print(result) + tensor([0.0698], dtype=torch.float64, grad_fn=<_TorchInterfaceBackward>) + >>> result.backward() + >>> print(p.grad) + tensor([0.2987, 0.3971, 0.0988]) + + The Torch interface defaults to ``torch.float64`` output. This can be modified by + providing the ``dtype`` argument when applying the interface: + + >>> p = torch.tensor([0.1, 0.2, 0.3], requires_grad=True) + >>> with TorchInterface.apply(JacobianTape()) as qtape: + ... qml.Rot(p[0], p[1] ** 2 + p[0] * p[2], p[1] * torch.sin(p[2]), wires=0) + ... expval(qml.PauliX(0)) + >>> result = qtape.execute(dev) + >>> print(result) + tensor([0.0698], grad_fn=<_TorchInterfaceBackward>) + >>> print(result.dtype) + torch.float32 + >>> result.backward() + >>> print(p.grad) + tensor([0.2987, 0.3971, 0.0988]) + >>> print(p.grad.dtype) + torch.float32 + """ + + dtype = torch.float64 + + @property + def interface(self): # pylint: disable=missing-function-docstring + return "torch" + + def _update_trainable_params(self): + params = self.get_parameters(trainable_only=False) + + trainable_params = set() + + for idx, p in enumerate(params): + if getattr(p, "requires_grad", False): + trainable_params.add(idx) + + self.trainable_params = trainable_params + return params + + def _execute(self, params, **kwargs): + kwargs["tape"] = self + res = _TorchInterface.apply(kwargs, *params) + return res + + @classmethod + def apply(cls, tape, dtype=torch.float64): + """Apply the Torch interface to an existing tape in-place. + + Args: + tape (.JacobianTape): a quantum tape to apply the Torch interface to + dtype (torch.dtype): the dtype that the returned quantum tape should + output + + **Example** + + >>> with JacobianTape() as tape: + ... qml.RX(0.5, wires=0) + ... expval(qml.PauliZ(0)) + >>> TorchInterface.apply(tape) + >>> tape + , params=1> + """ + if (dtype is torch.complex64 or dtype is torch.complex128) and not COMPLEX_SUPPORT: + raise QuantumFunctionError( + "Version 1.6.0 or above of PyTorch must be installed for complex support, " + "which is required for quantum functions that return the state." + ) + + tape_class = getattr(tape, "__bare__", tape.__class__) + tape.__bare__ = tape_class + tape.__class__ = type("TorchQuantumTape", (cls, tape_class), {"dtype": dtype}) + tape._update_trainable_params() + return tape diff --git a/pennylane/measure.py b/pennylane/measure.py index 77f32fb773a..11c349fba9d 100644 --- a/pennylane/measure.py +++ b/pennylane/measure.py @@ -15,11 +15,191 @@ """ This module contains the functions for computing different types of measurement outcomes from quantum observables - expectation values, variances of expectations, -and measurement samples. +and measurement samples using AnnotatedQueues. """ +import copy + +import numpy as np + import pennylane as qml -from pennylane.operation import Observable, Sample, Variance, Expectation, Probability, Tensor +from pennylane.operation import Expectation, Observable, Probability, Sample, State, Variance from pennylane.qnodes import QuantumFunctionError +from pennylane.wires import Wires + + +class MeasurementProcess: + """Represents a measurement process occurring at the end of a + quantum variational circuit. + + Args: + return_type (.ObservableReturnTypes): The type of measurement process. + This includes ``Expectation``, ``Variance``, ``Sample``, ``State``, or ``Probability``. + obs (.Observable): The observable that is to be measured as part of the + measurement process. Not all measurement processes require observables (for + example ``Probability``); this argument is optional. + wires (.Wires): The wires the measurement process applies to. + This can only be specified if an observable was not provided. + eigvals (array): A flat array representing the eigenvalues of the measurement. + This can only be specified if an observable was not provided. + """ + + # pylint: disable=too-few-public-methods + + def __init__(self, return_type, obs=None, wires=None, eigvals=None): + self.return_type = return_type + self.obs = obs + + if wires is not None and obs is not None: + raise ValueError("Cannot set the wires if an observable is provided.") + + self._wires = wires or Wires([]) + self._eigvals = None + + if eigvals is not None: + if obs is not None: + raise ValueError("Cannot set the eigenvalues if an observable is provided.") + + self._eigvals = np.array(eigvals) + + # TODO: remove the following lines once devices + # have been refactored to accept and understand recieving + # measurement processes rather than specific observables. + + # The following lines are only applicable for measurement processes + # that do no have corresponding observables (e.g., Probability). We use + # them to 'trick' the device into thinking it has recieved an observable. + + # Below, we imitate an identity observable, so that the + # device undertakes no action upon recieving this observable. + self.name = "Identity" + self.data = [] + + # Queue the measurement process + self.queue() + + def diagonalizing_gates(self): + """Returns the gates that diagonalize the measured wires such that they + are in the eigenbasis of the circuit observables. + + Returns: + List[.Operation]: the operations that diagonalize the observables + """ + try: + return self.expand().operations + except NotImplementedError: + return [] + + def __repr__(self): + """Representation of this class.""" + if self.obs is None: + return "{}(wires={})".format(self.return_type.value, self.wires.tolist()) + + # Todo: when tape is core the return type will always be taken from the MeasurementProcess + if self.obs.return_type is None: + return "{}({})".format(self.return_type.value, self.obs) + + return "{}".format(self.obs) + + def __copy__(self): + cls = self.__class__ + + if self.obs is not None: + return cls(self.return_type, obs=copy.copy(self.obs)) + + return cls(self.return_type, eigvals=self._eigvals, wires=self._wires) + + @property + def wires(self): + r"""The wires the measurement process acts on.""" + if self.obs is not None: + return self.obs.wires + return self._wires + + @property + def eigvals(self): + r"""Eigenvalues associated with the measurement process. + + If the measurement process has an associated observable, + the eigenvalues will correspond to this observable. Otherwise, + they will be the eigenvalues provided when the measurement + process was instantiated. + + Note that the eigenvalues are not guaranteed to be in any + particular order. + + **Example:** + + >>> m = MeasurementProcess(Expectation, obs=qml.PauliX(wires=1)) + >>> m.eigvals + array([1, -1]) + + Returns: + array: eigvals representation + """ + if self.obs is not None: + try: + return self.obs.eigvals + except NotImplementedError: + pass + + return self._eigvals + + def expand(self): + """Expand the measurement of an observable to a unitary + rotation and a measurement in the computational basis. + + Returns: + .JacobianTape: a quantum tape containing the operations + required to diagonalize the observable + + **Example** + + Consider a measurement process consisting of the expectation + value of an Hermitian observable: + + >>> H = np.array([[1, 2], [2, 4]]) + >>> obs = qml.Hermitian(H, wires=['a']) + >>> m = MeasurementProcess(Expectation, obs=obs) + + Expanding this out: + + >>> tape = m.expand() + + We can see that the resulting tape has the qubit unitary applied, + and a measurement process with no observable, but the eigenvalues + specified: + + >>> print(tape.operations) + [QubitUnitary(array([[-0.89442719, 0.4472136 ], + [ 0.4472136 , 0.89442719]]), wires=['a'])] + >>> print(tape.measurements[0].eigvals) + [0. 5.] + >>> print(tape.measurements[0].obs) + None + """ + if self.obs is None: + raise NotImplementedError("Cannot expand a measurement process with no observable.") + + from pennylane.tape import JacobianTape # pylint: disable=import-outside-toplevel + + with JacobianTape() as tape: + self.obs.diagonalizing_gates() + MeasurementProcess(self.return_type, wires=self.obs.wires, eigvals=self.obs.eigvals) + + return tape + + def queue(self): + """Append the measurement process to an annotated queue.""" + if self.obs is not None: + try: + qml.QueuingContext.update_info(self.obs, owner=self) + except ValueError: + self.obs.queue() + qml.QueuingContext.update_info(self.obs, owner=self) + + qml.QueuingContext.append(self, owns=self.obs) + else: + qml.QueuingContext.append(self) def expval(op): @@ -54,17 +234,7 @@ def circuit(x): "{} is not an observable: cannot be used with expval".format(op.name) ) - if isinstance(op, Tensor): - for o in op.obs: - qml.QueuingContext.remove(o) - else: - qml.QueuingContext.remove(op) - - op.return_type = Expectation - - qml.QueuingContext.append(op) - - return op + return MeasurementProcess(Expectation, obs=op) def var(op): @@ -99,17 +269,7 @@ def circuit(x): "{} is not an observable: cannot be used with var".format(op.name) ) - if isinstance(op, Tensor): - for o in op.obs: - qml.QueuingContext.remove(o) - else: - qml.QueuingContext.remove(op) - - op.return_type = Variance - - qml.QueuingContext.append(op) - - return op + return MeasurementProcess(Variance, obs=op) def sample(op): @@ -150,17 +310,7 @@ def circuit(x): "{} is not an observable: cannot be used with sample".format(op.name) ) - if isinstance(op, Tensor): - for o in op.obs: - qml.QueuingContext.remove(o) - else: - qml.QueuingContext.remove(op) - - op.return_type = Sample - - qml.QueuingContext.append(op) - - return op + return MeasurementProcess(Sample, obs=op) def probs(wires): @@ -200,9 +350,81 @@ def circuit(): wires (Sequence[int] or int): the wire the operation acts on """ # pylint: disable=protected-access - op = qml.Identity(wires=wires, do_queue=False) - op.return_type = Probability + return MeasurementProcess(Probability, wires=qml.wires.Wires(wires)) + + +def state(): + r"""Quantum state in the computational basis. + + This function accepts no observables and instead instructs the QNode to return its state. A + ``wires`` argument should *not* be provided since ``state()`` always returns a pure state + describing all wires in the device. + + **Example:** + + .. code-block:: python3 + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(wires=1) + return qml.state() + + Executing this QNode: + + >>> circuit() + array([0.70710678+0.j, 0.70710678+0.j, 0. +0.j, 0. +0.j]) + + The returned array is in lexicographic order. Hence, we have a :math:`1/\sqrt{2}` amplitude + in both :math:`|00\rangle` and :math:`|01\rangle`. + + .. note:: + + Calculating the derivative of :func:`~.state` is currently only supported when using the + classical backpropagation differentiation method (``diff_method="backprop"``) with a + compatible device. + """ + # pylint: disable=protected-access + return MeasurementProcess(State) + - qml.QueuingContext.append(op) +def density_matrix(wires): + r"""Quantum density matrix in the computational basis. + + This function accepts no observables and instead instructs the QNode to return its density + matrix or reduced density matrix. The ``wires`` argument gives the possibility + to trace out a part of the system. It can result in obtaining a mixed state, which can be + only represented by the reduced density matrix. + + **Example:** - return op + .. code-block:: python3 + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(): + qml.PauliY(wires=0) + qml.Hadamard(wires=1) + return qml.density_matrix([0]) + + Executing this QNode: + + >>> circuit() + array([[0.+0.j 0.+0.j] + [0.+0.j 1.+0.j]]) + + The returned matrix is the reduced density matrix, where system 1 is traced out. + + Args: + wires (Sequence[int] or int): the wires of the subsystem + + .. note:: + + Calculating the derivative of :func:`~.density_matrix` is currently only supported when + using the classical backpropagation differentiation method (``diff_method="backprop"``) + with a compatible device. + """ + # pylint: disable=protected-access + return MeasurementProcess(State, wires=qml.wires.Wires(wires)) diff --git a/pennylane/operation.py b/pennylane/operation.py index 7910f46c3c2..d0cbdd3d5cc 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -128,7 +128,6 @@ from pennylane.wires import Wires from .utils import pauli_eigs -from .variable import Variable # ============================================================================= # Wire types @@ -245,7 +244,7 @@ class Operator(abc.ABC): * :attr:`~.Operator.par_domain` Args: - params (tuple[float, int, array, Variable]): operator parameters + params (tuple[float, int, array]): operator parameters Keyword Args: wires (Iterable[Number, str], Number, str, Wires): Wires that the operator acts on. @@ -253,8 +252,6 @@ class Operator(abc.ABC): do_queue (bool): Indicates whether the operator should be immediately pushed into the Operator queue. """ - do_check_domain = True #: bool: flag: should we perform a domain check for the parameters? - def __copy__(self): cls = self.__class__ copied_op = cls.__new__(cls) @@ -437,10 +434,6 @@ def __init__(self, *params, wires=None, do_queue=True): "{} parameters passed, {} expected.".format(self.name, len(params), self.num_params) ) - # check the validity of the params - if self.do_check_domain: - for p in params: - self.check_domain(p) self.data = list(params) #: list[Any]: parameters of the operator if do_queue: @@ -448,80 +441,11 @@ def __init__(self, *params, wires=None, do_queue=True): def __repr__(self): """Constructor-call-like representation.""" - # FIXME using self.parameters here instead of self.data is dangerous, it assumes the data can be evaluated - # which is only true if something suitable happens to remain in VariableRef.positional_arg_values etc. after - # the last evaluation. if self.parameters: params = ", ".join([repr(p) for p in self.parameters]) return "{}({}, wires={})".format(self.name, params, self.wires.tolist()) return "{}(wires={})".format(self.name, self.wires.tolist()) - def check_domain(self, p, flattened=False): - """Check the validity of a parameter. - - :class:`.Variable` instances can represent any real scalars (but not arrays). - - Args: - p (Number, array, Variable): parameter to check - flattened (bool): True means p is an element of a flattened parameter - sequence (affects the handling of 'A' parameters) - Raises: - TypeError: parameter is not an element of the expected domain - ValueError: parameter is an element of an unknown domain - Returns: - Number, array, Variable: p - """ - # pylint: disable=too-many-branches - # If parameter is a NumPy scalar, convert it into a Python scalar. - if isinstance(p, np.ndarray) and p.ndim == 0: - p = p.item() - - if isinstance(p, Variable): - if self.par_domain == "A": - raise TypeError( - "{}: Array parameter expected, got a Variable, " - "which can only represent real scalars.".format(self.name) - ) - return p - - # p is not a Variable - if self.par_domain == "A": - if flattened: - if isinstance(p, np.ndarray): - raise TypeError( - "{}: Flattened array parameter expected, got {}.".format(self.name, type(p)) - ) - else: - if not isinstance(p, np.ndarray): - raise TypeError( - "{}: Array parameter expected, got {}.".format(self.name, type(p)) - ) - elif self.par_domain in ("R", "N"): - if not isinstance(p, numbers.Real): - raise TypeError( - "{}: Real scalar parameter expected, got {}.".format(self.name, type(p)) - ) - - if self.par_domain == "N": - if not isinstance(p, numbers.Integral): - raise TypeError( - "{}: Natural number parameter expected, got {}.".format(self.name, type(p)) - ) - if p < 0: - raise TypeError( - "{}: Natural number parameter expected, got {}.".format(self.name, p) - ) - elif self.par_domain == "L": - if not isinstance(p, list): - raise TypeError("{}: List parameter expected, got {}.".format(self.name, type(p))) - if not all(isinstance(elem, np.ndarray) for elem in p): - raise TypeError("List elements must be Numpy arrays.") - else: - raise ValueError( - "{}: Unknown parameter domain '{}'.".format(self.name, self.par_domain) - ) - return p - @property def wires(self): """Wires of this operator. @@ -533,41 +457,8 @@ def wires(self): @property def parameters(self): - """Current parameter values. - - Fixed parameters are returned as is, free parameters represented by - :class:`.Variable` instances are replaced by their - current numerical value. - - Returns: - list[Any]: parameter values - """ - # TODO profiling - def evaluate(p): - """Evaluate a single parameter.""" - if isinstance(p, np.ndarray): - # object arrays may have Variables inside them - if p.dtype == object: - temp = np.array([x.val if isinstance(x, Variable) else x for x in p.flat]) - return temp.reshape(p.shape) - return p - - if isinstance(p, list): - # p is assumed to be a list of numpy arrays - # object arrays may have Variables inside them - evaled_list = [] - for arr in p: - if arr.dtype == object: - temp = np.array([x.val if isinstance(x, Variable) else x for x in arr.flat]) - evaled_list.append(temp.reshape(arr.shape)) - return evaled_list - return p - - if isinstance(p, Variable): - p = self.check_domain(p.val) - return p - - return [evaluate(p) for p in self.data] + """Current parameter values.""" + return self.data def queue(self): """Append the operator to the Operator queue.""" @@ -604,7 +495,7 @@ class Operation(Operator): * :attr:`~.Operation.generator` Args: - params (tuple[float, int, array, Variable]): operation parameters + params (tuple[float, int, array]): operation parameters Keyword Args: wires (Sequence[int]): Subsystems it acts on. If not given, args[-1] @@ -666,21 +557,6 @@ def get_parameter_shift(self, idx, shift=np.pi / 2): # where we express a positive and a negative shift by default default_param_shift = [[multiplier, a, shift], [-multiplier, a, -shift]] param_shift = default_param_shift if recipe is None else recipe - - if hasattr(self.data[idx], "mult"): - # Parameter is a variable, we are in non-tape mode - # Need to use the internal multiplier in the Variable to update the - # multiplier and the shift - var_mult = self.data[idx].mult - - for elem in param_shift: - - # Update the multiplier - elem[0] *= var_mult - if var_mult != 0: - # Update the shift - # zero multiplier means the shift is unimportant - elem[2] /= var_mult return param_shift @property @@ -723,6 +599,29 @@ def decomposition(*params, wires): quantum operations.""" raise NotImplementedError + def expand(self): + """Returns a tape containing the decomposed operations, rather + than a list. + + Returns: + .JacobianTape: Returns a quantum tape that contains the + operations decomposition, or if not implemented, simply + the operation itself. + """ + tape = qml.tape.QuantumTape() + + with tape: + self.decomposition(*self.data, wires=self.wires) + + if not self.data: + # original operation has no trainable parameters + tape.trainable_params = {} + + if self.inverse: + tape.inv() + + return tape + def inv(self): """Inverts the operation, such that the inverse will be used for the computations by the specific device. @@ -736,7 +635,8 @@ def inv(self): Returns: :class:`Operator`: operation to be inverted """ - self.inverse = not self._inverse + current_inv = qml.QueuingContext.get_info(self).get("inverse", False) + qml.QueuingContext.update_info(self, inverse=not current_inv) return self @property @@ -820,7 +720,7 @@ class DiagonalOperation(Operation): * :attr:`~.Operation.generator` Args: - params (tuple[float, int, array, Variable]): operation parameters + params (tuple[float, int, array]): operation parameters Keyword Args: wires (Sequence[int]): Subsystems it acts on. If not given, args[-1] @@ -905,7 +805,7 @@ class Channel(Operation, abc.ABC): * :attr:`~.Operation.grad_recipe` Args: - params (tuple[float, int, array, Variable]): operation parameters + params (tuple[float, int, array]): operation parameters Keyword Args: wires (Sequence[int]): Subsystems the channel acts on. If not given, args[-1] @@ -982,7 +882,7 @@ class Observable(Operator): * :attr:`~.Operator.par_domain` Args: - params (tuple[float, int, array, Variable]): observable parameters + params (tuple[float, int, array]): observable parameters Keyword Args: wires (Sequence[int]): subsystems it acts on. @@ -1201,6 +1101,16 @@ def __init__(self, *args): # pylint: disable=super-init-not-called else: raise ValueError("Can only perform tensor products between observables.") + try: + qml.QueuingContext.update_info(o, owner=self) + except ValueError: + o.queue() + qml.QueuingContext.update_info(o, owner=self) + except NotImplementedError: + pass + + qml.QueuingContext.append(self, owns=tuple(args)) + def __copy__(self): cls = self.__class__ copied_op = cls.__new__(cls) @@ -1289,17 +1199,25 @@ def non_identity_obs(self): def __matmul__(self, other): if isinstance(other, Tensor): self.obs.extend(other.obs) - return self - if isinstance(other, Observable): + elif isinstance(other, Observable): self.obs.append(other) - return self - raise ValueError("Can only perform tensor products between observables.") + else: + raise ValueError("Can only perform tensor products between observables.") + + owning_info = qml.QueuingContext.get_info(self)["owns"] + (other,) + + # update the annotated queue information + qml.QueuingContext.update_info(self, owns=owning_info) + qml.QueuingContext.update_info(other, owner=self) + + return self def __rmatmul__(self, other): if isinstance(other, Observable): self.obs[:0] = [other] + qml.QueuingContext.update_info(other, owner=self) return self raise ValueError("Can only perform tensor products between observables.") diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index 28e47f4a1b2..2a6e2483690 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -1326,28 +1326,15 @@ def _matrix(cls, *params): @staticmethod def decomposition(phi, theta, omega, wires): - if qml.tape_mode_active(): - decomp_ops = [ - RZ((phi - omega) / 2, wires=wires[1]), - CNOT(wires=wires), - RZ(-(phi + omega) / 2, wires=wires[1]), - RY(-theta / 2, wires=wires[1]), - CNOT(wires=wires), - RY(theta / 2, wires=wires[1]), - RZ(omega, wires=wires[1]), - ] - else: # We cannot add gate parameters in non-tape mode, resulting in greater depth - decomp_ops = [ - RZ(phi / 2, wires=wires[1]), - RZ(-omega / 2, wires=wires[1]), - CNOT(wires=wires), - RZ(-phi / 2, wires=wires[1]), - RZ(-omega / 2, wires=wires[1]), - RY(-theta / 2, wires=wires[1]), - CNOT(wires=wires), - RY(theta / 2, wires=wires[1]), - RZ(omega, wires=wires[1]), - ] + decomp_ops = [ + RZ((phi - omega) / 2, wires=wires[1]), + CNOT(wires=wires), + RZ(-(phi + omega) / 2, wires=wires[1]), + RY(-theta / 2, wires=wires[1]), + CNOT(wires=wires), + RY(theta / 2, wires=wires[1]), + RZ(omega, wires=wires[1]), + ] return decomp_ops diff --git a/pennylane/optimize/qng.py b/pennylane/optimize/qng.py index da7a0ea7b4e..7a6c6e07945 100644 --- a/pennylane/optimize/qng.py +++ b/pennylane/optimize/qng.py @@ -175,7 +175,7 @@ def step_and_cost(self, qnode, x, recompute_tensor=True, metric_tensor_fn=None): """ # pylint: disable=arguments-differ if ( - not isinstance(qnode, (qml.tape.QNode, qml.qnodes.BaseQNode, qml.ExpvalCost)) + not isinstance(qnode, (qml.QNode, qml.ExpvalCost)) and metric_tensor_fn is None ): raise ValueError( diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index b761b923f64..e1d2702ff01 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -204,17 +204,13 @@ def __init__( for weight, size in weight_shapes.items() } - if qml.tape_mode_active(): - self._signature_validation_tape_mode(qnode, weight_shapes) - self.qnode = qnode + self._signature_validation(qnode, weight_shapes) + self.qnode = qnode - dtype = tf.float32 if tf.keras.backend.floatx() == tf.float32 else tf.float64 + dtype = tf.float32 if tf.keras.backend.floatx() == tf.float32 else tf.float64 - if self.qnode.diff_method != "backprop": - self.qnode.to_tf(dtype=dtype) - else: - self._signature_validation(qnode, weight_shapes) - self.qnode = to_tf(qnode, dtype=tf.keras.backend.floatx()) + if self.qnode.diff_method != "backprop": + self.qnode.to_tf(dtype=dtype) # Allows output_dim to be specified as an int, e.g., 5, or as a length-1 tuple, e.g., (5,) self.output_dim = output_dim[0] if isinstance(output_dim, Iterable) else output_dim @@ -225,7 +221,7 @@ def __init__( super().__init__(dynamic=True, **kwargs) - def _signature_validation_tape_mode(self, qnode, weight_shapes): + def _signature_validation(self, qnode, weight_shapes): sig = inspect.signature(qnode.func).parameters if self.input_arg not in sig: @@ -250,42 +246,6 @@ def _signature_validation_tape_mode(self, qnode, weight_shapes): if set(weight_shapes.keys()) | {self.input_arg} != set(sig.keys()): raise ValueError("Must specify a shape for every non-input parameter in the QNode") - def _signature_validation(self, qnode, weight_shapes): - self.sig = qnode.func.sig - - if self.input_arg not in self.sig: - raise TypeError( - "QNode must include an argument with name {} for inputting data".format( - self.input_arg - ) - ) - - if self.input_arg in set(weight_shapes.keys()): - raise ValueError( - "{} argument should not have its dimension specified in " - "weight_shapes".format(self.input_arg) - ) - - if qnode.func.var_pos: - raise TypeError("Cannot have a variable number of positional arguments") - - if qnode.func.var_keyword: - raise TypeError("Cannot have a variable number of keyword arguments") - - if set(weight_shapes.keys()) | {self.input_arg} != set(self.sig.keys()): - raise ValueError("Must specify a shape for every non-input parameter in the QNode") - - defaults = { - name for name, sig in self.sig.items() if sig.par.default != inspect.Parameter.empty - } - - self.input_is_default = self.input_arg in defaults - - if defaults - {self.input_arg} != set(): - raise TypeError( - "Only the argument {} is permitted to have a default".format(self.input_arg) - ) - def build(self, input_shape): """Initializes the QNode weights. @@ -309,30 +269,12 @@ def call(self, inputs): """ outputs = [] for x in inputs: # iterate over batch - - if qml.tape_mode_active(): - res = self._evaluate_qnode_tape_mode(x) - outputs.append(res) - else: - # The QNode can require some passed arguments to be positional and others to be - # keyword. The following loops through input arguments in order and uses - # functools.partial to bind the argument to the QNode. - qnode = self.qnode - - for arg in self.sig: - if arg is not self.input_arg: # Non-input arguments must always be positional - w = self.qnode_weights[arg] - qnode = functools.partial(qnode, w) - else: - if self.input_is_default: # The input argument can be positional or keyword - qnode = functools.partial(qnode, **{self.input_arg: x}) - else: - qnode = functools.partial(qnode, x) - outputs.append(qnode()) + res = self._evaluate_qnode(x) + outputs.append(res) return tf.stack(outputs) - def _evaluate_qnode_tape_mode(self, x): + def _evaluate_qnode(self, x): """Evaluates a tape-mode QNode for a single input datapoint. Args: diff --git a/pennylane/qnn/torch.py b/pennylane/qnn/torch.py index edc053e52ee..cb15fe5b07e 100644 --- a/pennylane/qnn/torch.py +++ b/pennylane/qnn/torch.py @@ -216,14 +216,10 @@ def __init__(self, qnode, weight_shapes: dict, init_method: Optional[Callable] = } # validate the QNode signature, and convert to a Torch QNode. - if qml.tape_mode_active(): - # TODO: update the docstring regarding changes to restrictions when tape mode is default. - self._signature_validation_tape_mode(qnode, weight_shapes) - self.qnode = qnode - self.qnode.to_torch() - else: - self._signature_validation(qnode, weight_shapes) - self.qnode = to_torch(qnode) + # TODO: update the docstring regarding changes to restrictions when tape mode is default. + self._signature_validation(qnode, weight_shapes) + self.qnode = qnode + self.qnode.to_torch() if not init_method: init_method = functools.partial(torch.nn.init.uniform_, b=2 * math.pi) @@ -238,7 +234,7 @@ def __init__(self, qnode, weight_shapes: dict, init_method: Optional[Callable] = self.register_parameter(name, self.qnode_weights[name]) - def _signature_validation_tape_mode(self, qnode, weight_shapes): + def _signature_validation(self, qnode, weight_shapes): sig = inspect.signature(qnode.func).parameters if self.input_arg not in sig: @@ -263,42 +259,6 @@ def _signature_validation_tape_mode(self, qnode, weight_shapes): if set(weight_shapes.keys()) | {self.input_arg} != set(sig.keys()): raise ValueError("Must specify a shape for every non-input parameter in the QNode") - def _signature_validation(self, qnode, weight_shapes): - self.sig = qnode.func.sig - - if self.input_arg not in self.sig: - raise TypeError( - "QNode must include an argument with name {} for inputting data".format( - self.input_arg - ) - ) - - if self.input_arg in set(weight_shapes.keys()): - raise ValueError( - "{} argument should not have its dimension specified in " - "weight_shapes".format(self.input_arg) - ) - - if qnode.func.var_pos: - raise TypeError("Cannot have a variable number of positional arguments") - - if qnode.func.var_keyword: - raise TypeError("Cannot have a variable number of keyword arguments") - - if set(weight_shapes.keys()) | {self.input_arg} != set(self.sig.keys()): - raise ValueError("Must specify a shape for every non-input parameter in the QNode") - - defaults = { - name for name, sig in self.sig.items() if sig.par.default != inspect.Parameter.empty - } - - self.input_is_default = self.input_arg in defaults - - if defaults - {self.input_arg} != set(): - raise TypeError( - "Only the argument {} is permitted to have a default".format(self.input_arg) - ) - def forward(self, inputs): # pylint: disable=arguments-differ """Evaluates a forward pass through the QNode based upon input data and the initialized weights. @@ -317,32 +277,6 @@ def forward(self, inputs): # pylint: disable=arguments-differ def _evaluate_qnode(self, x): """Evaluates the QNode for a single input datapoint. - Args: - x (tensor): the datapoint - - Returns: - tensor: output datapoint - """ - if qml.tape_mode_active(): - return self._evaluate_qnode_tape_mode(x) - - qnode = self.qnode - - for arg in self.sig: - if arg is not self.input_arg: # Non-input arguments must always be positional - w = self.qnode_weights[arg].to(x) - - qnode = functools.partial(qnode, w) - else: - if self.input_is_default: # The input argument can be positional or keyword - qnode = functools.partial(qnode, **{self.input_arg: x}) - else: - qnode = functools.partial(qnode, x) - return qnode().type(x.dtype) - - def _evaluate_qnode_tape_mode(self, x): - """Evaluates a tape-mode QNode for a single input datapoint. - Args: x (tensor): the datapoint diff --git a/pennylane/tape/qnode.py b/pennylane/qnode.py similarity index 79% rename from pennylane/tape/qnode.py rename to pennylane/qnode.py index 56277da76c2..442d89ee539 100644 --- a/pennylane/tape/qnode.py +++ b/pennylane/qnode.py @@ -26,8 +26,8 @@ from pennylane.operation import State -from pennylane.tape.interfaces.autograd import AutogradInterface, np as anp -from pennylane.tape.tapes import JacobianTape, QubitParamShiftTape, CVParamShiftTape, ReversibleTape +from pennylane.interfaces.autograd import AutogradInterface, np as anp +from pennylane.tape import JacobianTape, QubitParamShiftTape, CVParamShiftTape, ReversibleTape class QNode: @@ -450,7 +450,7 @@ def construct(self, args, kwargs): else: measurement_processes = self.qfunc_output - if not all(isinstance(m, qml.tape.MeasurementProcess) for m in measurement_processes): + if not all(isinstance(m, qml.measure.MeasurementProcess) for m in measurement_processes): raise qml.QuantumFunctionError( "A quantum function must return either a single measurement, " "or a nonempty sequence of measurements." @@ -841,234 +841,3 @@ def qfunc_decorator(func): return update_wrapper(qn, func) return qfunc_decorator - - -def _get_classical_jacobian(_qnode): - """Helper function to extract the Jacobian - matrix of the classical part of a QNode""" - - def classical_preprocessing(*args, **kwargs): - """Returns the trainable gate parameters for - a given QNode input""" - _qnode.construct(args, kwargs) - return qml.math.stack(_qnode.qtape.get_parameters()) - - if _qnode.interface == "autograd": - return qml.jacobian(classical_preprocessing) - - if _qnode.interface == "torch": - import torch - - def _jacobian(*args, **kwargs): # pylint: disable=unused-argument - return torch.autograd.functional.jacobian(classical_preprocessing, args) - - return _jacobian - - if _qnode.interface == "jax": - import jax - - return jax.jacobian(classical_preprocessing) - - if _qnode.interface == "tf": - import tensorflow as tf - - def _jacobian(*args, **kwargs): - with tf.GradientTape() as tape: - tape.watch(args) - gate_params = classical_preprocessing(*args, **kwargs) - - return tape.jacobian(gate_params, args) - - return _jacobian - - -def metric_tensor(_qnode, diag_approx=False, only_construct=False): - """metric_tensor(qnode, diag_approx=False, only_construct=False) - Returns a function that returns the value of the metric tensor - of a given QNode. - - .. note:: - - Currently, only the :class:`~.RX`, :class:`~.RY`, :class:`~.RZ`, and - :class:`~.PhaseShift` parametrized gates are supported. - All other parametrized gates will be decomposed if possible. - - Args: - qnode (.QNode or .ExpvalCost): QNode(s) to compute the metric tensor of - diag_approx (bool): iff True, use the diagonal approximation - only_construct (bool): Iff True, construct the circuits used for computing - the metric tensor but do not execute them, and return the tapes. - - Returns: - func: Function which accepts the same arguments as the QNode. When called, this - function will return the metric tensor. - - **Example** - - Consider the following QNode: - - .. code-block:: python - - dev = qml.device("default.qubit", wires=3) - - @qml.qnode(dev, interface="autograd") - def circuit(weights): - # layer 1 - qml.RX(weights[0, 0], wires=0) - qml.RX(weights[0, 1], wires=1) - - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - - # layer 2 - qml.RZ(weights[1, 0], wires=0) - qml.RZ(weights[1, 1], wires=2) - - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), qml.expval(qml.PauliY(2)) - - We can use the ``metric_tensor`` function to generate a new function, that returns the - metric tensor of this QNode: - - >>> met_fn = qml.metric_tensor(circuit) - >>> weights = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], requires_grad=True) - >>> met_fn(weights) - tensor([[0.25 , 0. , 0. , 0. ], - [0. , 0.25 , 0. , 0. ], - [0. , 0. , 0.0025, 0.0024], - [0. , 0. , 0.0024, 0.0123]], requires_grad=True) - - The returned metric tensor is also fully differentiable, in all interfaces. - For example, differentiating the ``(3, 2)`` element: - - >>> grad_fn = qml.grad(lambda x: met_fn(x)[3, 2]) - >>> grad_fn(weights) - array([[ 0.04867729, -0.00049502, 0. ], - [ 0. , 0. , 0. ]]) - """ - if _qnode.__class__.__name__ == "ExpvalCost": - if _qnode._multiple_devices: # pylint: disable=protected-access - warnings.warn( - "ExpvalCost was instantiated with multiple devices. Only the first device " - "will be used to evaluate the metric tensor." - ) - - _qnode = _qnode.qnodes.qnodes[0] - - if not isinstance(_qnode, QNode): - # non-tape mode QNode - return lambda *args, **kwargs: _qnode.metric_tensor( - args, kwargs, diag_approx=diag_approx, only_construct=only_construct - ) - - def _metric_tensor_fn(*args, **kwargs): - jac = qml.math.stack(_get_classical_jacobian(_qnode)(*args, **kwargs)) - jac = qml.math.reshape(jac, [_qnode.qtape.num_params, -1]) - - wrt, perm = np.nonzero(qml.math.toarray(jac)) - perm = np.argsort(np.argsort(perm)) - - _qnode.construct(args, kwargs) - - metric_tensor_tapes, processing_fn = qml.tape.transforms.metric_tensor( - _qnode.qtape, - diag_approx=diag_approx, - wrt=wrt.tolist() if _qnode.diff_options["method"] == "backprop" else None, - ) - - if only_construct: - return metric_tensor_tapes - - res = [t.execute(device=_qnode.device) for t in metric_tensor_tapes] - mt = processing_fn(res) - - # permute rows ad columns - mt = qml.math.gather(mt, perm) - mt = qml.math.gather(qml.math.T(mt), perm) - return mt - - return _metric_tensor_fn - - -def draw(_qnode, charset="unicode", wire_order=None, show_all_wires=False): - """draw(qnode, charset="unicode", wire_order=None, show_all_wires=False) - Create a function that draws the given _qnode. - - Args: - qnode (.QNode): the input QNode that is to be drawn. - charset (str, optional): The charset that should be used. Currently, "unicode" and - "ascii" are supported. - wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit - show_all_wires (bool): If True, all wires, including empty wires, are printed. - - Returns: - A function that has the same arguement signature as ``qnode``. When called, - the function will draw the QNode. - - **Example** - - Given the following definition of a QNode, - - .. code-block:: python3 - - qml.enable_tape() - - @qml.qnode(dev) - def circuit(a, w): - qml.Hadamard(0) - qml.CRX(a, wires=[0, 1]) - qml.Rot(*w, wires=[1]) - qml.CRX(-a, wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - We can draw the it like such: - - >>> drawer = qml.draw(circuit) - >>> drawer(a=2.3, w=[1.2, 3.2, 0.7]) - 0: ──H──╭C────────────────────────────╭C─────────╭┤ ⟨Z ⊗ Z⟩ - 1: ─────╰RX(2.3)──Rot(1.2, 3.2, 0.7)──╰RX(-2.3)──╰┤ ⟨Z ⊗ Z⟩ - - Circuit drawing works with devices with custom wire labels: - - .. code-block:: python3 - - dev = qml.device('default.qubit', wires=["a", -1, "q2"]) - - @qml.qnode(dev) - def circuit(): - qml.Hadamard(wires=-1) - qml.CNOT(wires=["a", "q2"]) - qml.RX(0.2, wires="a") - return qml.expval(qml.PauliX(wires="q2")) - - When printed, the wire order matches the order defined on the device: - - >>> drawer = qml.draw(circuit) - >>> drawer() - a: ─────╭C──RX(0.2)──┤ - -1: ──H──│────────────┤ - q2: ─────╰X───────────┤ ⟨X⟩ - - We can use the ``wire_order`` argument to change the wire order: - - >>> drawer = qml.draw(circuit, wire_order=["q2", "a", -1]) - >>> drawer() - q2: ──╭X───────────┤ ⟨X⟩ - a: ──╰C──RX(0.2)──┤ - -1: ───H───────────┤ - """ - if not hasattr(_qnode, "qtape"): - raise ValueError( - "qml.draw only works when tape mode is enabled. " - "You can enable tape mode with qml.enable_tape()." - ) - - @wraps(_qnode) - def wrapper(*args, **kwargs): - _qnode.construct(args, kwargs) - _wire_order = wire_order or _qnode.device.wires - _wire_order = qml.wires.Wires(_wire_order) - return _qnode.qtape.draw(charset, wire_order=_wire_order, show_all_wires=show_all_wires) - - return wrapper diff --git a/pennylane/qnodes/__init__.py b/pennylane/qnodes/__init__.py deleted file mode 100644 index 8af83aaa5aa..00000000000 --- a/pennylane/qnodes/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This subpackage contains the supported types of QNodes. - -.. currentmodule:: pennylane.qnodes -""" -from .base import BaseQNode, QuantumFunctionError -from .cv import CVQNode -from .decorator import qnode, QNode -from .jacobian import JacobianQNode -from .qubit import QubitQNode -from .passthru import PassthruQNode -from .rev import ReversibleQNode diff --git a/pennylane/qnodes/base.py b/pennylane/qnodes/base.py deleted file mode 100644 index ef22a53d30b..00000000000 --- a/pennylane/qnodes/base.py +++ /dev/null @@ -1,906 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Base QNode class and utilities -""" -from collections.abc import Sequence -from collections import namedtuple, OrderedDict -import inspect -import itertools - -import numpy as np - -import pennylane as qml -from pennylane.operation import Observable, CV, WiresEnum, ObservableReturnTypes -from pennylane.utils import _flatten, unflatten -from pennylane.circuit_graph import CircuitGraph, _is_observable -from pennylane.variable import Variable - - -ParameterDependency = namedtuple("ParameterDependency", ["op", "par_idx"]) -"""Represents the dependence of an Operator on a positional parameter of the quantum function. - -Args: - op (Operator): operator depending on the positional parameter in question - par_idx (int): flattened operator parameter index of the corresponding - :class:`~.Variable` instance -""" - - -SignatureParameter = namedtuple("SignatureParameter", ["idx", "par"]) -"""Describes a single parameter in a function signature. - -Args: - idx (int): positional index of the parameter in the function signature - par (inspect.Parameter): parameter description -""" - - -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. - - Adds the following attributes to func: - - * :attr:`func.sig`: OrderedDict[str, SignatureParameter]: mapping from parameters' names to their descriptions - * :attr:`func.n_pos`: int: number of required positional arguments - * :attr:`func.var_pos`: bool: can take a variable number of positional arguments (``*args``) - * :attr:`func.var_keyword`: bool: can take a variable number of keyword arguments (``**kwargs``) - - Args: - func (callable): function to introspect - """ - sig = inspect.signature(func) - # count positional args, see if VAR_ args are present - n_pos = 0 - func.var_pos = False - func.var_keyword = False - for p in sig.parameters.values(): - if p.kind <= inspect.Parameter.POSITIONAL_OR_KEYWORD: - n_pos += 1 - elif p.kind == inspect.Parameter.VAR_POSITIONAL: - func.var_pos = True - elif p.kind == inspect.Parameter.VAR_KEYWORD: - func.var_keyword = True - - func.sig = OrderedDict( - [(p.name, SignatureParameter(idx, p)) for idx, p in enumerate(sig.parameters.values())] - ) - 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.data, wires=op.wires) - if op.inverse: - decomposed_ops = qml.inv(decomposed_ops) - - 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 as e: - raise qml.DeviceError( - "Gate {} not supported on device {}".format(op.name, device.short_name) - ) from e - - return new_ops - - -class BaseQNode(qml.QueuingContext): - """Base class for quantum nodes in the hybrid computational graph. - - A *quantum node* encapsulates a :ref:`quantum function ` - (corresponding to a :ref:`variational circuit `) - and the computational device it is executed on. - - The QNode calls the quantum function to construct a :class:`.CircuitGraph` instance representing - the quantum circuit. The circuit can be either - - * *mutable*, which means the quantum function is called each time the QNode is evaluated, or - * *immutable*, which means the quantum function is called only once, on first evaluation, - to construct the circuit representation. - - If the circuit is mutable, its **auxiliary** parameters can undergo any kind of classical - processing inside the quantum function. It can also contain classical flow control structures - that depend on the auxiliary parameters, potentially resulting in a different circuit - on each call. The auxiliary parameters may also determine the wires on which operators act. - - For immutable circuits the quantum function must build the same circuit graph consisting of the same - :class:`.Operator` instances regardless of its parameters; they can only appear as the - arguments of the Operators in the circuit. Immutable circuits are slightly faster to execute, and - can be optimized, but require that the layout of the circuit is fixed. - - Args: - func (callable): The *quantum function* of the QNode. - A Python function containing :class:`~.operation.Operation` constructor calls, - and returning a tuple of measured :class:`~.operation.Observable` instances. - device (~pennylane._device.Device): computational device to execute the function on - mutable (bool): whether the circuit is mutable, see above - - Keyword Args: - vis_check (bool): whether to check for operations that cannot affect the output - par_check (bool): whether to check for unused positional params - """ - - # pylint: disable=too-many-instance-attributes - def __init__(self, func, device, *, mutable=True, **kwargs): - self.func = func #: callable: quantum function - self.device = device #: Device: device that executes the circuit - self.num_wires = device.num_wires #: int: number of subsystems (wires) in the circuit - #: int: number of flattened differentiable parameters in the circuit - self.num_variables = None - self.arg_vars = None - self.kwarg_vars = None - - #: List[Operator]: quantum circuit, in the order the quantum function defines it - self.ops = [] - - self.circuit = None #: CircuitGraph: DAG representation of the quantum circuit - - self.mutable = mutable #: bool: whether the circuit is mutable - #: dict[str, Any]: additional keyword kwargs for adjusting the QNode behavior - self.kwargs = kwargs or {} - - self.variable_deps = {} - """dict[int, list[ParameterDependency]]: Mapping from flattened qfunc positional parameter - index to the list of :class:`~pennylane.operation.Operator` instances (in this circuit) - that depend on it. - """ - - self._trainable_args = None - - self._metric_tensor_subcircuits = None - """dict[tuple[int], dict[str, Any]]: circuit descriptions for computing the metric tensor""" - - # introspect the quantum function signature - _get_signature(self.func) - - self.output_conversion = None #: callable: for transforming the output of :meth:`.Device.execute` to QNode output - self.output_dim = None #: int: dimension of the QNode output vector - self.model = self.device.capabilities()["model"] #: str: circuit type, in {'cv', 'qubit'} - - def __repr__(self): - """String representation.""" - detail = "" - return detail.format(self.device.short_name, self.func.__name__, self.num_wires) - - def print_applied(self): - """Prints the most recently applied operations from the QNode.""" - if self.circuit is None: - print("QNode has not yet been executed.") - return - self.circuit.print_contents() - - def draw( - self, charset="unicode", show_variable_names=False, wire_order=None, show_all_wires=False - ): - """Draw the QNode as a circuit diagram. - - Consider the following circuit as an example: - - .. code-block:: python3 - - @qml.qnode(dev) - def qfunc(a, w): - qml.Hadamard(0) - qml.CRX(a, wires=[0, 1]) - qml.Rot(w[0], w[1], w[2], wires=[1]) - qml.CRX(-a, wires=[0, 1]) - - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - We can draw the circuit after it has been executed: - - .. code-block:: python - - >>> result = qfunc(2.3, [1.2, 3.2, 0.7]) - >>> print(qfunc.draw()) - 0: ──H──╭C────────────────────────────╭C─────────╭┤ ⟨Z ⊗ Z⟩ - 1: ─────╰RX(2.3)──Rot(1.2, 3.2, 0.7)──╰RX(-2.3)──╰┤ ⟨Z ⊗ Z⟩ - >>> print(qfunc.draw(charset="ascii")) - 0: --H--+C----------------------------+C---------+| - 1: -----+RX(2.3)--Rot(1.2, 3.2, 0.7)--+RX(-2.3)--+| - >>> print(qfunc.draw(show_variable_names=True)) - 0: ──H──╭C─────────────────────────────╭C─────────╭┤ ⟨Z ⊗ Z⟩ - 1: ─────╰RX(a)──Rot(w[0], w[1], w[2])──╰RX(-1*a)──╰┤ ⟨Z ⊗ Z⟩ - - Args: - charset (str, optional): The charset that should be used. Currently, "unicode" and "ascii" are supported. - show_variable_names (bool, optional): Show variable names instead of values. - wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit - show_all_wires (bool): If True, all wires, including empty wires, are printed. - - Raises: - ValueError: If the given charset is not supported - pennylane.QuantumFunctionError: Drawing is impossible because the underlying CircuitGraph has not yet been constructed - - Returns: - str: The circuit representation of the QNode - """ - if self.circuit: - return self.circuit.draw( - charset=charset, - show_variable_names=show_variable_names, - wire_order=wire_order, - show_all_wires=show_all_wires, - ) - - raise RuntimeError( - "The QNode can only be drawn after its CircuitGraph has been constructed." - ) - - def set_trainable_args(self, arg_indices): - """Store the indices of quantum function positional arguments - that support differentiability. - - Args: - args (None or Set[int]): Differentiable positional argument indices. A - value of ``None`` means that all argument indices are differentiable. - """ - if not self.mutable and self.circuit is not None: - - if self.get_trainable_args() == arg_indices: - return - - raise QuantumFunctionError( - "The trainability of arguments on immutable QNodes cannot be modified after the first evaluation." - ) - - if arg_indices is None: - # all arguments are differentiable - self._trainable_args = None - return - - if not arg_indices: - # The provided arg_indices are an empty set; - # no arguments are differentiable. - self._trainable_args = set() - return - - # Perform validation - if not self.func.var_pos and max(arg_indices) > self.func.n_pos: - # QNode does not allow variable positional arguments (*args), and - # the provided index set contains a value larger than the number - # of positional arguments. - raise ValueError( - f"Argument index not available. QNode has at most {self.func.n_pos} arguments." - ) - - if any(not isinstance(i, int) or i < 0 for i in arg_indices): - raise ValueError("Argument indices must be positive integers.") - - self._trainable_args = arg_indices - - def get_trainable_args(self): - """Returns the indices of quantum function positional arguments - that support differentiability. - - Returns: - None or Set[int]: Differentiable positional argument indices. A - value of ``None`` means that all argument indices are differentiable. - """ - return self._trainable_args - - def _set_variables(self, args, kwargs): - """Store the current values of the quantum function parameters in the Variable class - so the Operators may access them. - - Args: - args (tuple[Any]): positional (differentiable) arguments - kwargs (dict[str, Any]): auxiliary arguments - """ - Variable.positional_arg_values = list(_flatten(args)) - if not self.mutable: - # only immutable circuits access auxiliary arguments through Variables - Variable.kwarg_values = {k: np.array(list(_flatten(v))) for k, v in kwargs.items()} - - def _op_descendants(self, op, only): - """Descendants of the given operator in the quantum circuit. - - Args: - op (Operator): operator in the quantum circuit - only (str, None): the type of descendants to return. - - - ``'G'``: only return non-observables (default) - - ``'O'``: only return observables - - ``None``: return all descendants - - Returns: - list[Operator]: descendants in a topological order - """ - succ = self.circuit.descendants_in_order((op,)) - if only == "O": - return list(filter(_is_observable, succ)) - if only == "G": - return list(itertools.filterfalse(_is_observable, succ)) - return succ - - def _remove(self, obj): - if isinstance(obj, Observable) and obj.return_type is not None: - self.obs_queue.remove(obj) - else: - self.queue.remove(obj) - - def _append(self, obj): - if obj.num_wires == WiresEnum.AllWires: - # check here only if enough wires - if len(obj.wires) != self.num_wires: - raise QuantumFunctionError("Operator {} must act on all wires".format(obj.name)) - - # Make sure only existing wires are used. - for w in obj.wires: - if w not in self.device.wires: - raise QuantumFunctionError( - "Operation {} applied to invalid wire {} " - "on device with wires {}.".format(obj.name, w, self.device.wires.labels) - ) - - # observables go to their own, temporary queue - if isinstance(obj, Observable): - if obj.return_type is None: - self.queue.append(obj) - else: - self.obs_queue.append(obj) - else: - if self.obs_queue: - raise QuantumFunctionError( - "State preparations and gates must precede measured observables." - ) - self.queue.append(obj) - - def _determine_structured_variable_name(self, parameter_value, prefix): - """Determine the variable names corresponding to a parameter. - - This method unrolls the parameter value if it has an array - or list structure. - - Args: - parameter_value (Union[Number, Sequence[Any], array[Any]]): The value of the parameter. This will be used - as a blueprint for the returned variable name(s). - prefix (str): Prefix that will be added to the variable name(s), usually the parameter name - - Returns: - Union[str,Sequence[str],array[str]]: The variable name(s) in the same structure as the parameter value - """ - if isinstance(parameter_value, np.ndarray): - variable_name_string = np.empty_like(parameter_value, dtype=object) - - for index in np.ndindex(*variable_name_string.shape): - variable_name_string[index] = "{}[{}]".format( - prefix, ",".join([str(i) for i in index]) - ) - elif isinstance(parameter_value, Sequence): - variable_name_string = [] - - for idx, val in enumerate(parameter_value): - variable_name_string.append( - self._determine_structured_variable_name(val, "{}[{}]".format(prefix, idx)) - ) - else: - variable_name_string = prefix - - return variable_name_string - - def _make_variables(self, args, kwargs): - """Create the :class:`~.variable.Variable` instances representing the QNode's arguments. - - The created :class:`~.variable.Variable` instances are given in the same nested structure - as the original arguments. The :class:`~.variable.Variable` instances are named according - to the argument names given in the QNode definition. Consider the following example: - - .. code-block:: python3 - - @qml.qnode(dev) - def qfunc(a, w): - qml.Hadamard(0) - qml.CRX(a, wires=[0, 1]) - qml.Rot(w[0], w[1], w[2], wires=[1]) - qml.CRX(-a, wires=[0, 1]) - - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - In this example, ``_make_variables`` will return the following :class:`~.variable.Variable` instances - - .. code-block:: python3 - - >>> qfunc(3.4, [1.2, 3.4, 5.6]) - -0.031664133410566786 - >>> qfunc._make_variables([3.4, [1.2, 3.4, 5.6]], {}) - ["a", ["w[0]", "w[1]", "w[2]"]], {} - - where the Variable instances are replaced with their name for readability. - - Args: - args (tuple[Any]): Positional arguments passed to the quantum function. - During the construction we are not concerned with the numerical values, but with - the nesting structure. - Each positional argument is replaced with a :class:`~.variable.Variable` instance. - kwargs (dict[str, Any]): Auxiliary arguments passed to the quantum function. - """ - kwargs = self.unwrap_tensor_kwargs(kwargs) - - # Get the name of the qfunc's arguments - full_argspec = inspect.getfullargspec(self.func) - - # args - variable_name_strings = [] - for variable_name, variable_value in zip(full_argspec.args, args): - variable_name_strings.append( - self._determine_structured_variable_name(variable_value, variable_name) - ) - - # varargs - len_diff = len(args) - len(full_argspec.args) - if len_diff > 0: - for idx, variable_value in enumerate(args[-len_diff:]): - variable_name = "{}[{}]".format(full_argspec.varargs, idx) - - variable_name_strings.append( - self._determine_structured_variable_name(variable_value, variable_name) - ) - - arg_vars = [Variable(idx, name) for idx, name in enumerate(_flatten(variable_name_strings))] - self.num_variables = len(arg_vars) - - # Arrange the newly created Variables in the nested structure of args. - # Make sure that NumPy scalars are converted into Python scalars. - arg_vars = [ - i.item() if isinstance(i, np.ndarray) and i.ndim == 0 else i - for i in unflatten(arg_vars, args) - ] - - if self._trainable_args is not None and len(self._trainable_args) != len(args): - # If some of the input arguments are marked as non-differentiable, - # then replace the variable instances in arg_vars back with the - # original objects. - for a, _ in enumerate(args): - if a not in self._trainable_args: - arg_vars[a] = args[a] - - # kwargs - # if not mutable: must convert auxiliary arguments to named Variables so they can be updated without re-constructing the circuit - # kwarg_vars = {} - # for key, val in kwargs.items(): - # temp = [Variable(idx, name=key) for idx, _ in enumerate(_flatten(val))] - # kwarg_vars[key] = unflatten(temp, val) - - variable_name_strings = {} - kwarg_vars = {} - for variable_name in full_argspec.kwonlyargs: - if variable_name in kwargs: - variable_value = kwargs[variable_name] - else: - variable_value = full_argspec.kwonlydefaults[variable_name] - - if isinstance(variable_value, np.ndarray): - variable_name_string = np.empty_like(variable_value, dtype=object) - - for index in np.ndindex(*variable_name_string.shape): - variable_name_string[index] = "{}[{}]".format( - variable_name, ",".join([str(i) for i in index]) - ) - - kwarg_variable = [ - Variable(idx, name=name, is_kwarg=True) - for idx, name in enumerate(_flatten(variable_name_string)) - ] - else: - kwarg_variable = Variable(0, name=variable_name, is_kwarg=True) - - kwarg_vars[variable_name] = kwarg_variable - - return arg_vars, kwarg_vars - - @staticmethod - def unwrap_tensor_kwargs(kwargs): - """Unwraps the pennylane.numpy.tensor objects that were passed as - keyword arguments so that they can be handled as gate parameters by - arbitrary devices. - - Args: - kwargs (dict[str, Any]): Auxiliary arguments passed to the quantum function. - - Returns: - dict[str, Any]: Auxiliary arguments passed to the quantum function - in an unwrapped form (if applicable). - """ - for k, v in kwargs.items(): - if isinstance(v, qml.numpy.tensor): - kwargs[k] = v.unwrap() - - return kwargs - - def _construct(self, args, kwargs): - """Construct the quantum circuit graph by calling the quantum function. - - For immutable nodes this method is called the first time :meth:`BaseQNode.evaluate` - or :meth:`.JacobianQNode.jacobian` is called, and for mutable nodes *each time* - they are called. It executes the quantum function, - stores the resulting sequence of :class:`.Operator` instances, - converts it into a circuit graph, and creates the Variable mapping. - - .. note:: - The Variables are only required for analytic differentiation, - for evaluation we could simply reconstruct the circuit each time. - - Args: - args (tuple[Any]): Positional arguments passed to the quantum function. - During the construction we are not concerned with the numerical values, but with - the nesting structure. - Each positional argument is replaced with a :class:`~.Variable` instance. - kwargs (dict[str, Any]): Auxiliary arguments passed to the quantum function. - """ - # TODO: Update the docstring to reflect the kwargs and the raising conditions - # pylint: disable=attribute-defined-outside-init, too-many-branches, too-many-statements - - self.arg_vars, self.kwarg_vars = self._make_variables(args, kwargs) - - # temporary queues for operations and observables - # TODO rename self.queue to self.op_queue - self.queue = [] #: list[Operation]: applied operations - self.obs_queue = [] #: list[Observable]: applied observables - - tape_mode = qml.tape_mode_active() - if tape_mode: - qml.disable_tape() - - try: - # set up the context for Operator entry - with self: - try: - # generate the program queue by executing the quantum circuit function - if self.mutable: - # it's ok to directly pass auxiliary arguments since the circuit is - # re-constructed each time (positional args must be replaced because - # parameter-shift differentiation requires Variables) - res = self.func(*self.arg_vars, **kwargs) - else: - # TODO: Maybe we should only convert the kwarg_vars that were actually given - res = self.func(*self.arg_vars, **self.kwarg_vars) - except: - # The qfunc call may have failed because the user supplied bad parameters, - # which is why we must wipe the created Variables. - self.arg_vars = None - self.kwarg_vars = None - raise - finally: - if tape_mode: - qml.enable_tape() - - # check the validity of the circuit - self._check_circuit(res) - del self.queue - del self.obs_queue - - # Prune all the Tensor objects that have been used in the circuit - self.ops = self._prune_tensors(self.ops) - - # map each free variable to the operators which depend on it - self.variable_deps = {k: [] for k in range(self.num_variables)} - for op in self.ops: - for j, p in enumerate(_flatten(op.data)): - if isinstance(p, Variable): - if not p.is_kwarg: # ignore auxiliary arguments - self.variable_deps[p.idx].append(ParameterDependency(op, j)) - - # generate the DAG - self.circuit = CircuitGraph(self.ops, self.variable_deps, self.device.wires) - - # check for unused positional params - if self.kwargs.get("par_check", False): - unused = [k for k, v in self.variable_deps.items() if not v] - if unused: - raise QuantumFunctionError( - "The positional parameters {} are unused.".format(unused) - ) - - # check for operations that cannot affect the output - if self.kwargs.get("vis_check", False): - invisible = self.circuit.invisible_operations() - if invisible: - raise QuantumFunctionError( - "The operations {} cannot affect the circuit output.".format(invisible) - ) - - @staticmethod - def _prune_tensors(res): - """Prune the tensors that have been passed by the quantum function. - - .. seealso:: :meth:`~.Tensor.prune` - - Args: - res (Sequence[Observable], Observable): output returned by the quantum function - - Returns: - res (Sequence[Observable], Observable): pruned output returned by the quantum function - """ - if isinstance(res, qml.operation.Tensor): - return res.prune() - - if isinstance(res, Sequence): - ops = [] - for o in res: - if isinstance(o, qml.operation.Tensor): - ops.append(o.prune()) - else: - ops.append(o) - return ops - - return res - - def _check_circuit(self, res): - """Check that the generated Operator queue corresponds to a valid quantum circuit. - - .. note:: The validity of individual Operators is checked already in :meth:`_append_op`. - - Args: - res (Sequence[Observable], Observable): output returned by the quantum function - - Raises: - QuantumFunctionError: an error was discovered in the circuit - """ - # pylint: disable=too-many-branches - - # check the return value - if isinstance(res, Observable): - self.output_dim = 1 - - if res.return_type is ObservableReturnTypes.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 - elif res.return_type is ObservableReturnTypes.Probability: - self.output_conversion = np.squeeze - - if self.model == "qubit": - num_basis_states = 2 - elif self.model == "cv": - num_basis_states = getattr(self.device, "cutoff", 10) - - self.output_dim = num_basis_states ** len(res.wires) - - else: - self.output_conversion = float - - res = (res,) - - elif isinstance(res, Sequence) and res and all(isinstance(x, 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) - ) - - # check that all ev's are returned, in the correct order - if res != tuple(self.obs_queue): - raise QuantumFunctionError( - "All measured observables must be returned in the order they are measured." - ) - - # check that no wires are measured more than once - m_wires = list(w for ob in res for w in ob.wires) - if len(m_wires) != len(set(m_wires)): - raise QuantumFunctionError( - "Each wire in the quantum circuit can only be measured once." - ) - - # True if op is a CV, False if it is a discrete variable (Identity could be either) - are_cvs = [ - isinstance(op, CV) for op in self.queue + list(res) if not isinstance(op, qml.Identity) - ] - - if not all(are_cvs) and any(are_cvs): - raise QuantumFunctionError( - "Continuous and discrete operations are not allowed in the same quantum circuit." - ) - - if any(are_cvs) and self.model == "qubit": - raise QuantumFunctionError( - "Device {} is a qubit device; CV operations are not allowed.".format( - self.device.short_name - ) - ) - - if not all(are_cvs) and self.model == "cv": - raise QuantumFunctionError( - "Device {} is a CV device; qubit operations are not allowed.".format( - self.device.short_name - ) - ) - - queue = self.queue - if self.device.operations: - # replace operations in the queue with any decompositions if required - queue = decompose_queue(self.queue, self.device) - - self.ops = queue + list(res) - - def _default_args(self, kwargs): - """Validate the quantum function arguments, apply defaults. - - Here we apply default values for the auxiliary parameters of :attr:`BaseQNode.func`. - - Args: - kwargs (dict[str, Any]): auxiliary arguments (given using the keyword syntax) - - Raises: - QuantumFunctionError: some of the arguments are invalid - - Returns: - dict[str, Any]: all auxiliary arguments (with defaults) - """ - forbidden_kinds = ( - inspect.Parameter.POSITIONAL_ONLY, - inspect.Parameter.VAR_POSITIONAL, - inspect.Parameter.VAR_KEYWORD, - ) - - # check the validity of kwargs items - for name in kwargs: - s = self.func.sig.get(name) - if s is None: - if self.func.var_keyword: - continue # unknown parameter, but **kwargs will take it TODO should it? - raise QuantumFunctionError("Unknown quantum function parameter '{}'.".format(name)) - - 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). - # FIXME why are numpy array default values not good automatically? - 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 - ) - ) - - # apply defaults - for name, s in self.func.sig.items(): - default = s.par.default - 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) - - return kwargs - - def __call__(self, *args, **kwargs): - """Wrapper for :meth:`BaseQNode.evaluate`.""" - return self.evaluate(args, kwargs) - - def evaluate(self, args, kwargs): - """Evaluate the quantum function on the specified device. - - Args: - args (tuple[Any]): positional arguments to the quantum function (differentiable) - kwargs (dict[str, Any]): auxiliary arguments (not differentiable) - - Keyword Args: - use_native_type (bool): If True, return the result in whatever type the device uses - internally, otherwise convert it into array[float]. Default: False. - - Returns: - float or array[float]: output measured value(s) - """ - kwargs = self._default_args(kwargs) - self._set_variables(args, kwargs) - - if self.circuit is None or self.mutable: - self._construct(args, kwargs) - - self.device.reset() - - temp = self.kwargs.get("use_native_type", False) - if isinstance(self.device, qml.QubitDevice): - # TODO: remove this if statement once all devices are ported to the QubitDevice API - ret = self.device.execute(self.circuit, return_native_type=temp) - else: - ret = self.device.execute( - self.circuit.operations, - self.circuit.observables, - self.variable_deps, - return_native_type=temp, - ) - - return self.output_conversion(ret) - - 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 (tuple[Any]): positional arguments to the quantum function (differentiable) - kwargs (dict[str, Any]): auxiliary arguments (not differentiable) - - Returns: - array[float]: measured values - """ - kwargs = self._default_args(kwargs) - self._set_variables(args, kwargs) - - self.device.reset() - - if isinstance(self.device, qml.QubitDevice): - # create a circuit graph containing the existing operations, and the - # observables to be evaluated. - circuit_graph = CircuitGraph( - self.circuit.operations + list(obs), - self.circuit.variable_deps, - self.device.wires, - ) - ret = self.device.execute(circuit_graph) - else: - ret = self.device.execute(self.circuit.operations, obs, self.circuit.variable_deps) - return ret diff --git a/pennylane/qnodes/cv.py b/pennylane/qnodes/cv.py deleted file mode 100644 index 11a0798b58f..00000000000 --- a/pennylane/qnodes/cv.py +++ /dev/null @@ -1,320 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -CV parameter shift quantum node. - -Provides analytic differentiation for Gaussian operations succeeded by -first and second order observables. -""" -import copy - -import numpy as np - -import pennylane as qml -from pennylane.operation import ObservableReturnTypes - -from .base import QuantumFunctionError -from .jacobian import JacobianQNode - - -class CVQNode(JacobianQNode): - """Quantum node for CV parameter shift analytic differentiation""" - - def _best_method(self, idx): - """Determine the correct partial derivative computation method for a free parameter. - - Use the parameter-shift analytic method iff every gate that depends on the parameter supports it. - If not, use the finite difference method only (one would have to use it anyway). - - Note that if even one dependent Operation does not support differentiation, - we cannot differentiate with respect to this parameter at all. - - Args: - idx (int): free parameter index - - Returns: - str: partial derivative method to be used - """ - # pylint: disable=too-many-branches - # operations that depend on this free parameter - ops = [d.op for d in self.variable_deps[idx]] - - # Observables in the circuit - # (the topological order is the queue order) - observables = self.circuit.observables_in_order - - # an empty list to store the 'best' partial derivative method - # for each operator/observable pair - best = np.empty((len(ops), len(observables)), dtype=object) - - # find the best supported partial derivative method for each operator - for k_op, op in enumerate(ops): - if op.grad_method is None: - # one nondifferentiable item makes the whole nondifferentiable - op.use_method = None - continue - - # loop over all observables - for k_ob, ob in enumerate(observables): - # get the set of operations betweens the - # operation and the observable - S = self.circuit.nodes_between(op, ob) - x = op.grad_method - - if op.grad_method == "A": - # for parameter-shift compatible CV gates we need to check both the - # intervening gates, and the type of the observable - if any(not k.supports_heisenberg for k in S): - # non-Gaussian operators present - x = "F" - elif ob.return_type == ObservableReturnTypes.Variance: - if ob.ev_order is None or ob.ev_order >= 2: - x = "F" - elif ob.return_type == ObservableReturnTypes.Probability: - x = "F" - elif ob.ev_order is None or ob.ev_order >= 2: - x = "B" - else: - x = "A" - - # If there is no path between them, p.d. is zero - # Otherwise, use finite differences - best[k_op, k_ob] = "0" if not S else x - - if all(k == "0" for k in best[k_op, :]): - op.use_method = "0" - elif "F" in best[k_op, :]: - # one non-analytic item makes the whole numeric - op.use_method = "F" - elif "B" in best[k_op, :]: - op.use_method = "B" - else: - op.use_method = "A" - - # if all ops that depend on the free parameter have a best method - # of "0", then we can skip the partial derivative altogether - if all(o.use_method == "0" for o in ops): - return "0" - - # one nondifferentiable item makes the whole nondifferentiable - if any(o.use_method is None for o in ops): - return None - - # one non-analytic item makes the whole numeric - if any(o.use_method == "F" for o in ops): - return "F" - - return "A" - - @staticmethod - def _transform_observable(obs, w, Z, device_wires): - """Apply a Gaussian linear transformation to each index of an observable. - - Args: - obs (Observable): observable to transform - w (int): number of wires in the circuit - Z (array[float]): Heisenberg picture representation of the linear transformation - device_wires (Wires): wires on the device that the observable gets applied to - - Returns: - Observable: transformed observable - """ - q = obs.heisenberg_obs(device_wires) - - if q.ndim != obs.ev_order: - raise QuantumFunctionError( - "Mismatch between the polynomial order of observable and its Heisenberg representation" - ) - - qp = q @ Z - if q.ndim == 2: - # 2nd order observable - qp = qp + qp.T - elif q.ndim > 2: - raise NotImplementedError("Transforming observables of order > 2 not implemented.") - return qml.expval(qml.PolyXP(qp, wires=range(w), do_queue=False)) - - def _pd_analytic(self, idx, args, kwargs, **options): - """Partial derivative of the node using the analytic parameter shift 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: - idx (int): flattened index of the parameter wrt. which the p.d. is computed - args (array[float]): flattened positional arguments at which to evaluate the p.d. - kwargs (dict[str, Any]): auxiliary arguments - - Keyword Args: - force_order2 (bool): iff True, use the order-2 method even if not necessary - - Returns: - array[float]: partial derivative of the node - """ - force_order2 = options.get("force_order2", False) - - n = self.num_variables - w = self.num_wires - pd = np.zeros(self.output_dim) - # find the Operators in which the free parameter appears, use the product rule - for op, p_idx in self.variable_deps[idx]: - - # We temporarily edit the Operator such that parameter p_idx is replaced by a new one, - # which we can modify without affecting other Operators depending on the original. - orig = op.data[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.data[p_idx] = temp_var - - param_shift = op.get_parameter_shift(p_idx) - - if not force_order2 and op.use_method != "B": - # basic parameter-shift method, for Gaussian CV gates - # succeeded by order-1 observables - # evaluate the circuit at multiple points with the linear - # combination of parameter values (in most cases at two points) - for multiplier, a, shift in param_shift: - - # shifted parameter values - shift_p = np.r_[args, a * args[idx] + shift] - - term = multiplier * np.asarray(self.evaluate(shift_p, kwargs)) - pd += term - else: - if len(param_shift) != 2: - # The 2nd order CV parameter-shift rule only accepts two-term shifts - raise NotImplementedError( - "Taking the analytic gradient for order-2 operators is " - "unsupported for {op} which contains a parameter with a " - "gradient recipe of more than two terms." - ) - - # Get the shifts and the multipliers - pos_multiplier, a1, pos_shift = param_shift[0] - neg_multiplier, a2, neg_shift = param_shift[1] - - # shifted parameter values - shift_p1 = np.r_[args, a1 * args[idx] + pos_shift] - shift_p2 = np.r_[args, a2 * args[idx] + neg_shift] - - # order-2 parameter-shift method, for gaussian CV gates - # succeeded by order-2 observables - # evaluate transformed observables at the original parameter point - # first build the Heisenberg picture transformation matrix Z - self._set_variables(shift_p1, kwargs) - Z2 = op.heisenberg_tr(self.device.wires) - self._set_variables(shift_p2, kwargs) - Z1 = op.heisenberg_tr(self.device.wires) - Z = pos_multiplier * Z2 + neg_multiplier * Z1 # derivative of the operation - - unshifted_args = np.r_[args, args[idx]] - self._set_variables(unshifted_args, kwargs) - Z0 = op.heisenberg_tr(self.device.wires, inverse=True) - Z = Z @ Z0 - - # conjugate Z with all the descendant operations - B = np.eye(1 + 2 * w) - B_inv = B.copy() - for BB in self._op_descendants(op, "G"): - if not BB.supports_heisenberg: - # if the descendant gate is non-Gaussian in parameter-shift differentiation - # mode, then there must be no observable following it. - continue - B = BB.heisenberg_tr(self.device.wires) @ B - B_inv = B_inv @ BB.heisenberg_tr(self.device.wires, inverse=True) - Z = B @ Z @ B_inv # conjugation - - # transform the descendant observables into their derivatives using Z - desc = self._op_descendants(op, "O") - obs = [self._transform_observable(x, w, Z, self.device.wires) for x in desc] - # Measure the transformed observables. - # The other observables do not depend on this parameter instance, - # hence their partial derivatives are zero. - res = self.evaluate_obs(obs, unshifted_args, kwargs) - - # add the measured pd's to the correct locations - inds = [self.circuit.observables.index(x) for x in desc] - pd[inds] += res - - # restore the original parameter - op.data[p_idx] = orig - - return pd - - def _pd_analytic_var(self, idx, args, kwargs, **options): - """Partial derivative of the variance of an observable using the parameter-shift method. - - Args: - idx (int): flattened index of the parameter wrt. which the p.d. is computed - args (array[float]): flattened positional arguments at which to evaluate the p.d. - kwargs (dict[str, Any]): auxiliary arguments - - Returns: - array[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 ObservableReturnTypes.Variance for e in self.circuit.observables - ] - var_observables = [ - e for e in self.circuit.observables if e.return_type == ObservableReturnTypes.Variance - ] - - # first, replace each var(A) with - new_observables = [] - for e in var_observables: - # need to calculate d/dp - w = e.wires - - # CV first order observable - # get the heisenberg representation - # This will be a real 1D vector representing the - # first order observable in the basis [I, x, p] - A = e._heisenberg_rep(e.parameters) # pylint: disable=protected-access - - # take the outer product of the heisenberg representation - # with itself, to get a square symmetric matrix representing - # the square of the observable - A = np.outer(A, A) - new = qml.expval(qml.PolyXP(A, w, do_queue=False)) - - # replace the var(A) observable with - self.circuit.update_node(e, new) - new_observables.append(new) - - # calculate the analytic derivatives of the observables - pdA2 = self._pd_analytic(idx, args, kwargs, force_order2=True) - - # restore the original observables, but convert their return types to expectation - for e, new in zip(var_observables, new_observables): - self.circuit.update_node(new, e) - e.return_type = ObservableReturnTypes.Expectation - - # evaluate - evA = np.asarray(self.evaluate(args, kwargs)) - - # evaluate the analytic derivative of - pdA = self._pd_analytic(idx, args, kwargs) - - # restore return types - for e in var_observables: - e.return_type = ObservableReturnTypes.Variance - - # return d(var(A))/dp = d/dp -2 * * d/dp for the variances, - # d/dp for plain expectations - return np.where(where_var, pdA2 - 2 * evA * pdA, pdA) diff --git a/pennylane/qnodes/decorator.py b/pennylane/qnodes/decorator.py deleted file mode 100644 index 8f707eb7852..00000000000 --- a/pennylane/qnodes/decorator.py +++ /dev/null @@ -1,334 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -QNode decorator. -""" -from functools import lru_cache - -from .base import BaseQNode -from .cv import CVQNode -from .device_jacobian import DeviceJacobianQNode -from .jacobian import JacobianQNode -from .qubit import QubitQNode -from .passthru import PassthruQNode -from .rev import ReversibleQNode - - -PARAMETER_SHIFT_QNODES = {"qubit": QubitQNode, "cv": CVQNode} -ALLOWED_DIFF_METHODS = ( - "best", - "backprop", - "device", - "parameter-shift", - "finite-diff", - "reversible", -) -ALLOWED_INTERFACES = ("autograd", "numpy", "torch", "tf") - - -def _get_qnode_class(device, interface, diff_method): - """Returns the class for the specified QNode. - - Args: - device (~.Device): a PennyLane-compatible device - interface (str): the interface that will be used for classical backpropagation - diff_method (str, None): the method of differentiation to use in the created QNode - - Raises: - ValueError: if an unrecognized ``diff_method`` is provided - - Returns: - ~.BaseQNode: the QNode class object that is compatible with the provided device and - differentiation method - """ - # pylint: disable=too-many-return-statements,too-many-branches - model = device.capabilities().get("model", "qubit") - passthru_interface = device.capabilities().get("passthru_interface", None) - device_provides_jacobian = device.capabilities().get("provides_jacobian", False) - - allows_passthru = passthru_interface is not None - - if diff_method is None: - # QNode is not differentiable - return BaseQNode - - if diff_method == "best": - - if allows_passthru and interface == passthru_interface: - # hand off differentiation to the device without type conversion - return PassthruQNode - - if device_provides_jacobian: - # hand off differentiation to the device - return DeviceJacobianQNode - - if model in PARAMETER_SHIFT_QNODES: - # parameter-shift analytic differentiation - return PARAMETER_SHIFT_QNODES[model] - - if diff_method == "backprop": - if allows_passthru: - if interface != passthru_interface: - raise ValueError( - "Device {} only supports diff_method='backprop' when using the " - "{} interface.".format(device.short_name, passthru_interface) - ) - return PassthruQNode - - raise ValueError( - "The {} device does not support native computations with " - "autodifferentiation frameworks.".format(device.short_name) - ) - - if diff_method == "device": - if device_provides_jacobian: - return DeviceJacobianQNode - - raise ValueError( - "The {} device does not provide a native method " - "for computing the jacobian.".format(device.short_name) - ) - - if diff_method == "parameter-shift": - if model in PARAMETER_SHIFT_QNODES: - # parameter-shift analytic differentiation - return PARAMETER_SHIFT_QNODES[model] - - raise ValueError( - "The parameter shift rule is not available for devices with model {}.".format(model) - ) - - if diff_method == "reversible": - # pylint: disable=protected-access - - # TODO: update when all capabilities keys changed to "supports_reversible_diff" - supports_reverse = device.capabilities().get( - "supports_reversible_diff", False - ) or device.capabilities().get("reversible_diff", False) - if not supports_reverse: - raise ValueError( - "Reversible differentiation method not supported on {}".format(device.short_name) - ) - return ReversibleQNode - - if diff_method in ALLOWED_DIFF_METHODS: - # finite differences - return JacobianQNode - - raise ValueError( - "Differentiation method {} not recognized. Allowed " - "options are {}".format(diff_method, ALLOWED_DIFF_METHODS) - ) - - -def _apply_interface(qnode_, interface, diff_method): - """Applies an interface to a specified QNode. - - Args: - qnode_ (~.BaseQNode): the QNode to which the interface is applied - interface (str): the interface that will be used for classical backpropagation - diff_method (str, None): the method of differentiation to use in the created QNode - - Raises: - ValueError: if an unrecognized or invalid ``interface`` is provided - - Returns: - callable: the QNode method that creates the interface - """ - if interface is None: - # if no interface is specified, return the 'bare' QNode - return qnode_ - - if interface == "torch": - return qnode_.to_torch() - - if interface == "tf": - return qnode_.to_tf() - - if interface in ("autograd", "numpy"): - # keep "numpy" for backwards compatibility - return qnode_.to_autograd() - - raise ValueError( - "Interface {} not recognized. Allowed " - "interfaces are {}".format(diff_method, ALLOWED_INTERFACES) - ) - - -def QNode(func, device, *, interface="autograd", mutable=True, diff_method="best", **kwargs): - """QNode constructor for creating QNodes. - - When applied to a quantum function and device, converts it into - a :class:`QNode` instance. - - **Example** - - >>> def circuit(x): - >>> qml.RX(x, wires=0) - >>> return qml.expval(qml.PauliZ(0)) - >>> dev = qml.device("default.qubit", wires=1) - >>> qnode = QNode(circuit, dev) - - Args: - func (callable): a quantum function - device (~.Device): a PennyLane-compatible device - interface (str): The interface that will be used for classical backpropagation. - This affects the types of objects that can be passed to/returned from the QNode: - - * ``interface='autograd'``: Allows autograd to backpropogate - through the QNode. The QNode accepts default Python types - (floats, ints, lists) as well as NumPy array arguments, - and returns NumPy arrays. - - * ``interface='torch'``: Allows PyTorch to backpropogate - through the QNode. The QNode accepts and returns Torch tensors. - - * ``interface='tf'``: Allows TensorFlow in eager mode to backpropogate - through the QNode. The QNode accepts and returns - TensorFlow ``tf.Variable`` and ``tf.tensor`` objects. - - * ``None``: The QNode accepts default Python types - (floats, ints, lists) as well as NumPy array arguments, - and returns NumPy arrays. It does not connect to any - machine learning library automatically for backpropagation. - - mutable (bool): whether the QNode circuit is mutable - diff_method (str, None): the method of differentiation to use in the created QNode. - - * ``"best"``: Best available method. Uses classical backpropagation or the - device directly to compute the gradient if supported, otherwise will use - the analytic parameter-shift rule where possible with finite-difference as a fallback. - - * ``"backprop"``: Use classical backpropagation. Only allowed on simulator - devices that are classically end-to-end differentiable, for example - :class:`default.tensor.tf <~.DefaultTensorTF>`. Note that the returned - QNode can only be used with the machine-learning framework supported - by the device; a separate ``interface`` argument should not be passed. - - * ``"reversible"``: Uses a reversible method for computing the gradient. - This method is similar to ``"backprop"``, but trades off increased - runtime with significantly lower memory usage. Compared to the - parameter-shift rule, the reversible method can be faster or slower, - depending on the density and location of parametrized gates in a circuit. - Only allowed on (simulator) devices with the "reversible" capability, - for example :class:`default.qubit <~.DefaultQubit>`. - - * ``"device"``: Queries the device directly for the gradient. - Only allowed on devices that provide their own gradient rules. - - * ``"parameter-shift"``: Use the analytic parameter-shift - rule where possible, with finite-difference as a fallback. - - * ``"finite-diff"``: Uses numerical finite-differences for all parameters. - - * ``None``: a non-differentiable QNode is returned. - - Keyword Args: - h (float): Step size for the finite difference method. Default is ``1e-7`` for analytic devices, or - ``0.3`` for non-analytic devices (those that estimate expectation values with a finite number of shots). - order (int): order for the finite-difference method, must be 1 (default) or 2 - """ - qnode_class = _get_qnode_class(device, interface, diff_method) - qnode_ = qnode_class(func, device, mutable=mutable, **kwargs) - - if isinstance(qnode_, PassthruQNode): - qnode_.interface = interface - else: - # PassthruQNode's do not support interface conversions - qnode_ = _apply_interface(qnode_, interface, diff_method) - - return qnode_ - - -def qnode(device, *, interface="autograd", mutable=True, diff_method="best", **kwargs): - """Decorator for creating QNodes. - - When applied to a quantum function, this decorator converts it into - a :class:`QNode` instance. - - **Example** - - >>> dev = qml.device("default.qubit", wires=1) - >>> @qml.qnode(dev) - >>> def circuit(x): - >>> qml.RX(x, wires=0) - >>> return qml.expval(qml.PauliZ(0)) - - Args: - device (~.Device): a PennyLane-compatible device - interface (str): The interface that will be used for classical backpropagation. - This affects the types of objects that can be passed to/returned from the QNode: - - * ``interface='autograd'``: Allows autograd to backpropogate - through the QNode. The QNode accepts default Python types - (floats, ints, lists) as well as NumPy array arguments, - and returns NumPy arrays. - - * ``interface='torch'``: Allows PyTorch to backpropogate - through the QNode. The QNode accepts and returns Torch tensors. - - * ``interface='tf'``: Allows TensorFlow in eager mode to backpropogate - through the QNode. The QNode accepts and returns - TensorFlow ``tf.Variable`` and ``tf.tensor`` objects. - - * ``None``: The QNode accepts default Python types - (floats, ints, lists) as well as NumPy array arguments, - and returns NumPy arrays. It does not connect to any - machine learning library automatically for backpropagation. - - mutable (bool): whether the QNode circuit is mutable - diff_method (str, None): the method of differentiation to use in the created QNode. - - * ``"best"``: Best available method. Uses classical backpropagation or the - device directly to compute the gradient if supported, otherwise will use - the analytic parameter-shift rule where possible with finite-difference as a fallback. - - * ``"backprop"``: Use classical backpropagation. Only allowed on simulator - devices that are classically end-to-end differentiable, for example - :class:`default.tensor.tf <~.DefaultTensorTF>`. Note that the returned - QNode can only be used with the machine learning framework supported - by the device; a separate ``interface`` argument should not be passed. - - * ``"reversible"``: Uses a reversible method for computing the gradient. - This method is similar to ``"backprop"``, but trades off increased - runtime with significantly lower memory usage. Compared to the - parameter-shift rule, the reversible method can be faster or slower, - depending on the density and location of parametrized gates in a circuit. - Only allowed on (simulator) devices with the "reversible" capability, - for example :class:`default.qubit <~.DefaultQubit>`. - - * ``"device"``: Queries the device directly for the gradient. - Only allowed on devices that provide their own gradient rules. - - * ``"parameter-shift"``: Use the analytic parameter-shift - rule where possible, with finite-difference as a fallback. - - * ``"finite-diff"``: Uses numerical finite-differences for all parameters. - - * ``None``: a non-differentiable QNode is returned. - - Keyword Args: - h (float): Step size for the finite difference method. Default is ``1e-7`` for analytic devices, or - ``0.3`` for non-analytic devices (those that estimate expectation values with a finite number of shots). - order (int): order for the finite-difference method, must be 1 (default) or 2 - """ - - @lru_cache() - def qfunc_decorator(func): - """The actual decorator""" - return QNode( - func, device, interface=interface, mutable=mutable, diff_method=diff_method, **kwargs - ) - - return qfunc_decorator diff --git a/pennylane/qnodes/device_jacobian.py b/pennylane/qnodes/device_jacobian.py deleted file mode 100644 index ad40066c89d..00000000000 --- a/pennylane/qnodes/device_jacobian.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Device Jacobian QNode. - -A QNode that delegates all gradient computations directly to the device. -""" -from .jacobian import JacobianQNode - - -class DeviceJacobianQNode(JacobianQNode): - """Quantum node that delegates gradient computation to the device""" - - # pylint: disable=abstract-method - - def _best_method(self, idx): - """Determine the correct partial derivative computation method for a positional parameter. - - For this QNode, the partial derivative of every free parameter will be - computed using the device; only parameters used in operations with - ``grad_method=None`` will be marked as non-differentiable. - - Args: - idx (int): free parameter index - - Returns: - str: partial derivative method to be used - """ - # operations that depend on this free parameter - ops = [d.op for d in self.variable_deps[idx]] - methods = [op.grad_method for op in ops] - - # one nondifferentiable item makes the whole nondifferentiable - if None in methods: - return None - - return "A" - - def jacobian( - self, args, kwargs=None, *, wrt=None, options=None - ): # pylint: disable=arguments-differ - return super().jacobian(args, kwargs=kwargs, wrt=wrt, method="device", options=options) diff --git a/pennylane/qnodes/jacobian.py b/pennylane/qnodes/jacobian.py deleted file mode 100644 index 5cacebb3a75..00000000000 --- a/pennylane/qnodes/jacobian.py +++ /dev/null @@ -1,449 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Differentiable quantum nodes. -""" -from collections.abc import Iterable - -import numpy as np - -from pennylane.operation import ObservableReturnTypes -from pennylane.utils import _flatten, _inv_dict - -from .base import BaseQNode, QuantumFunctionError - -DEFAULT_STEP_SIZE = 0.3 -DEFAULT_STEP_SIZE_ANALYTIC = 1e-7 - - -class JacobianQNode(BaseQNode): - """Quantum node that can be differentiated with respect to its positional parameters.""" - - def __init__(self, func, device, mutable=True, **kwargs): - super().__init__(func, device, mutable=mutable, **kwargs) - - self.par_to_grad_method = None - """dict[int, str]: map from flattened quantum function positional parameter index - to the gradient method to be used with that parameter""" - - analytic = getattr(self.device, "analytic", False) - """bool: whether the device runs in analytic mode; this attribute is - not defined for hardware devices so set to False in such cases""" - - default_step_size = DEFAULT_STEP_SIZE_ANALYTIC if analytic else DEFAULT_STEP_SIZE - self._h = kwargs.get("h", default_step_size) - """float: step size for the finite difference method""" - - self._order = kwargs.get("order", 1) - """float: order for the finite difference method""" - - metric_tensor = None - - @property - def interface(self): - """str, None: automatic differentiation interface used by the node, if any""" - return None - - @property - def h(self): - """float: step size for the finite difference method""" - return self._h - - @h.setter - def h(self, value): - self._h = value - - @property - def order(self): - """float: order for the finite difference method""" - return self._order - - @order.setter - def order(self, value): - self._order = value - - def __repr__(self): - """String representation.""" - detail = "" - return detail.format( - self.device.short_name, self.func.__name__, self.num_wires, self.interface - ) - - def _construct(self, args, kwargs): - """Constructs the quantum circuit graph by calling the quantum function. - - Like :meth:`.QNode._construct`, additionally determines the best gradient computation method - for each positional parameter. - """ - super()._construct(args, kwargs) - self.par_to_grad_method = {k: self._best_method(k) for k in self.variable_deps} - - def _best_method(self, idx): - """Determine the correct partial derivative computation method for a free parameter. - - Note that if even one dependent Operation does not support differentiation, - we cannot differentiate with respect to this parameter at all. - - .. note:: - - The ``JacobianQNode`` only supports numerical differentiation, so - this method will always return either ``"F"`` or ``None``. If an inheriting - QNode supports analytic differentiation for certain operations, make sure - that this method is overwritten appropriately to return ``"A"`` where - required. - - Args: - idx (int): free parameter index - - Returns: - str: partial derivative method to be used - """ - # operations that depend on this free parameter - ops = [d.op for d in self.variable_deps[idx]] - - # Observables in the circuit - # (the topological order is the queue order) - observables = self.circuit.observables_in_order - - # an empty list to store the 'best' partial derivative method - # for each operator/observable pair - best = np.empty((len(ops), len(observables)), dtype=object) - - # find the best supported partial derivative method for each operator - for k_op, op in enumerate(ops): - if op.grad_method is None: - # one nondifferentiable item makes the whole nondifferentiable - op.use_method = None - continue - - # loop over all observables - for k_ob, ob in enumerate(observables): - # get the set of operations betweens the - # operation and the observable - S = self.circuit.nodes_between(op, ob) - - # If there is no path between them, p.d. is zero - # Otherwise, use finite differences - best[k_op, k_ob] = "0" if not S else "F" - - if all(k == "0" for k in best[k_op, :]): - op.use_method = "0" - else: - op.use_method = "F" - - # if all ops that depend on the free parameter have a best method - # of "0", then we can skip the partial derivative altogether - if all(o.use_method == "0" for o in ops): - return "0" - - # one nondifferentiable item makes the whole nondifferentiable - if any(o.use_method is None for o in ops): - return None - - return "F" - - def jacobian(self, args, kwargs=None, *, wrt=None, method="best", options=None): - r"""Compute the Jacobian of the QNode. - - Returns the Jacobian of the parametrized quantum circuit encapsulated in the QNode. - The Jacobian is returned as a two-dimensional array. The (possibly nested) input arguments - of the QNode are :func:`flattened <_flatten>` so the QNode can be interpreted as a simple - :math:`\mathbb{R}^m \to \mathbb{R}^n` function. - - 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(wrt)``. - - * Analytic method (``'A'``). Analytic, if implemented by the inheriting QNode. - - * Best known method for each parameter (``'best'``): uses the analytic method if - possible, otherwise finite difference. - - * Device method (``'device'``): Delegates the computation of the Jacobian to the - device executing the circuit. Only supported by devices that provide their - own method for computing derivatives; support can be checked by - querying the device capabilities: ``dev.capabilities()['provides_jacobian']`` must - return ``True``. Examples of supported devices include the experimental - :class:`"default.tensor.tf" <~.DefaultTensorTF>` device. - - .. note:: - The finite difference method is sensitive to statistical noise in the circuit output, - since it compares the output at two points infinitesimally close to each other. Hence the - 'F' method requires exact expectation values, i.e., ``analytic=True`` in simulator devices. - - Args: - args (nested Iterable[float] or float): positional arguments to the quantum function (differentiable) - kwargs (dict[str, Any]): auxiliary arguments to the quantum function (not differentiable) - wrt (Sequence[int] or None): Indices of the flattened positional parameters with respect - to which to compute the Jacobian. None means all the parameters. - Note that you cannot compute the Jacobian with respect to the kwargs. - method (str): Jacobian computation method, in ``{'F', 'A', 'best', 'device'}``, see above - options (dict[str, Any]): additional options for the computation methods - - * h (float): finite difference method step size - * order (int): finite difference method order, 1 or 2 - - Returns: - array[float]: Jacobian, shape ``(n, len(wrt))``, where ``n`` is the number of outputs returned by the QNode - """ - # pylint: disable=too-many-branches,too-many-statements - if not isinstance(args, Iterable): - args = (args,) - kwargs = kwargs or {} - - # apply defaults - kwargs = self._default_args(kwargs) - - options = options or {} - - # Add the step size into the options, if it was not there already - if "h" not in options.keys(): - options = {"h": self.h, **options} - if "order" not in options.keys(): - options = {"order": self._order, **options} - - # (re-)construct the circuit if necessary - if self.circuit is None or self.mutable: - self._construct(args, kwargs) - - returns_samples = [ - str(ob) - for ob in self.circuit.observables - if ob.return_type is ObservableReturnTypes.Sample - ] - if returns_samples: - raise QuantumFunctionError( - "Circuits that include sampling can not be differentiated. " - "The following observables include sampling: {}".format("; ".join(returns_samples)) - ) - - # check that the wrt parameters are ok - if wrt is None: - wrt = range(self.num_variables) - else: - if min(wrt) < 0 or max(wrt) >= self.num_variables: - raise ValueError( - "Tried to compute the gradient with respect to parameters {} " - "(this node has {} parameters).".format(wrt, self.num_variables) - ) - if len(wrt) != len(set(wrt)): # set removes duplicates - raise ValueError("Parameter indices must be unique.") - - # check if the method can be used on the requested parameters - method_map = _inv_dict(self.par_to_grad_method) - - def inds_using(m): - """Intersection of ``wrt`` with free params indices whose best grad method is m.""" - return method_map.get(m, set()).intersection(wrt) - - # are we trying to differentiate wrt. params that don't support any method? - bad = inds_using(None) - if bad: - # get bad argument name - bad_var_names = { - v.name for v in _flatten(self.arg_vars) if hasattr(v, "idx") and v.idx in bad - } - raise ValueError( - "Cannot differentiate with respect to argument(s) {}.".format(bad_var_names) - ) - - if method == "device": - self._set_variables(args, kwargs) - return self.device.jacobian( - self.circuit.operations, self.circuit.observables, self.variable_deps - ) - - if method == "A": - bad = inds_using("F") - - # get bad argument name - bad_var_names = { - v.name for v in _flatten(self.arg_vars) if hasattr(v, "idx") and v.idx in bad - } - - if bad: - raise ValueError( - "The analytic gradient method cannot be " - "used with the argument(s) {}.".format(bad_var_names) - ) - # only variants of the analytic method remain - method = self.par_to_grad_method - elif method == "F": - # use the requested method for every parameter - method = {k: "F" for k in wrt} - elif method == "best": - # use best known method for each parameter - method = self.par_to_grad_method - else: - raise ValueError("Unknown gradient method.") - - if "F" in method.values(): - if options.get("order", 1) == 1: - # the value of the circuit at args, computed only once here - options["y0"] = np.asarray(self.evaluate(args, kwargs)) - - # In the following, to evaluate the Jacobian we call self.evaluate several times using - # modified args (and possibly modified circuit Operators). - # We do not want evaluate to call _construct again. This would only be necessary if the - # auxiliary args changed, since only they can change the structure of the circuit, - # and we do not modify them. To achieve this, we temporarily make the circuit immutable. - mutable = self.mutable - self.mutable = False - - # flatten the nested Sequence of input arguments - flat_args = np.array(list(_flatten(args)), dtype=float) - variances_required = any( - ob.return_type is ObservableReturnTypes.Variance for ob in self.circuit.observables - ) - - # compute the partial derivative wrt. each parameter using the appropriate method - grad = np.zeros((self.output_dim, len(wrt)), dtype=float) - for i, k in enumerate(wrt): - par_method = method[k] - - if par_method == "0": - # unused/invisible, partial derivatives wrt. this param are zero - continue - - if par_method == "A": - if variances_required: - grad[:, i] = self._pd_analytic_var(k, flat_args, kwargs, **options) - else: - grad[:, i] = self._pd_analytic(k, flat_args, kwargs, **options) - elif par_method == "F": - grad[:, i] = self._pd_finite_diff(k, flat_args, kwargs, **options) - else: - raise ValueError("Unknown gradient method.") - - self.mutable = mutable # restore original mutability - return grad - - def _pd_finite_diff(self, idx, args, kwargs, **options): - """Partial derivative of the node using the finite difference method. - - Args: - idx (int): flattened index of the parameter wrt. which the p.d. is computed - args (array[float]): flattened positional arguments at which to evaluate the p.d. - kwargs (dict[str, Any]): auxiliary arguments - - Keyword Args: - y0 (array[float], None): value of the circuit at the given arguments - h (float): step size - order (int): finite difference method order, 1 or 2 - - Returns: - array[float]: partial derivative of the node - """ - h = options.get("h", self.h) - order = options.get("order", self.order) - - shift_args = args.copy() - if order == 1: - y0 = options.get("y0", None) - # shift the parameter by h - shift_args[idx] += h - y = np.asarray(self.evaluate(shift_args, kwargs)) - return (y - y0) / h - - if order == 2: - # symmetric difference - # shift the parameter by +-h/2 - shift_args[idx] += 0.5 * h - y2 = np.asarray(self.evaluate(shift_args, kwargs)) - shift_args[idx] = args[idx] - 0.5 * h - y1 = np.asarray(self.evaluate(shift_args, kwargs)) - return (y2 - y1) / h - - raise ValueError("Order must be 1 or 2.") - - def _pd_analytic(self, idx, args, kwargs, **options): - """Partial derivative of the node using an analytic method. - - Args: - idx (int): flattened index of the parameter wrt. which the p.d. is computed - args (array[float]): flattened positional arguments at which to evaluate the p.d. - kwargs (dict[str, Any]): auxiliary arguments - - Returns: - array[float]: partial derivative of the node - """ - raise NotImplementedError - - def _pd_analytic_var(self, idx, args, kwargs, **options): - """Partial derivative of the variance of an observable using an analytic method. - - Args: - idx (int): flattened index of the parameter wrt. which the p.d. is computed - args (array[float]): flattened positional arguments at which to evaluate the p.d. - kwargs (dict[str, Any]): auxiliary arguments - - Returns: - array[float]: partial derivative of the node - """ - raise NotImplementedError - - def to_torch(self): - """Attach the Torch interface to the Jacobian QNode. - - Raises: - QuantumFunctionError: if PyTorch is not installed - """ - # 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 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 _to_torch(self) - - def to_tf(self): - """Attach the TensorFlow interface to the Jacobian QNode. - - Raises: - QuantumFunctionError: if TensorFlow >= 1.12 is not installed - """ - # 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 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 _to_tf(self) - - def to_autograd(self): - """Attach the TensorFlow interface to the Jacobian QNode. - - Raises: - QuantumFunctionError: if Autograd is not installed - """ - # 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.autograd import to_autograd as _to_autograd - except ImportError: # pragma: no cover - raise QuantumFunctionError( - "Autograd not found. Please install " - "the latest version of Autograd to enable the 'autograd' interface." - ) from None - - return _to_autograd(self) diff --git a/pennylane/qnodes/passthru.py b/pennylane/qnodes/passthru.py deleted file mode 100644 index 41f543aa2ee..00000000000 --- a/pennylane/qnodes/passthru.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -PassthruQNode class -""" -import pennylane.operation -import pennylane.circuit_graph -from .base import BaseQNode, QuantumFunctionError - - -""" -Design notes ------------- - -PassthruQNode requires some changes to the way other PennyLane components work: - -1. :class:`Operator` must not do domain checking for its parameters, or it must let the ADT pass the check. -2. The simulator device must return the result as the ADT instead of plain Python/NumPy types. -3. Any output_conversion in :meth:`BaseQNode.evaluate` must be skipped. - -Additionally, any array-like ADT needs to be able to handle (1) scalar multiplication, -(2) indexing/slicing, and possibly (3) iteration, as these are the things qfuncs expect of -array-like parameters. - -PassthruQNode does not have a Jacobian method, so it does not HAVE to use Variables or scalar linear indexing of input parameters. -Two options: -1. Use Variables anyway, re-use most BaseQNode methods. - Problem: after evaluating the Variables, stacking sliced/indexed Tensors in Operation.parameters should somehow result in a Tensor, not an object array. -2. Do not use Variables, call the qfunc each time :meth:`PassthruQNode.evaluate` is called (always mutable). - Problem: tensornet_tf requires variable_deps? - -TODO rethink output_conversion? should require device to return things in a fixed form, but either as arrays or as AD Tensors, do conversion in interface (if necessary...) -""" - - -class PassthruQNode(BaseQNode): - """Differentiable quantum node that appears as a white box to an external autodiff framework. - - In PennyLane, the QNode classes work as black box functions with respect to any - autodiff (AD) framework (such as TensorFlow or PyTorch). This means that the QNode - converts all its inputs (which may come in data types specific to the - AD framework used, which we denote *ADT* here) into plain Python/NumPy types, - computes the required :ref:`quantum function ` value or Jacobian, - and converts the result back into the ADT if necessary. - - In contrast, PassthruQNode works as a white box: it preserves the ADT - throughout the computation. This requires that the quantum function is computed - using a simulator device that is compatible with the AD framework used (typically - implemented using that same framework), and returns the result as the ADT instead - of plain Python/NumPy types. - - The advantages of this approach are that the qfunc can be differentiated using its AD framework - without requiring a separate method for computing the Jacobian, and that the internals - of the simulation are visible in the computational graph. - - Args: - func (callable): The *quantum function* of the QNode. - A Python function containing :class:`~.operation.Operation` constructor calls, - and returning a tuple of measured :class:`~.operation.Observable` instances. - device (~pennylane._device.Device): computational device to execute the function on - - Keyword Args: - use_native_type (bool): If True, return the result in whatever type the device uses - internally, otherwise convert it into array[float]. Default: True. - """ - - def __init__(self, func, device, **kwargs): - # make the device return the result in its native type - kwargs = kwargs or {} - kwargs.setdefault("use_native_type", True) - kwargs.setdefault("mutable", True) - - if not kwargs.get("mutable"): - raise ValueError("PassthruQNode does not support immutable mode.") - - super().__init__(func, device, **kwargs) - - def __repr__(self): - """String representation.""" - detail = "" - return detail.format(self.device.short_name, self.func.__name__, self.num_wires) - - def _set_variables(self, args, kwargs): - # do nothing, since we do not use Variables - pass - - def _construct(self, args, kwargs): - """Construct the quantum circuit graph by calling the quantum function. - - Like :class:`.BaseQNode._construct`, but does not use Variables. - """ - # temporary queues for operations and observables - self.queue = [] #: list[Operation]: applied operations - self.obs_queue = [] #: list[Observable]: applied observables - - # set up the context for Operator entry - with self: - # use a try/finally block such that if any errors arise during - # checking, and the user manually catches the exception, the class - # attribute pennylane.operation.Operator.do_check_domain is - # properly reset to True - try: - # turn off domain checking since PassthruQNode qfuncs can take any class as input - pennylane.operation.Operator.do_check_domain = False - # generate the program queue by executing the quantum circuit function - res = self.func(*args, **kwargs) - finally: - pennylane.operation.Operator.do_check_domain = True - - # use a try/finally block here too - try: - # check the validity of the circuit - # turn off domain checking, but outside of the context such that no - # queuing takes place (e.g. from decompositions) - pennylane.operation.Operator.do_check_domain = False - self._check_circuit(res) - finally: - pennylane.operation.Operator.do_check_domain = True - - del self.queue - del self.obs_queue - - # no output conversion - self.output_conversion = lambda x: x - - # no Variables, self.variable_deps is empty! - # generate the DAG - self.circuit = pennylane.circuit_graph.CircuitGraph( - self.ops, self.variable_deps, self.device.wires - ) - - # check for operations that cannot affect the output - if self.kwargs.get("vis_check", False): - invisible = self.circuit.invisible_operations() - if invisible: - raise QuantumFunctionError( - "The operations {} cannot affect the circuit output.".format(invisible) - ) diff --git a/pennylane/qnodes/qubit.py b/pennylane/qnodes/qubit.py deleted file mode 100644 index eaa421173d8..00000000000 --- a/pennylane/qnodes/qubit.py +++ /dev/null @@ -1,434 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Qubit parameter shift quantum node. - -Provides analytic differentiation for all one-parameter gates where the generator -only has two unique eigenvalues; this includes one-parameter single-qubit gates. -""" -import itertools -import copy - -import numpy as np -from scipy import linalg - -import pennylane as qml -from pennylane.measure import var -from pennylane.utils import expand - -from pennylane.operation import Observable, ObservableReturnTypes - -from .base import QuantumFunctionError -from .jacobian import JacobianQNode - - -class QubitQNode(JacobianQNode): - """Quantum node for qubit parameter-shift analytic differentiation method.""" - - def _best_method(self, idx): - """Determine the correct partial derivative computation method for a free parameter. - - Use the parameter-shift analytic method iff every gate that depends on the parameter supports it. - If not, use the finite difference method only. - - Note that if even one dependent Operation does not support differentiation, - we cannot differentiate with respect to this parameter at all. - - Args: - idx (int): free parameter index - - Returns: - str: partial derivative method to be used - """ - # operations that depend on this free parameter - ops = [d.op for d in self.variable_deps[idx]] - - # Observables in the circuit - # (the topological order is the queue order) - observables = self.circuit.observables_in_order - - # an empty list to store the 'best' partial derivative method - # for each operator/observable pair - best = np.empty((len(ops), len(observables)), dtype=object) - - # find the best supported partial derivative method for each operator - for k_op, op in enumerate(ops): - if op.grad_method is None: - # one nondifferentiable item makes the whole nondifferentiable - op.use_method = None - continue - - # loop over all observables - for k_ob, ob in enumerate(observables): - # get the set of operations betweens the - # operation and the observable - S = self.circuit.nodes_between(op, ob) - - # If there is no path between them, p.d. is zero - # Otherwise, use finite differences - best[k_op, k_ob] = "0" if not S else op.grad_method - - if all(k == "0" for k in best[k_op, :]): - # one nondifferentiable item makes the whole nondifferentiable - op.use_method = "0" - elif "F" in best[k_op, :]: - # one non-analytic item makes the whole numeric - op.use_method = "F" - else: - op.use_method = "A" - - # if all ops that depend on the free parameter have a best method - # of "0", then we can skip the partial derivative altogether - if all(o.use_method == "0" for o in ops): - return "0" - - # one nondifferentiable item makes the whole nondifferentiable - if any(o.use_method is None for o in ops): - return None - - # one non-analytic item makes the whole numeric - if any(o.use_method == "F" for o in ops): - return "F" - - return "A" - - def _pd_analytic(self, idx, args, kwargs, **options): - """Partial derivative of the node using the analytic parameter shift method. - Args: - idx (int): flattened index of the parameter wrt. which the p.d. is computed - args (array[float]): flattened positional arguments at which to evaluate the p.d. - kwargs (dict[str, Any]): auxiliary arguments - - Returns: - array[float]: partial derivative of the node - """ - n = self.num_variables - pd = 0.0 - # find the Operators in which the free parameter appears, use the product rule - for op, p_idx in self.variable_deps[idx]: - - # We temporarily edit the Operator such that parameter p_idx is replaced by a new one, - # which we can modify without affecting other Operators depending on the original. - orig = op.data[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.data[p_idx] = temp_var - - param_shift = op.get_parameter_shift(p_idx) - - for multiplier, a, shift in param_shift: - - # shifted parameter values - shift_p = np.r_[args, a * args[idx] + shift] - - # evaluate the circuit at point with shifted parameter values - y = np.asarray(self.evaluate(shift_p, kwargs)) - - # add the contribution to the partial derivative - pd += multiplier * y - - # restore the original parameter - op.data[p_idx] = orig - - return pd - - def _pd_analytic_var(self, idx, args, kwargs, **options): - """Partial derivative of the variance of an observable using the parameter-shift method. - - Args: - idx (int): flattened index of the parameter wrt. which the p.d. is computed - args (array[float]): flattened positional arguments at which to evaluate the p.d. - kwargs (dict[str, Any]): auxiliary arguments - - Returns: - array[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 ObservableReturnTypes.Variance for e in self.circuit.observables - ] - var_observables = [ - e for e in self.circuit.observables if e.return_type == ObservableReturnTypes.Variance - ] - - # first, replace each var(A) with - new_observables = [] - for e in var_observables: - # need to calculate d/dp - w = e.wires - - if e.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.data[0] # Hermitian matrix - # if not np.allclose(A @ A, np.identity(A.shape[0])): - new = qml.expval(qml.Hermitian(A @ A, w, do_queue=False)) - else: - # involutory, A^2 = I - # For involutory observables (A^2 = I) we have d/dp = 0 - new = qml.expval(qml.Hermitian(np.identity(2 ** len(w)), w, do_queue=False)) - - # replace the var(A) observable with - self.circuit.update_node(e, new) - new_observables.append(new) - - # calculate the analytic derivatives of the observables - pdA2 = self._pd_analytic(idx, args, kwargs) - - # restore the original observables, but convert their return types to expectation - for e, new in zip(var_observables, new_observables): - self.circuit.update_node(new, e) - e.return_type = ObservableReturnTypes.Expectation - - # evaluate - evA = np.asarray(self.evaluate(args, kwargs)) - - # evaluate the analytic derivative of - pdA = self._pd_analytic(idx, args, kwargs) - - # restore return types - for e in var_observables: - e.return_type = ObservableReturnTypes.Variance - - # return d(var(A))/dp = d/dp -2 * * d/dp for the variances, - # d/dp for plain expectations - return np.where(where_var, pdA2 - 2 * evA * pdA, pdA) - - def _construct_metric_tensor(self, *, diag_approx=False): - """Construct metric tensor subcircuits for qubit circuits. - - Constructs a set of quantum circuits for computing a block-diagonal approximation of the - Fubini-Study metric tensor on the parameter space of the variational circuit represented - by the QNode, using the Quantum Geometric Tensor. - - 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: - diag_approx (bool): iff True, use the diagonal approximation - - Raises: - QuantumFunctionError: if a metric tensor cannot be generated because no generator - was defined - - """ - # pylint: disable=too-many-statements, too-many-branches - - self._metric_tensor_subcircuits = {} - for queue, curr_ops, param_idx, _ in self.circuit.iterate_parametrized_layers(): - obs = [] - scale = [] - - Ki_matrices = [] - KiKj_matrices = [] - Ki_ev = [] - KiKj_ev = [] - V = None - - # for each operation in the layer, get the generator and convert it to a variance - for n, op in enumerate(curr_ops): - gen, s = op.generator - w = op.wires - # get the wire's indices on the device - wire_indices = self.device.wires.indices(w) - - 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 = var(qml.Hermitian(gen, w, do_queue=False)) - - if not diag_approx: - Ki_matrices.append((n, expand(gen, wire_indices, self.num_wires))) - - elif issubclass(gen, Observable): - # generator is an existing PennyLane operation - variance = var(gen(w, do_queue=False)) - - if not diag_approx: - if issubclass(gen, qml.PauliX): - mat = np.array([[0, 1], [1, 0]]) - elif issubclass(gen, qml.PauliY): - mat = np.array([[0, -1j], [1j, 0]]) - elif issubclass(gen, qml.PauliZ): - mat = np.array([[1, 0], [0, -1]]) - - Ki_matrices.append((n, expand(mat, wire_indices, 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[param_idx] = { - "queue": queue, - "observable": obs, - "Ki_expectations": Ki_ev, - "KiKj_expectations": KiKj_ev, - "eigenbasis_matrix": V, - "result": None, - "scale": scale, - } - - def metric_tensor(self, args, kwargs=None, *, diag_approx=False, only_construct=False): - """Evaluate the value of the metric tensor. - - Args: - args (tuple[Any]): positional (differentiable) arguments - kwargs (dict[str, Any]): auxiliary arguments - diag_approx (bool): iff True, use the diagonal approximation - only_construct (bool): Iff True, construct the circuits used for computing - the metric tensor but do not execute them, and return None. - - Returns: - array[float]: metric tensor - """ - # pylint:disable=too-many-branches - kwargs = kwargs or {} - kwargs = self._default_args(kwargs) - - if self.circuit is None or self.mutable: - # construct the circuit - self._construct(args, kwargs) - - if self._metric_tensor_subcircuits is None: - self._construct_metric_tensor(diag_approx=diag_approx) - - if only_construct: - return None - - # temporarily store the parameter values in the Variable class - self._set_variables(args, kwargs) - - tensor = np.zeros([self.num_variables, self.num_variables]) - - # execute 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.QubitUnitary(V, wires=list(range(self.num_wires)), do_queue=False) - - if isinstance(self.device, qml.QubitDevice): - ops = circuit["queue"] + [unitary_op] + [qml.expval(qml.PauliZ(0))] - circuit_graph = qml.CircuitGraph(ops, self.variable_deps, self.device.wires) - self.device.execute(circuit_graph) - else: - self.device.execute( - circuit["queue"] + [unitary_op], - [ - qml.expval(qml.PauliZ(wire)) - for wire in list(range(self.device.num_wires)) - ], - ) - - probs = list(self.device.probability()) - - 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 - if isinstance(self.device, qml.QubitDevice): - circuit_graph = qml.CircuitGraph( - circuit["queue"] + circuit["observable"], - self.variable_deps, - self.device.wires, - ) - variances = self.device.execute(circuit_graph) - else: - variances = self.device.execute(circuit["queue"], circuit["observable"]) - - circuit["result"] = s ** 2 * variances - tensor[np.array(params), np.array(params)] = circuit["result"] - - return tensor diff --git a/pennylane/qnodes/rev.py b/pennylane/qnodes/rev.py deleted file mode 100644 index 12094df6fd0..00000000000 --- a/pennylane/qnodes/rev.py +++ /dev/null @@ -1,178 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -ReversibleQNode class. -""" -from copy import copy -from functools import reduce -from string import ascii_letters as ABC - -import numpy as np - -from .qubit import QubitQNode - -ABC_ARRAY = np.array(list(ABC)) - - -class ReversibleQNode(QubitQNode): - r"""Quantum node for reversible analytic differentiation method. - - This QNode enables a specific kind of differentiation method unique to simulators. - - The ReversibleQNode computes the analytic derivative of the circuit by using - the following strategy: - - Assume a circuit has a gate :math:`G(\theta)` that we want to differentiate. - Without loss of generality, we can write the circuit in the form three unitaries: :math:`UGV`. - Starting from the initial state :math:`\vert 0\rangle`, the quantum state is evolved up to the - "pre-measurement" state :math:`\vert\psi\rangle=UGV\vert 0\rangle`, which is saved - (this can be reused for each variable being differentiated). - - We then apply the unitary :math:`V^{-1}` to evolve this state backwards in time - until just after the gate :math:`G` (hence the name "reversible"). - The generator of :math:`G` is then applied as a gate, and we evolve forward using :math:`V` again. - At this stage, the state of the simulator is proportional to - :math:`\frac{\partial}{\partial\theta}\vert\psi\rangle`. - Some further post-processing of this gives the derivative - :math:`\frac{\partial}{\partial\theta} \langle \hat{O} \rangle` for any observable O. - - The reversible approach is similar to backpropagation, but trades off extra computation for - enhanced memory efficiency. Where backpropagation caches the state tensors at each step during - a forward pass, the reversible method only caches the final pre-measurement state. - - Compared to the parameter-shift rule, the reversible method can - be faster or slower, depending on the density and location of parametrized gates in a circuit - (circuits with higher density of parametrized gates near the end of the circuit will see a - benefit). - - Args: - func (callable): The *quantum function* of the QNode. - A Python function containing :class:`~.operation.Operation` constructor calls, - and returning a tuple of measured :class:`~.operation.Observable` instances. - device (~pennylane._device.Device): computational device to execute the function on - - Keyword Args: - mutable (bool): whether the QNode is mutable or not - use_native_type (bool): If True, return the result in whatever type the device uses - internally, otherwise convert it into array[float]. Default: True. - """ - - def __init__(self, func, device, mutable=True, **kwargs): - - # TODO: update when all capabilities keys changed to "supports_reversible_diff" - supports_reverse = device.capabilities().get( - "supports_reversible_diff", False - ) or device.capabilities().get("reversible_diff", False) - if not supports_reverse: - raise ValueError( - "Reversible differentiation method not supported on {}".format(device.short_name) - ) - super().__init__(func, device, mutable=mutable, **kwargs) - - def _pd_analytic(self, idx, args, kwargs, **options): - """Partial derivative of the node using the reversible method. - - Args: - idx (int): flattened index of the parameter wrt. which the p.d. is computed - args (array[float]): flattened positional arguments at which to evaluate the p.d. - kwargs (dict[str, Any]): auxiliary arguments - - Returns: - array[float]: partial derivative of the node - """ - # pylint: disable=protected-access - - # TODO: cache these so they aren't created on each call from the same `jacobian` - self.evaluate(args, kwargs) - state = self.device._pre_rotated_state # only works if forward pass has occured - ops = self.circuit.operations_in_order - obs = self.circuit.observables_in_order - - pd = 0.0 - # find the Operators in which the free parameter appears, use the product rule - for op, p_idx in self.variable_deps[idx]: - - # create a new circuit which rewinds the pre-measurement state to just after `op`, - # applies the generator of `op`, and then plays forward back to - # pre-measurement step - wires = op.wires - op_idx = ops.index(op) - - # TODO: likely better to use circuitgraph to determine minimally necessary ops - between_ops = ops[op_idx + 1 :] - if op.name == "Rot": - decomp = op.decomposition(*op.parameters, wires=wires) - generator, multiplier = decomp[p_idx].generator - between_ops = decomp[p_idx + 1 :] + between_ops - else: - generator, multiplier = op.generator - - # CRX, CRY, CRZ ops have a non-unitary matrix as generator - # TODO: these can be supported by multiplying ``state`` directly by these generators within this function - # (or by allowing non-unitary matrix multiplies in the simulator backends) - if op.name in ["PhaseShift", "CRX", "CRY", "CRZ"]: - raise ValueError( - "The {} gate is not currently supported with the " - "reversible gradient method.".format(op.name) - ) - generator = generator(wires) - diff_circuit = [copy(op).inv() for op in between_ops[::-1]] + [generator] + between_ops - - # set the simulator state to be the pre-measurement state - self.device._state = state - - # evolve the pre-measurement state under this new circuit - self.device.apply(diff_circuit) - dstate = self.device._pre_rotated_state # TODO: this will only work for QubitDevices - - # compute matrix element for each observable O - matrix_elems = self.device._asarray( - [self._matrix_elem(dstate, ob, state) for ob in obs] - # TODO: if all observables act on same number of wires, could - # do all at once with einsum - ) - - # post-process to get partial derivative contribution from this op - multiplier *= op.data[p_idx].mult # possible scalar multiplier - pd += 2 * multiplier * self.device._imag(matrix_elems) - - # reset state back to pre-measurement value - self.device._pre_rotated_state = state - return pd - - def _matrix_elem(self, vec1, obs, vec2): - """Computes the matrix element of observable ``obs`` between the two vectors - ``vec1`` and ``vec2``, i.e., . - Unmeasured wires are contracted, and a scalar is returned.""" - # pylint: disable=protected-access - - mat = self.device._reshape(obs.matrix, [2] * len(obs.wires) * 2) - wires = obs.wires - - vec1_indices = ABC[: self.num_wires] - obs_in_indices = "".join(ABC_ARRAY[wires.tolist()].tolist()) - obs_out_indices = ABC[self.num_wires : self.num_wires + len(wires)] - obs_indices = "".join([obs_in_indices, obs_out_indices]) - vec2_indices = reduce( - lambda old_string, idx_pair: old_string.replace(idx_pair[0], idx_pair[1]), - zip(obs_in_indices, obs_out_indices), - vec1_indices, - ) - - einsum_str = "{vec1_indices},{obs_indices},{vec2_indices}->".format( - vec1_indices=vec1_indices, - obs_indices=obs_indices, - vec2_indices=vec2_indices, - ) - return self.device._einsum(einsum_str, self.device._conj(vec1), mat, vec2) diff --git a/pennylane/tape/queuing.py b/pennylane/queuing.py similarity index 76% rename from pennylane/tape/queuing.py rename to pennylane/queuing.py index 3657257e1f4..e47a358c353 100644 --- a/pennylane/tape/queuing.py +++ b/pennylane/queuing.py @@ -246,3 +246,79 @@ def _get_info(self, obj): def queue(self): """Returns a list of objects in the annotated queue""" return list(self._queue.keys()) + + +class OperationRecorder(QuantumTape): + """A template and quantum function inspector, + allowing easy introspection of operators that have been + applied without requiring a QNode. + + **Example**: + + The OperationRecorder is a context manager. Executing templates + or quantum functions stores applied operators in the + recorder, which can then be printed. + + >>> weights = qml.init.strong_ent_layers_normal(n_layers=1, n_wires=2) + >>> + >>> with OperationRecorder() as rec: + >>> qml.templates.layers.StronglyEntanglingLayers(*weights, wires=[0, 1]) + >>> + >>> print(rec) + Operations + ========== + Rot(-0.10832656163640327, 0.14429091013664083, -0.010835826725765343, wires=[0]) + Rot(-0.11254523669444501, 0.0947222564914006, -0.09139600968423377, wires=[1]) + CNOT(wires=[0, 1]) + CNOT(wires=[1, 0]) + + Alternatively, the :attr:`~.OperationRecorder.queue` attribute can be used + to directly access the applied :class:`~.Operation` and :class:`~.Observable` + objects. + + Attributes: + queue (List[Operator]): list of operators applied within + the OperatorRecorder context, includes operations and observables + operations (List[Operation]): list of operations applied within + the OperatorRecorder context + observables (List[Observable]): list of observables applied within + the OperatorRecorder context + """ + + def __init__(self): + super().__init__() + self.ops = None + self.obs = None + + def _process_queue(self): + super()._process_queue() + + for obj, info in self._queue.items(): + QueuingContext.append(obj, **info) + + # remove the operation recorder from the queuing + # context + QueuingContext.remove(self) + + new_tape = self.expand(depth=5, stop_at=lambda obj: not isinstance(obj, QuantumTape)) + self.ops = new_tape.operations + self.obs = new_tape.observables + + def __str__(self): + output = "" + output += "Operations\n" + output += "==========\n" + for op in self.ops: + output += repr(op) + "\n" + + output += "\n" + output += "Observables\n" + output += "==========\n" + for op in self.obs: + output += repr(op) + "\n" + + return output + + @property + def queue(self): + return self.ops + self.obs diff --git a/pennylane/tape/__init__.py b/pennylane/tape/__init__.py index de23d8fb0b3..ee2b309c1de 100644 --- a/pennylane/tape/__init__.py +++ b/pennylane/tape/__init__.py @@ -1,239 +1,22 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This subpackage contains various quantum tapes, which track, queue, -validate, execute, and differentiate quantum circuits. -""" -import contextlib -import inspect -import functools -from unittest import mock -import warnings - -import pennylane as qml - -from . import measure -from . import transforms -from .circuit_graph import TapeCircuitGraph -from .queuing import AnnotatedQueue, Queue, QueuingContext -from .measure import MeasurementProcess, state, density_matrix -from .qnode import QNode, qnode, draw -from .tapes import ( - QuantumTape, - JacobianTape, - QubitParamShiftTape, - CVParamShiftTape, - ReversibleTape, -) - - -_mock_stack = [] - - -class TapeOperationRecorder(QuantumTape): - """A template and quantum function inspector, - allowing easy introspection of operators that have been - applied without requiring a QNode. - - **Example**: - - The OperationRecorder is a context manager. Executing templates - or quantum functions stores applied operators in the - recorder, which can then be printed. - - >>> weights = qml.init.strong_ent_layers_normal(n_layers=1, n_wires=2) - >>> - >>> with OperationRecorder() as rec: - >>> qml.templates.layers.StronglyEntanglingLayers(*weights, wires=[0, 1]) - >>> - >>> print(rec) - Operations - ========== - Rot(-0.10832656163640327, 0.14429091013664083, -0.010835826725765343, wires=[0]) - Rot(-0.11254523669444501, 0.0947222564914006, -0.09139600968423377, wires=[1]) - CNOT(wires=[0, 1]) - CNOT(wires=[1, 0]) - - Alternatively, the :attr:`~.OperationRecorder.queue` attribute can be used - to directly access the applied :class:`~.Operation` and :class:`~.Observable` - objects. - - Attributes: - queue (List[Operator]): list of operators applied within - the OperatorRecorder context, includes operations and observables - operations (List[Operation]): list of operations applied within - the OperatorRecorder context - observables (List[Observable]): list of observables applied within - the OperatorRecorder context - """ - - def __init__(self): - super().__init__() - self.ops = None - self.obs = None - - def _process_queue(self): - super()._process_queue() - - for obj, info in self._queue.items(): - QueuingContext.append(obj, **info) - - # remove the operation recorder from the queuing - # context - QueuingContext.remove(self) - - new_tape = self.expand(depth=5, stop_at=lambda obj: not isinstance(obj, QuantumTape)) - self.ops = new_tape.operations - self.obs = new_tape.observables - - def __str__(self): - output = "" - output += "Operations\n" - output += "==========\n" - for op in self.ops: - output += repr(op) + "\n" - - output += "\n" - output += "Observables\n" - output += "==========\n" - for op in self.obs: - output += repr(op) + "\n" - - return output - - @property - def queue(self): - return self.ops + self.obs - - -def TapeTemplateDecorator(func): - """Register a quantum template with PennyLane. - - This decorator wraps the given function and makes it return a list of all queued Operations. - - **Example:** - - When defining a :doc:`template `, simply decorate - the template function with this decorator. - - .. code-block:: python3 - - @qml.template - def bell_state_preparation(wires): - qml.Hadamard(wires=wires[0]) - qml.CNOT(wires=wires) - - This registers the template with PennyLane, making it compatible with - functions that act on templates, such as :func:`pennylane.inv`: - - .. code-block:: python3 - - dev = qml.device('default.qubit', wires=2) - - @qml.qnode(dev) - def circuit(): - qml.inv(bell_state_preparation(wires=[0, 1])) - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - Args: - func (callable): A template function - - Returns: - callable: The wrapper function - """ - - @functools.wraps(func) - def wrapper(*args, **kwargs): - with TapeOperationRecorder() as rec: - func(*args, **kwargs) - - return rec.queue - - return wrapper - - -def enable_tape(): - """Enable tape mode. - - Tape mode is an experimental new mode of PennyLane. QNodes created in tape mode have support for - in-QNode classical processing, differentiable quantum decompositions, returning the quantum - state, less restrictive QNode signatures, and various other improvements. - - For more details on tape mode, see :mod:`pennylane.tape`. - - **Example** - - Simply call this function at the beginning of your script or session. - - >>> qml.enable_tape() - - All subsequent QNodes will be created using tape mode, and can take - advantage of the various tape mode features: - - >>> dev = qml.device("default.qubit", wires=1) - >>> @qml.qnode(dev) - ... def circuit(x, y): - ... qml.RX(np.sin(x) * y, wires=0) - ... return qml.expval(qml.PauliZ(0)) - >>> print(circuit(0.5, 0.1)) - 0.9988509758748578 - >>> qml.grad(circuit)(0.5, 0.1) - (array(-0.00420574), array(-0.02297608))) - >>> type(circuit) - pennylane.tape.qnode.QNode - - Tape mode can be disabled by calling :func:`~.disable_tape`. - """ - if _mock_stack: - return - - mocks = [ - mock.patch("pennylane.qnode", qnode), - mock.patch("pennylane.QNode", QNode), - mock.patch("pennylane.expval", measure.expval), - mock.patch("pennylane.var", measure.var), - mock.patch("pennylane.probs", measure.probs), - mock.patch("pennylane.sample", measure.sample), - mock.patch("pennylane._queuing.OperationRecorder", TapeOperationRecorder), - mock.patch("pennylane.template", TapeTemplateDecorator), - ] - - with contextlib.ExitStack() as stack: - for m in mocks: - stack.enter_context(m) - - _mock_stack.append(stack.pop_all()) - - -def disable_tape(): - """Disable tape mode. - - This function may be called at any time after :func:`~.enable_tape` has been executed - in order to disable tape mode. - - Tape mode is an experimental new mode of PennyLane. QNodes created in tape mode have support for - in-QNode classical processing, differentiable quantum decompositions, returning the quantum - state, less restrictive QNode signatures, and various other improvements. - - For more details on tape mode, see :mod:`~.tape`. - """ - if not _mock_stack: - warnings.warn("Tape mode is not currently enabled.", UserWarning) - else: - _mock_stack.pop().close() - - -def tape_mode_active(): - """Returns whether tape mode is enabled.""" - return inspect.isclass(qml.QNode) and issubclass(qml.QNode, qml.tape.QNode) +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This subpackage contains various quantum tapes, which track, queue, +validate, execute, and differentiate quantum circuits. +""" +from .tape import QuantumTape +from .jacobian_tape import JacobianTape +from .cv_param_shift import CVParamShiftTape +from .qubit_param_shift import QubitParamShiftTape +from .reversible import ReversibleTape diff --git a/pennylane/tape/circuit_graph.py b/pennylane/tape/circuit_graph.py deleted file mode 100644 index b1c90ab9833..00000000000 --- a/pennylane/tape/circuit_graph.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the CircuitGraph class which is used to generate a DAG (directed acyclic graph) -representation of a quantum circuit from an operator and observable queue. -""" -# pylint: disable=too-many-arguments -import networkx as nx - -from pennylane.circuit_graph import CircuitGraph, Layer - - -class TapeCircuitGraph(CircuitGraph): - """Represents a quantum circuit as a directed acyclic graph. - - This will eventually grow to replace the existing ``CircuitGraph``; for now, we simply inherit - from the current ``CircuitGraph``, and modify the instantiation so that it can be created via - the quantum tape. - - In this representation the :class:`~.Operator` instances are the nodes of the graph, - and each directed edge represent a subsystem (or a group of subsystems) on which the two - Operators act subsequently. This representation can describe the causal relationships - between arbitrary quantum channels and measurements, not just unitary gates. - - Args: - ops (Iterable[.Operator]): quantum operators constituting the circuit, in temporal order - obs (Iterable[.MeasurementProcess]): terminal measurements, in temporal order - wires (.Wires): The addressable wire register of the device that will be executing this graph - par_info (dict[int, dict[str, .Operation or int]]): Parameter information. Keys are - parameter indices (in the order they appear on the tape), and values are a - dictionary containing the corresponding operation and operation parameter index. - trainable_params (set[int]): A set containing the indices of parameters that support - differentiability. The indices provided match the order of appearence in the - quantum circuit. - """ - - def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): - self._operations = ops - self._observables = obs - self.par_info = par_info - self.trainable_params = trainable_params - - self._depth = None - - super().__init__(ops + obs, variable_deps={}, wires=wires) - - # For computing depth; want only a graph with the operations, not - # including the observables - self._operation_graph = None - - @property - def operations(self): - """Operations in the circuit.""" - return self._operations - - @property - def observables(self): - """Observables in the circuit.""" - return self._observables - - def update_node(self, old, new): - super().update_node(old, new) - self._operations = self.operations_in_order - self._observables = self.observables_in_order - - @property - def parametrized_layers(self): - """Identify the parametrized layer structure of the circuit. - - Returns: - list[Layer]: layers of the circuit - """ - # FIXME maybe layering should be greedier, for example [a0 b0 c1 d1] should layer as [a0 - # c1], [b0, d1] and not [a0], [b0 c1], [d1] keep track of the current layer - current = Layer([], []) - layers = [current] - - for idx, info in self.par_info.items(): - if idx in self.trainable_params: - op = info["op"] - - # get all predecessor ops of the op - sub = self.ancestors((op,)) - - # check if any of the dependents are in the - # currently assembled layer - if set(current.ops) & sub: - # operator depends on current layer, start a new layer - current = Layer([], []) - layers.append(current) - - # store the parameters and ops indices for the layer - current.ops.append(op) - current.param_inds.append(idx) - - return layers - - def get_depth(self): - """Depth of the quantum circuit (longest path in the DAG).""" - # If there are no operations in the circuit, the depth is 0 - if not self.operations: - self._depth = 0 - - # If there are operations but depth is uncomputed, compute the truncated graph - # with only the operations, and return the longest path + 1 (since the path is - # expressed in terms of edges, and we want it in terms of nodes). - if self._depth is None and self.operations: - if self._operation_graph is None: - self._operation_graph = self.graph.subgraph(self.operations) - self._depth = nx.dag_longest_path_length(self._operation_graph) + 1 - - return self._depth - - def has_path(self, a, b): - """Checks if a path exists between the two given nodes. - - Args: - a (Operator): initial node - b (Operator): final node - - Returns: - bool: returns ``True`` if a path exists - """ - return nx.has_path(self._graph, a, b) diff --git a/pennylane/tape/tapes/cv_param_shift.py b/pennylane/tape/cv_param_shift.py similarity index 97% rename from pennylane/tape/tapes/cv_param_shift.py rename to pennylane/tape/cv_param_shift.py index 809cceed8b3..0f0da911aab 100644 --- a/pennylane/tape/tapes/cv_param_shift.py +++ b/pennylane/tape/cv_param_shift.py @@ -24,8 +24,8 @@ import numpy as np import pennylane as qml -from pennylane.tape.measure import MeasurementProcess -from pennylane.tape.tapes.tape import QuantumTape +from pennylane.measure import MeasurementProcess +from pennylane.tape import QuantumTape from .qubit_param_shift import QubitParamShiftTape diff --git a/pennylane/tape/interfaces/__init__.py b/pennylane/tape/interfaces/__init__.py deleted file mode 100644 index ae9ca6d41d1..00000000000 --- a/pennylane/tape/interfaces/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This subpackage defines functions convert quantum tapes to interface with different machine -learning libraries. -""" diff --git a/pennylane/tape/interfaces/autograd.py b/pennylane/tape/interfaces/autograd.py deleted file mode 100644 index b82673b5270..00000000000 --- a/pennylane/tape/interfaces/autograd.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the mixin interface class for creating differentiable quantum tapes with -Autograd. -""" -# pylint: disable=protected-access -import autograd.extend -import autograd.builtins -from autograd.numpy.numpy_boxes import ArrayBox - -from pennylane import numpy as np -from pennylane.tape.queuing import AnnotatedQueue - - -class AutogradInterface(AnnotatedQueue): - """Mixin class for applying an autograd interface to a :class:`~.JacobianTape`. - - Autograd-compatible quantum tape classes can be created via subclassing: - - .. code-block:: python - - class MyAutogradQuantumTape(AutogradInterface, JacobianTape): - - Alternatively, the autograd interface can be dynamically applied to existing - quantum tapes via the :meth:`~.apply` class method. This modifies the - tape **in place**. - - Once created, the autograd interface can be used to perform quantum-classical - differentiable programming. - - .. note:: - - If using a device that supports native autograd computation and backpropagation, such as - :class:`~.DefaultQubitAutograd`, the Autograd interface **does not need to be applied**. It - is only applied to tapes executed on non-Autograd compatible devices. - - **Example** - - Once an autograd quantum tape has been created, it can be differentiated using autograd: - - .. code-block:: python - - tape = AutogradInterface.apply(JacobianTape()) - - with tape: - qml.Rot(0, 0, 0, wires=0) - expval(qml.PauliX(0)) - - def cost_fn(x, y, z, device): - tape.set_parameters([x, y ** 2, y * np.sin(z)], trainable_only=False) - return tape.execute(device=device) - - >>> x = np.array(0.1, requires_grad=False) - >>> y = np.array(0.2, requires_grad=True) - >>> z = np.array(0.3, requires_grad=True) - >>> dev = qml.device("default.qubit", wires=2) - >>> cost_fn(x, y, z, device=dev) - [0.03991951] - >>> jac_fn = qml.jacobian(cost_fn) - >>> jac_fn(x, y, z, device=dev) - [[ 0.39828408, -0.00045133]] - """ - - # pylint: disable=attribute-defined-outside-init - dtype = np.float64 - - @property - def interface(self): # pylint: disable=missing-function-docstring - return "autograd" - - def _update_trainable_params(self): - """Set the trainable parameters. - - Unlike in :class:`~.JacobianTape`, we also set the private attribute - ``self._all_parameter_values``. - """ - params = self.get_parameters(trainable_only=False, return_arraybox=True) - trainable_params = set() - - for idx, p in enumerate(params): - if getattr(p, "requires_grad", False) or isinstance(p, ArrayBox): - trainable_params.add(idx) - - self.trainable_params = trainable_params - self._all_parameter_values = params - - def get_parameters(self, trainable_only=True, return_arraybox=False): - """Return the parameters incident on the tape operations. - - The returned parameters are provided in order of appearance - on the tape. By default, the returned parameters are wrapped in - an ``autograd.builtins.list`` container. - - Args: - trainable_only (bool): if True, returns only trainable parameters - return_arraybox (bool): if True, the returned parameters are not - wrapped in an ``autograd.builtins.list`` container - Returns: - autograd.builtins.list or list: the corresponding parameter values - - **Example** - - .. code-block:: python - - with JacobianTape() as tape: - qml.RX(0.432, wires=0) - qml.RY(0.543, wires=0) - qml.CNOT(wires=[0, 'a']) - qml.RX(0.133, wires='a') - expval(qml.PauliZ(wires=[0])) - - By default, all parameters are trainable and will be returned: - - >>> tape.get_parameters() - [0.432, 0.543, 0.133] - - Setting the trainable parameter indices will result in only the specified - parameters being returned: - - >>> tape.trainable_params = {1} # set the second parameter as free - >>> tape.get_parameters() - [0.543] - - The ``trainable_only`` argument can be set to ``False`` to instead return - all parameters: - - >>> tape.get_parameters(trainable_only=False) - [0.432, 0.543, 0.133] - """ - params = [] - iterator = self.trainable_params if trainable_only else self._par_info - - for p_idx in iterator: - op = self._par_info[p_idx]["op"] - op_idx = self._par_info[p_idx]["p_idx"] - params.append(op.data[op_idx]) - - return params if return_arraybox else autograd.builtins.list(params) - - @autograd.extend.primitive - def _execute(self, params, device): - # unwrap all NumPy scalar arrays to Python literals - params = [p.item() if p.shape == tuple() else p for p in params] - params = autograd.builtins.tuple(params) - - # unwrap constant parameters - self._all_params_unwrapped = [ - p.numpy() if isinstance(p, np.tensor) else p for p in self._all_parameter_values - ] - - # evaluate the tape - self.set_parameters(self._all_params_unwrapped, trainable_only=False) - res = self.execute_device(params, device=device) - self.set_parameters(self._all_parameter_values, trainable_only=False) - - if self.is_sampled: - return res - - if res.dtype == np.dtype("object"): - return np.hstack(res) - - requires_grad = False - - if self.trainable_params: - requires_grad = True - - return np.array(res, requires_grad=requires_grad) - - @staticmethod - def vjp(ans, self, params, device): # pylint: disable=unused-argument - """Returns the vector-Jacobian product operator for the quantum tape. - The returned function takes the arguments as :meth:`~.JacobianTape.execute`. - - Args: - ans (array): the result of the tape execution - self (.AutogradQuantumTape): the tape instance - params (list[Any]): the quantum tape operation parameters - device (.Device): a PennyLane device that can execute quantum - operations and return measurement statistics - - Returns: - function: this function accepts the backpropagation - gradient output vector, and computes the vector-Jacobian product - """ - - def gradient_product(g): - # In autograd, the forward pass is always performed prior to the backwards - # pass, so we do not need to re-unwrap the parameters. - self.set_parameters(self._all_params_unwrapped, trainable_only=False) - jac = self.jacobian(device, params=params, **self.jacobian_options) - self.set_parameters(self._all_parameter_values, trainable_only=False) - vjp = g.flatten() @ jac - return vjp - - return gradient_product - - @classmethod - def apply(cls, tape): - """Apply the autograd interface to an existing tape in-place. - - Args: - tape (.JacobianTape): a quantum tape to apply the Autograd interface to - - **Example** - - >>> with JacobianTape() as tape: - ... qml.RX(0.5, wires=0) - ... expval(qml.PauliZ(0)) - >>> AutogradInterface.apply(tape) - >>> tape - , params=1> - """ - tape_class = getattr(tape, "__bare__", tape.__class__) - tape.__bare__ = tape_class - tape.__class__ = type("AutogradQuantumTape", (cls, tape_class), {}) - tape._update_trainable_params() - return tape - - -autograd.extend.defvjp(AutogradInterface._execute, AutogradInterface.vjp, argnums=[1]) diff --git a/pennylane/tape/interfaces/tf.py b/pennylane/tape/interfaces/tf.py deleted file mode 100644 index e121f65268a..00000000000 --- a/pennylane/tape/interfaces/tf.py +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the mixin interface class for creating differentiable quantum tapes with -TensorFlow. -""" -# pylint: disable=protected-access, attribute-defined-outside-init -import numpy as np -import tensorflow as tf - -try: - from tensorflow.python.eager.tape import should_record_backprop -except ImportError: - from tensorflow.python.eager.tape import should_record as should_record_backprop - - -from pennylane.tape.queuing import AnnotatedQueue - - -class TFInterface(AnnotatedQueue): - """Mixin class for applying an TensorFlow interface to a :class:`~.JacobianTape`. - - TensorFlow-compatible quantum tape classes can be created via subclassing: - - .. code-block:: python - - class MyTFQuantumTape(TFInterface, JacobianTape): - - Alternatively, the TensorFlow interface can be dynamically applied to existing - quantum tapes via the :meth:`~.apply` class method. This modifies the - tape **in place**. - - Once created, the TensorFlow interface can be used to perform quantum-classical - differentiable programming. - - .. note:: - - If using a device that supports native TensorFlow computation and backpropagation, such as - :class:`~.DefaultQubitTF`, the TensorFlow interface **does not need to be applied**. It is - only applied to tapes executed on non-TensorFlow compatible devices. - - **Example** - - Once a TensorFlow quantum tape has been created, it can be differentiated using the gradient tape: - - .. code-block:: python - - dev = qml.device("default.qubit", wires=1) - p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float64) - - with tf.GradientTape() as tape: - with TFInterface.apply(JacobianTape()) as qtape: - qml.Rot(p[0], p[1] ** 2 + p[0] * p[2], p[1] * tf.sin(p[2]), wires=0) - expval(qml.PauliX(0)) - - result = qtape.execute(dev) - - >>> print(result) - tf.Tensor([0.06982072], shape=(1,), dtype=float64) - >>> grad = tape.gradient(result, p) - >>> print(grad) - tf.Tensor([0.29874274 0.39710271 0.09958091], shape=(3,), dtype=float64) - - The TensorFlow interface defaults to ``tf.float64`` output. This can be modified by - providing the ``dtype`` argument when applying the interface: - - >>> p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float32) - >>> with tf.GradientTape() as tape: - ... TFInterface.apply(qtape, dtype=tf.float32) # reusing the previous qtape - ... result = qtape.execute(dev) - >>> print(result) - tf.Tensor([0.06982072], shape=(1,), dtype=float32) - >>> grad = tape.gradient(result, p) - >>> print(grad) - tf.Tensor([0.2895088 0.38464668 0.09645163], shape=(3,), dtype=float32) - """ - - dtype = tf.float64 - - @property - def interface(self): # pylint: disable=missing-function-docstring - return "tf" - - def _update_trainable_params(self): - params = self.get_parameters(trainable_only=False) - - trainable_params = set() - - for idx, p in enumerate(params): - # Determine which input tensors/Variables are being recorded for backpropagation. - # The function should_record_backprop, documented here: - # https://github.com/tensorflow/tensorflow/tree/master/tensorflow/python/eager/tape.py#L167 - # accepts lists of *Tensors* (not Variables), returning True if all are being watched by one or more - # existing gradient tapes, False if not. - - if isinstance(p, (tf.Variable, tf.Tensor)) and should_record_backprop( - # we need to convert any Variable objects to Tensors here, otherwise - # should_record_backprop will raise an error - [tf.convert_to_tensor(p)] - ): - trainable_params.add(idx) - - self.trainable_params = trainable_params - - @staticmethod - def convert_to_numpy(tensors): - """Converts any TensorFlow tensors in a sequence to NumPy arrays. - - Args: - tensors (Sequence[Any, tf.Variable, tf.Tensor]): input sequence - - Returns: - list[Any, array]: list with all tensors converted to NumPy arrays - """ - return [i.numpy() if isinstance(i, (tf.Variable, tf.Tensor)) else i for i in tensors] - - @tf.custom_gradient - def _execute(self, params, **input_kwargs): - # unwrap free parameters - args = self.convert_to_numpy(params) - - # unwrap constant parameters - all_params = self.get_parameters(trainable_only=False) - all_params_unwrapped = self.convert_to_numpy(all_params) - - self.set_parameters(all_params_unwrapped, trainable_only=False) - res = self.execute_device(args, input_kwargs["device"]) - self.set_parameters(all_params, trainable_only=False) - - def grad(grad_output, **tfkwargs): - variables = tfkwargs.get("variables", None) - - self.set_parameters(all_params_unwrapped, trainable_only=False) - jacobian = self.jacobian(input_kwargs["device"], params=args, **self.jacobian_options) - self.set_parameters(all_params, trainable_only=False) - - jacobian = tf.constant(jacobian, dtype=self.dtype) - - # Reshape gradient output array as a 2D row-vector. - grad_output_row = tf.reshape(grad_output, [1, -1]) - - # Calculate the vector-Jacobian matrix product, and unstack the output. - grad_input = tf.matmul(grad_output_row, jacobian) - grad_input = tf.unstack(tf.reshape(grad_input, [-1])) - - if variables is not None: - return grad_input, variables - - return grad_input - - if self.is_sampled: - return res, grad - - if res.dtype == np.dtype("object"): - res = np.hstack(res) - - return tf.convert_to_tensor(res, dtype=self.dtype), grad - - @classmethod - def apply(cls, tape, dtype=tf.float64): - """Apply the TensorFlow interface to an existing tape in-place. - - Args: - tape (.JacobianTape): a quantum tape to apply the TF interface to - dtype (tf.dtype): the dtype that the returned quantum tape should - output - - **Example** - - >>> with JacobianTape() as tape: - ... qml.RX(0.5, wires=0) - ... expval(qml.PauliZ(0)) - >>> TFInterface.apply(tape) - >>> tape - , params=1> - """ - tape_class = getattr(tape, "__bare__", tape.__class__) - tape.__bare__ = tape_class - tape.__class__ = type("TFQuantumTape", (cls, tape_class), {"dtype": dtype}) - tape._update_trainable_params() - return tape diff --git a/pennylane/tape/interfaces/torch.py b/pennylane/tape/interfaces/torch.py deleted file mode 100644 index ffced188b6b..00000000000 --- a/pennylane/tape/interfaces/torch.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the mixin interface class for creating differentiable quantum tapes with -PyTorch. -""" -# pylint: disable=protected-access, attribute-defined-outside-init, arguments-differ, no-member, import-self -import numpy as np -import semantic_version -import torch - -from pennylane import QuantumFunctionError -from pennylane.interfaces.torch import args_to_numpy - -from pennylane.tape.queuing import AnnotatedQueue - -COMPLEX_SUPPORT = semantic_version.match(">=1.6.0", torch.__version__) - - -class _TorchInterface(torch.autograd.Function): - @staticmethod - def forward(ctx, input_kwargs, *input_): - """Implements the forward pass QNode evaluation""" - # detach all input tensors, convert to NumPy array - ctx.args = args_to_numpy(input_) - ctx.kwargs = input_kwargs - ctx.save_for_backward(*input_) - - tape = ctx.kwargs["tape"] - device = ctx.kwargs["device"] - - # unwrap constant parameters - ctx.all_params = tape.get_parameters(trainable_only=False) - ctx.all_params_unwrapped = args_to_numpy(ctx.all_params) - - # evaluate the tape - tape.set_parameters(ctx.all_params_unwrapped, trainable_only=False) - res = tape.execute_device(ctx.args, device) - tape.set_parameters(ctx.all_params, trainable_only=False) - - if hasattr(res, "numpy"): - res = res.numpy() - - # if any input tensor uses the GPU, the output should as well - for i in input_: - if isinstance(i, torch.Tensor): - if i.is_cuda: # pragma: no cover - cuda_device = i.get_device() - return torch.as_tensor( - torch.from_numpy(res), device=cuda_device, dtype=tape.dtype - ) - - if tape.is_sampled and not tape.all_sampled: - return tuple([torch.as_tensor(t, dtype=tape.dtype) for t in res]) - - if res.dtype == np.dtype("object"): - res = np.hstack(res) - - return torch.as_tensor(torch.from_numpy(res), dtype=tape.dtype) - - @staticmethod - def backward(ctx, grad_output): # pragma: no cover - """Implements the backwards pass QNode vector-Jacobian product""" - tape = ctx.kwargs["tape"] - device = ctx.kwargs["device"] - - tape.set_parameters(ctx.all_params_unwrapped, trainable_only=False) - jacobian = tape.jacobian(device, params=ctx.args, **tape.jacobian_options) - tape.set_parameters(ctx.all_params, trainable_only=False) - - jacobian = torch.as_tensor(jacobian, dtype=grad_output.dtype).to(grad_output) - - vjp = grad_output.view(1, -1) @ jacobian - grad_input_list = torch.unbind(vjp.flatten()) - grad_input = [] - - # match the type and device of the input tensors - for i, j in zip(grad_input_list, ctx.saved_tensors): - res = torch.as_tensor(i, dtype=tape.dtype) - if j.is_cuda: # pragma: no cover - cuda_device = j.get_device() - res = torch.as_tensor(res, device=cuda_device) - grad_input.append(res) - - return (None,) + tuple(grad_input) - - -class TorchInterface(AnnotatedQueue): - """Mixin class for applying an Torch interface to a :class:`~.JacobianTape`. - - Torch-compatible quantum tape classes can be created via subclassing: - - .. code-block:: python - - class MyTorchQuantumTape(TorchInterface, JacobianTape): - - Alternatively, the Torch interface can be dynamically applied to existing - quantum tapes via the :meth:`~.apply` class method. This modifies the - tape **in place**. - - Once created, the Torch interface can be used to perform quantum-classical - differentiable programming. - - **Example** - - Once a Torch quantum tape has been created, it can be evaluated and differentiated: - - .. code-block:: python - - dev = qml.device("default.qubit", wires=1) - p = torch.tensor([0.1, 0.2, 0.3], requires_grad=True) - - with TorchInterface.apply(JacobianTape()) as qtape: - qml.Rot(p[0], p[1] ** 2 + p[0] * p[2], p[1] * torch.sin(p[2]), wires=0) - expval(qml.PauliX(0)) - - result = qtape.execute(dev) - - >>> print(result) - tensor([0.0698], dtype=torch.float64, grad_fn=<_TorchInterfaceBackward>) - >>> result.backward() - >>> print(p.grad) - tensor([0.2987, 0.3971, 0.0988]) - - The Torch interface defaults to ``torch.float64`` output. This can be modified by - providing the ``dtype`` argument when applying the interface: - - >>> p = torch.tensor([0.1, 0.2, 0.3], requires_grad=True) - >>> with TorchInterface.apply(JacobianTape()) as qtape: - ... qml.Rot(p[0], p[1] ** 2 + p[0] * p[2], p[1] * torch.sin(p[2]), wires=0) - ... expval(qml.PauliX(0)) - >>> result = qtape.execute(dev) - >>> print(result) - tensor([0.0698], grad_fn=<_TorchInterfaceBackward>) - >>> print(result.dtype) - torch.float32 - >>> result.backward() - >>> print(p.grad) - tensor([0.2987, 0.3971, 0.0988]) - >>> print(p.grad.dtype) - torch.float32 - """ - - dtype = torch.float64 - - @property - def interface(self): # pylint: disable=missing-function-docstring - return "torch" - - def _update_trainable_params(self): - params = self.get_parameters(trainable_only=False) - - trainable_params = set() - - for idx, p in enumerate(params): - if getattr(p, "requires_grad", False): - trainable_params.add(idx) - - self.trainable_params = trainable_params - return params - - def _execute(self, params, **kwargs): - kwargs["tape"] = self - res = _TorchInterface.apply(kwargs, *params) - return res - - @classmethod - def apply(cls, tape, dtype=torch.float64): - """Apply the Torch interface to an existing tape in-place. - - Args: - tape (.JacobianTape): a quantum tape to apply the Torch interface to - dtype (torch.dtype): the dtype that the returned quantum tape should - output - - **Example** - - >>> with JacobianTape() as tape: - ... qml.RX(0.5, wires=0) - ... expval(qml.PauliZ(0)) - >>> TorchInterface.apply(tape) - >>> tape - , params=1> - """ - if (dtype is torch.complex64 or dtype is torch.complex128) and not COMPLEX_SUPPORT: - raise QuantumFunctionError( - "Version 1.6.0 or above of PyTorch must be installed for complex support, " - "which is required for quantum functions that return the state." - ) - - tape_class = getattr(tape, "__bare__", tape.__class__) - tape.__bare__ = tape_class - tape.__class__ = type("TorchQuantumTape", (cls, tape_class), {"dtype": dtype}) - tape._update_trainable_params() - return tape diff --git a/pennylane/tape/tapes/jacobian_tape.py b/pennylane/tape/jacobian_tape.py similarity index 99% rename from pennylane/tape/tapes/jacobian_tape.py rename to pennylane/tape/jacobian_tape.py index c2611f551e0..29c1b2f45e8 100644 --- a/pennylane/tape/tapes/jacobian_tape.py +++ b/pennylane/tape/jacobian_tape.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane.operation import State -from pennylane.tape.tapes.tape import QuantumTape +from pennylane.tape import QuantumTape STATE_PREP_OPS = ( qml.BasisState, diff --git a/pennylane/tape/measure.py b/pennylane/tape/measure.py deleted file mode 100644 index e79c20bd43a..00000000000 --- a/pennylane/tape/measure.py +++ /dev/null @@ -1,430 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=protected-access -""" -This module contains the functions for computing different types of measurement -outcomes from quantum observables - expectation values, variances of expectations, -and measurement samples using AnnotatedQueues. -""" -import copy - -import numpy as np - -import pennylane as qml -from pennylane.operation import Expectation, Observable, Probability, Sample, State, Variance -from pennylane.qnodes import QuantumFunctionError -from pennylane.wires import Wires - - -class MeasurementProcess: - """Represents a measurement process occurring at the end of a - quantum variational circuit. - - Args: - return_type (.ObservableReturnTypes): The type of measurement process. - This includes ``Expectation``, ``Variance``, ``Sample``, ``State``, or ``Probability``. - obs (.Observable): The observable that is to be measured as part of the - measurement process. Not all measurement processes require observables (for - example ``Probability``); this argument is optional. - wires (.Wires): The wires the measurement process applies to. - This can only be specified if an observable was not provided. - eigvals (array): A flat array representing the eigenvalues of the measurement. - This can only be specified if an observable was not provided. - """ - - # pylint: disable=too-few-public-methods - - def __init__(self, return_type, obs=None, wires=None, eigvals=None): - self.return_type = return_type - self.obs = obs - - if wires is not None and obs is not None: - raise ValueError("Cannot set the wires if an observable is provided.") - - self._wires = wires or Wires([]) - self._eigvals = None - - if eigvals is not None: - if obs is not None: - raise ValueError("Cannot set the eigenvalues if an observable is provided.") - - self._eigvals = np.array(eigvals) - - # TODO: remove the following lines once devices - # have been refactored to accept and understand recieving - # measurement processes rather than specific observables. - - # The following lines are only applicable for measurement processes - # that do no have corresponding observables (e.g., Probability). We use - # them to 'trick' the device into thinking it has recieved an observable. - - # Below, we imitate an identity observable, so that the - # device undertakes no action upon recieving this observable. - self.name = "Identity" - self.data = [] - - # Queue the measurement process - self.queue() - - def diagonalizing_gates(self): - """Returns the gates that diagonalize the measured wires such that they - are in the eigenbasis of the circuit observables. - - Returns: - List[.Operation]: the operations that diagonalize the observables - """ - try: - return self.expand().operations - except NotImplementedError: - return [] - - def __repr__(self): - """Representation of this class.""" - if self.obs is None: - return "{}(wires={})".format(self.return_type.value, self.wires.tolist()) - - # Todo: when tape is core the return type will always be taken from the MeasurementProcess - if self.obs.return_type is None: - return "{}({})".format(self.return_type.value, self.obs) - - return "{}".format(self.obs) - - def __copy__(self): - cls = self.__class__ - - if self.obs is not None: - return cls(self.return_type, obs=copy.copy(self.obs)) - - return cls(self.return_type, eigvals=self._eigvals, wires=self._wires) - - @property - def wires(self): - r"""The wires the measurement process acts on.""" - if self.obs is not None: - return self.obs.wires - return self._wires - - @property - def eigvals(self): - r"""Eigenvalues associated with the measurement process. - - If the measurement process has an associated observable, - the eigenvalues will correspond to this observable. Otherwise, - they will be the eigenvalues provided when the measurement - process was instantiated. - - Note that the eigenvalues are not guaranteed to be in any - particular order. - - **Example:** - - >>> m = MeasurementProcess(Expectation, obs=qml.PauliX(wires=1)) - >>> m.eigvals - array([1, -1]) - - Returns: - array: eigvals representation - """ - if self.obs is not None: - try: - return self.obs.eigvals - except NotImplementedError: - pass - - return self._eigvals - - def expand(self): - """Expand the measurement of an observable to a unitary - rotation and a measurement in the computational basis. - - Returns: - .JacobianTape: a quantum tape containing the operations - required to diagonalize the observable - - **Example** - - Consider a measurement process consisting of the expectation - value of an Hermitian observable: - - >>> H = np.array([[1, 2], [2, 4]]) - >>> obs = qml.Hermitian(H, wires=['a']) - >>> m = MeasurementProcess(Expectation, obs=obs) - - Expanding this out: - - >>> tape = m.expand() - - We can see that the resulting tape has the qubit unitary applied, - and a measurement process with no observable, but the eigenvalues - specified: - - >>> print(tape.operations) - [QubitUnitary(array([[-0.89442719, 0.4472136 ], - [ 0.4472136 , 0.89442719]]), wires=['a'])] - >>> print(tape.measurements[0].eigvals) - [0. 5.] - >>> print(tape.measurements[0].obs) - None - """ - if self.obs is None: - raise NotImplementedError("Cannot expand a measurement process with no observable.") - - from pennylane.tape import JacobianTape # pylint: disable=import-outside-toplevel - - with JacobianTape() as tape: - self.obs.diagonalizing_gates() - MeasurementProcess(self.return_type, wires=self.obs.wires, eigvals=self.obs.eigvals) - - return tape - - def queue(self): - """Append the measurement process to an annotated queue.""" - if self.obs is not None: - try: - qml.tape.QueuingContext.update_info(self.obs, owner=self) - except ValueError: - self.obs.queue() - qml.tape.QueuingContext.update_info(self.obs, owner=self) - - qml.tape.QueuingContext.append(self, owns=self.obs) - else: - qml.tape.QueuingContext.append(self) - - -def expval(op): - r"""Expectation value of the supplied observable. - - **Example:** - - .. code-block:: python3 - - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(x): - qml.RX(x, wires=0) - qml.Hadamard(wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliY(0)) - - Executing this QNode: - - >>> circuit(0.5) - -0.4794255386042029 - - Args: - op (Observable): a quantum observable object - - Raises: - QuantumFunctionError: `op` is not an instance of :class:`~.Observable` - """ - if not isinstance(op, Observable): - raise QuantumFunctionError( - "{} is not an observable: cannot be used with expval".format(op.name) - ) - - return MeasurementProcess(Expectation, obs=op) - - -def var(op): - r"""Variance of the supplied observable. - - **Example:** - - .. code-block:: python3 - - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(x): - qml.RX(x, wires=0) - qml.Hadamard(wires=1) - qml.CNOT(wires=[0, 1]) - return qml.var(qml.PauliY(0)) - - Executing this QNode: - - >>> circuit(0.5) - 0.7701511529340698 - - Args: - op (Observable): a quantum observable object - - Raises: - QuantumFunctionError: `op` is not an instance of :class:`~.Observable` - """ - if not isinstance(op, Observable): - raise QuantumFunctionError( - "{} is not an observable: cannot be used with var".format(op.name) - ) - - return MeasurementProcess(Variance, obs=op) - - -def sample(op): - r"""Sample from the supplied observable, with the number of shots - determined from the ``dev.shots`` attribute of the corresponding device. - - The samples are drawn from the eigenvalues :math:`\{\lambda_i\}` of the observable. - The probability of drawing eigenvalue :math:`\lambda_i` is given by - :math:`p(\lambda_i) = |\langle \xi_i | \psi \rangle|^2`, where :math:`| \xi_i \rangle` - is the corresponding basis state from the observable's eigenbasis. - - **Example:** - - .. code-block:: python3 - - dev = qml.device("default.qubit", wires=2, shots=4) - - @qml.qnode(dev) - def circuit(x): - qml.RX(x, wires=0) - qml.Hadamard(wires=1) - qml.CNOT(wires=[0, 1]) - return qml.sample(qml.PauliY(0)) - - Executing this QNode: - - >>> circuit(0.5) - array([ 1., 1., 1., -1.]) - - Args: - op (Observable): a quantum observable object - - Raises: - QuantumFunctionError: `op` is not an instance of :class:`~.Observable` - """ - if not isinstance(op, Observable): - raise QuantumFunctionError( - "{} is not an observable: cannot be used with sample".format(op.name) - ) - - return MeasurementProcess(Sample, obs=op) - - -def probs(wires): - r"""Probability of each computational basis state. - - This measurement function accepts no observables, and instead - instructs the QNode to return a flat array containing the - probabilities :math:`|\langle i | \psi \rangle |^2` of measuring - the computational basis state :math:`| i \rangle` given the current - state :math:`| \psi \rangle`. - - Marginal probabilities may also be requested by restricting - the wires to a subset of the full system; the size of the - returned array will be ``[2**len(wires)]``. - - **Example:** - - .. code-block:: python3 - - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(): - qml.Hadamard(wires=1) - return qml.probs(wires=[0, 1]) - - Executing this QNode: - - >>> circuit() - array([0.5, 0.5, 0. , 0. ]) - - The returned array is in lexicographic order, so corresponds - to a :math:`50\%` chance of measuring either :math:`|00\rangle` - or :math:`|01\rangle`. - - Args: - wires (Sequence[int] or int): the wire the operation acts on - """ - # pylint: disable=protected-access - return MeasurementProcess(Probability, wires=qml.wires.Wires(wires)) - - -def state(): - r"""Quantum state in the computational basis. - - This function accepts no observables and instead instructs the QNode to return its state. A - ``wires`` argument should *not* be provided since ``state()`` always returns a pure state - describing all wires in the device. - - **Example:** - - .. code-block:: python3 - - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(): - qml.Hadamard(wires=1) - return qml.state() - - Executing this QNode: - - >>> circuit() - array([0.70710678+0.j, 0.70710678+0.j, 0. +0.j, 0. +0.j]) - - The returned array is in lexicographic order. Hence, we have a :math:`1/\sqrt{2}` amplitude - in both :math:`|00\rangle` and :math:`|01\rangle`. - - .. note:: - - Calculating the derivative of :func:`~.state` is currently only supported when using the - classical backpropagation differentiation method (``diff_method="backprop"``) with a - compatible device. - """ - # pylint: disable=protected-access - return MeasurementProcess(State) - - -def density_matrix(wires): - r"""Quantum density matrix in the computational basis. - - This function accepts no observables and instead instructs the QNode to return its density - matrix or reduced density matrix. The ``wires`` argument gives the possibility - to trace out a part of the system. It can result in obtaining a mixed state, which can be - only represented by the reduced density matrix. - - **Example:** - - .. code-block:: python3 - - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(): - qml.PauliY(wires=0) - qml.Hadamard(wires=1) - return qml.density_matrix([0]) - - Executing this QNode: - - >>> circuit() - array([[0.+0.j 0.+0.j] - [0.+0.j 1.+0.j]]) - - The returned matrix is the reduced density matrix, where system 1 is traced out. - - Args: - wires (Sequence[int] or int): the wires of the subsystem - - .. note:: - - Calculating the derivative of :func:`~.density_matrix` is currently only supported when - using the classical backpropagation differentiation method (``diff_method="backprop"``) - with a compatible device. - """ - # pylint: disable=protected-access - return MeasurementProcess(State, wires=qml.wires.Wires(wires)) diff --git a/pennylane/tape/operation.py b/pennylane/tape/operation.py deleted file mode 100644 index a8d1d50e9f1..00000000000 --- a/pennylane/tape/operation.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=protected-access -r""" -The following functions in this module monkeypatch and un-monkeypatch -the PennyLane operations to work with the new AnnotatedQueue. -""" -from unittest import mock - -import pennylane as qml - -from .queuing import QueuingContext - - -def operation_queue(self): - """Monkeypatched :meth:`~.Operation.queue` method, allowing - operations to queue themselves to the beta :class:`~.QueuingContext`. - """ - QueuingContext.append(self) - return self - - -def operation_inv(self): - """Monkeypatched :meth:`~.Operation.inv` method. - - Rather than updating some internal attribute, this monkeypatched - method instead annotates the queue with the current 'inverse' - status. - - This operation acts as a 'radio button', swapping the current - boolean property of the 'inverse' annotation on the object in the queue. - """ - current_inv = QueuingContext.get_info(self).get("inverse", False) - QueuingContext.update_info(self, inverse=not current_inv) - return self - - -def operation_expand(self): - """Monkeypatched :meth:`~.Operation.expand` method for operations. - - Currently, this monkeypatched expand method simply mirrors the - existing :meth:`~.Operation.decomposition` method; however with - two main differences: - - * It returns a tape containing the decomposed operations, rather - than a list. - - * If a decomposition is not available, it simply returns itself. - - Returns: - .JacobianTape: Returns a quantum tape that contains the - operations decomposition, or if not implemented, simply - the operation itself. - """ - tape = qml.tape.QuantumTape() - - with tape: - self.decomposition(*self.data, wires=self.wires) - - if not self.data: - # original operation has no trainable parameters - tape.trainable_params = {} - - if self.inverse: - tape.inv() - - return tape - - -def tensor_init(self, *args): - """Monkeypatched :meth:`~.Tensor.__init__` method, allowing - Tensors to queue themselves to the beta :class:`~.QueuingContext`, - and to annotate the queue specifying previously queued objects they - 'own'. - """ - self._eigvals_cache = None - self.obs = [] - - for o in args: - if isinstance(o, qml.operation.Tensor): - self.obs.extend(o.obs) - - elif isinstance(o, qml.operation.Observable): - self.obs.append(o) - - try: - QueuingContext.update_info(o, owner=self) - except ValueError: - o.queue() - qml.tape.QueuingContext.update_info(o, owner=self) - except NotImplementedError: - pass - - QueuingContext.append(self, owns=tuple(args)) - - -def tensor_matmul(self, other): - """Monkeypatched :meth:`~.Tensor.__matmul__` method, to ensure - that tensors created via left matrix multiplication are correctly - added to the queue, and their components are annotated. - """ - if isinstance(other, qml.operation.Tensor): - self.obs.extend(other.obs) - - elif isinstance(other, qml.operation.Observable): - self.obs.append(other) - - owning_info = QueuingContext.get_info(self)["owns"] + (other,) - - # update the annotated queue information - QueuingContext.update_info(self, owns=owning_info) - QueuingContext.update_info(other, owner=self) - - return self - - -def tensor_rmatmul(self, other): - """Monkeypatched :meth:`~.Tensor.__rmatmul__` method, to ensure - that tensors created via right matrix multiplication are correctly - added to the queue, and their components are annotated. - """ - self.obs[:0] = [other] - QueuingContext.update_info(other, owner=self) - return self - - -def mock_operations(): - """Create mock operations, observables and tensors that are monkeypatched - to work with the new QueuingContext. - - Creating mocked methods, rather than directly monkeypatching/overwriting the methods, - allows us to later remove/undo the monkeypatching once no longer needed. - - Returns: - list[MagicMock]: list containing the mocked operations. - """ - # Monkeypatch the 'expand' method of operations directly. - # This is required since it does not already exist, and so can't be mocked. - qml.operation.Operation.expand = operation_expand - - mocks = [] - - # create mock operation methods - mocks += [mock.patch.object(qml.operation.Operation, "queue", operation_queue)] - mocks += [mock.patch.object(qml.operation.Operation, "inv", operation_inv)] - - # create mock observable methods - mocks += [mock.patch.object(qml.operation.Observable, "queue", operation_queue)] - - # create mock tensor methods - mocks += [mock.patch.object(qml.operation.Tensor, "__init__", tensor_init)] - mocks += [mock.patch.object(qml.operation.Tensor, "__matmul__", tensor_matmul)] - mocks += [mock.patch.object(qml.operation.Tensor, "__rmatmul__", tensor_rmatmul)] - - # create mock measurement functions - mocks += [mock.patch.object(qml, "expval", qml.tape.measure.expval)] - mocks += [mock.patch.object(qml, "var", qml.tape.measure.var)] - mocks += [mock.patch.object(qml, "sample", qml.tape.measure.sample)] - mocks += [mock.patch.object(qml, "probs", qml.tape.measure.probs)] - mocks += [mock.patch.object(qml, "state", qml.tape.measure.state, create=True)] - mocks += [ - mock.patch.object(qml, "density_matrix", qml.tape.measure.density_matrix, create=True) - ] - - # Mock the operations so that they no longer perform validation - # on argument types and domain. This is required to avoid the operations - # complaining when unknown types (such as TensorFlow and Torch tensors) are - # used as arguments. - mocks += [mock.patch.object(qml.operation.Operator, "do_check_domain", False)] - - return mocks diff --git a/pennylane/tape/tapes/qubit_param_shift.py b/pennylane/tape/qubit_param_shift.py similarity index 97% rename from pennylane/tape/tapes/qubit_param_shift.py rename to pennylane/tape/qubit_param_shift.py index d0f5dec8793..63883854eef 100644 --- a/pennylane/tape/tapes/qubit_param_shift.py +++ b/pennylane/tape/qubit_param_shift.py @@ -22,8 +22,8 @@ import numpy as np import pennylane as qml -from pennylane.tape.measure import MeasurementProcess -from pennylane.tape.tapes import QuantumTape +from pennylane.measure import MeasurementProcess +from pennylane.tape import QuantumTape from .jacobian_tape import JacobianTape diff --git a/pennylane/tape/tapes/reversible.py b/pennylane/tape/reversible.py similarity index 97% rename from pennylane/tape/tapes/reversible.py rename to pennylane/tape/reversible.py index d8111b932e7..f3072981f2e 100644 --- a/pennylane/tape/tapes/reversible.py +++ b/pennylane/tape/reversible.py @@ -212,7 +212,7 @@ def reversible_diff(self, idx, params, **options): for op in between_ops: op.queue() - qml.tape.measure.state() + qml.state() tapes = [new_circuit] diff --git a/pennylane/tape/tapes/tape.py b/pennylane/tape/tape.py similarity index 97% rename from pennylane/tape/tapes/tape.py rename to pennylane/tape/tape.py index 1b13f611af7..09395e0af76 100644 --- a/pennylane/tape/tapes/tape.py +++ b/pennylane/tape/tape.py @@ -24,9 +24,7 @@ import pennylane as qml from pennylane.grouping import diagonalize_qwc_pauli_words -from pennylane.tape.circuit_graph import TapeCircuitGraph -from pennylane.tape.operation import mock_operations -from pennylane.tape.queuing import AnnotatedQueue, QueuingContext +from pennylane.queuing import AnnotatedQueue, QueuingContext from pennylane.operation import Sample STATE_PREP_OPS = ( @@ -114,7 +112,7 @@ def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False): tape._ops.extend(rotations) for o, i in zip(diag_obs, tape._obs_sharing_wires_id): - new_m = qml.tape.measure.MeasurementProcess(tape.measurements[i].return_type, obs=o) + new_m = qml.measure.MeasurementProcess(tape.measurements[i].return_type, obs=o) tape._measurements[i] = new_m for queue in ("_prep", "_ops", "_measurements"): @@ -125,7 +123,7 @@ def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False): if not expand_measurements: # Measurements should not be expanded; treat measurements # as a stopping condition - stop = stop or isinstance(obj, qml.tape.measure.MeasurementProcess) + stop = stop or isinstance(obj, qml.measure.MeasurementProcess) if stop: # do not expand out the object; append it to the @@ -133,7 +131,7 @@ def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False): getattr(new_tape, queue).append(obj) continue - if isinstance(obj, (qml.operation.Operation, qml.tape.measure.MeasurementProcess)): + if isinstance(obj, (qml.operation.Operation, qml.measure.MeasurementProcess)): # Object is an operation; query it for its expansion try: obj = obj.expand() @@ -193,10 +191,10 @@ class QuantumTape(AnnotatedQueue): >>> tape.num_params 3 - The :class:`~.TapeCircuitGraph` can also be accessed: + The :class:`~.CircuitGraph` can also be accessed: >>> tape.graph - + Once constructed, the quantum tape can be executed directly on a supported device: @@ -350,7 +348,7 @@ def _process_queue(self): else: self._ops.append(obj) - elif isinstance(obj, qml.tape.measure.MeasurementProcess): + elif isinstance(obj, qml.measure.MeasurementProcess): # measurement process self._measurements.append(obj) @@ -849,16 +847,16 @@ def graph(self): quantum circuit: >>> tape.graph - + Note that the circuit graph is only constructed once, on first call to this property, and cached for future use. Returns: - .TapeCircuitGraph: the circuit graph object + .CircuitGraph: the circuit graph object """ if self._graph is None: - self._graph = TapeCircuitGraph( + self._graph = qml.CircuitGraph( self.operations, self.observables, self.wires, self._par_info, self.trainable_params ) diff --git a/pennylane/tape/tapes/__init__.py b/pennylane/tape/tapes/__init__.py deleted file mode 100644 index ee2b309c1de..00000000000 --- a/pennylane/tape/tapes/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This subpackage contains various quantum tapes, which track, queue, -validate, execute, and differentiate quantum circuits. -""" -from .tape import QuantumTape -from .jacobian_tape import JacobianTape -from .cv_param_shift import CVParamShiftTape -from .qubit_param_shift import QubitParamShiftTape -from .reversible import ReversibleTape diff --git a/pennylane/templates/broadcast.py b/pennylane/templates/broadcast.py index 433079c4646..0a6207d9636 100644 --- a/pennylane/templates/broadcast.py +++ b/pennylane/templates/broadcast.py @@ -118,19 +118,11 @@ def _preprocess(parameters, pattern, wires): # check that there are enough parameters for pattern if parameters is not None: - if qml.tape_mode_active(): - shape = qml.math.shape(parameters) + shape = qml.math.shape(parameters) - # expand dimension so that parameter sets for each unitary can be unpacked - if len(shape) == 1: - parameters = qml.math.expand_dims(parameters, 1) - - else: - shape = get_shape(parameters) - - # expand dimension so that parameter sets for each unitary can be unpacked - if len(shape) == 1: - parameters = [[p] for p in parameters] + # expand dimension so that parameter sets for each unitary can be unpacked + if len(shape) == 1: + parameters = qml.math.expand_dims(parameters, 1) # specific error message for ring edge case of 2 wires if (pattern == "ring") and (len(wires) == 2) and (shape[0] != 1): diff --git a/pennylane/templates/decorator.py b/pennylane/templates/decorator.py index 4f2b1c31a3b..4fc15f13b93 100644 --- a/pennylane/templates/decorator.py +++ b/pennylane/templates/decorator.py @@ -16,7 +16,7 @@ """ from functools import wraps -from pennylane._queuing import OperationRecorder +from pennylane.queuing import OperationRecorder def template(func): @@ -58,14 +58,8 @@ def circuit(): @wraps(func) def wrapper(*args, **kwargs): - import pennylane as qml - recorder_class = OperationRecorder - - if qml.tape_mode_active(): - recorder_class = qml.tape.TapeOperationRecorder - - with recorder_class() as rec: + with OperationRecorder() as rec: func(*args, **kwargs) return rec.queue diff --git a/pennylane/templates/embeddings/amplitude.py b/pennylane/templates/embeddings/amplitude.py index 818975660d6..c369511a255 100644 --- a/pennylane/templates/embeddings/amplitude.py +++ b/pennylane/templates/embeddings/amplitude.py @@ -21,7 +21,6 @@ import pennylane as qml from pennylane.templates.decorator import template from pennylane.ops import QubitStateVector -from pennylane.variable import Variable from pennylane.wires import Wires from pennylane.templates.utils import check_shape, get_shape, check_type @@ -49,106 +48,42 @@ def _preprocess(features, wires, pad_with, normalize): tensor: pre-processed features """ - if qml.tape_mode_active(): + shape = qml.math.shape(features) - shape = qml.math.shape(features) + # check shape + if len(shape) != 1: + raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") - # check shape - if len(shape) != 1: - raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") + n_features = shape[0] + if pad_with is None and n_features != 2 ** len(wires): + raise ValueError( + f"Features must be of length {2 ** len(wires)}; got length {n_features}. " + f"Use the 'pad' argument for automated padding." + ) - n_features = shape[0] - if pad_with is None and n_features != 2 ** len(wires): - raise ValueError( - f"Features must be of length {2 ** len(wires)}; got length {n_features}. " - f"Use the 'pad' argument for automated padding." - ) + if pad_with is not None and n_features > 2 ** len(wires): + raise ValueError( + f"Features must be of length {2 ** len(wires)} or " + f"smaller to be padded; got length {n_features}." + ) - if pad_with is not None and n_features > 2 ** len(wires): - raise ValueError( - f"Features must be of length {2 ** len(wires)} or " - f"smaller to be padded; got length {n_features}." - ) + # pad + if pad_with is not None and n_features < 2 ** len(wires): + padding = [pad_with] * (2 ** len(wires) - n_features) + features = qml.math.concatenate([features, padding], axis=0) - # pad - if pad_with is not None and n_features < 2 ** len(wires): - padding = [pad_with] * (2 ** len(wires) - n_features) - features = qml.math.concatenate([features, padding], axis=0) - - # normalize - norm = qml.math.sum(qml.math.abs(features) ** 2) - - if not qml.math.allclose(norm, 1.0, atol=TOLERANCE): - if normalize or pad_with: - features = features / np.sqrt(norm) - else: - raise ValueError( - f"Features must be a vector of length 1.0; got length {norm}." - "Use 'normalize=True' to automatically normalize." - ) - - # todo: delete if tape is only core - else: - n_amplitudes = 2 ** len(wires) - expected_shape = (n_amplitudes,) - - if len(get_shape(features)) > 1: - raise ValueError( - f"Features must be a one-dimensional vector; got shape {get_shape(features)}." - ) + # normalize + norm = qml.math.sum(qml.math.abs(features) ** 2) - if pad_with is None: - shape = check_shape( - features, - expected_shape, - msg="Features must be of length {}; got {}. Use the 'pad' " - "argument for automated padding." - "".format(expected_shape, get_shape(features)), - ) + if not qml.math.allclose(norm, 1.0, atol=TOLERANCE): + if normalize or pad_with: + features = features / np.sqrt(norm) else: - shape = check_shape( - features, - expected_shape, - bound="max", - msg="Features must be of length {} or smaller " - "to be padded; got {}" - "".format(expected_shape, get_shape(features)), - ) - - check_type( - pad_with, - [float, complex, type(None)], - msg="'pad' must be a float or complex; got {}".format(pad_with), - ) - check_type(normalize, [bool], msg="'normalize' must be a boolean; got {}".format(normalize)) - - # pad - n_features = shape[0] - if pad_with is not None and n_amplitudes > n_features: - features = np.pad( - features, (0, n_amplitudes - n_features), mode="constant", constant_values=pad_with + raise ValueError( + f"Features must be a vector of length 1.0; got length {norm}." + "Use 'normalize=True' to automatically normalize." ) - # normalize - if isinstance(features[0], Variable): - feature_values = [s.val for s in features] - norm = np.sum(np.abs(feature_values) ** 2) - else: - norm = np.sum(np.abs(features) ** 2) - - if not np.isclose(norm, 1.0, atol=TOLERANCE): - if normalize or pad_with: - features = features / np.sqrt(norm) - else: - raise ValueError( - "Features must be a vector of length 1.0; got length {}." - "Use 'normalize=True' to automatically normalize.".format(norm) - ) - - ############### - - features = np.array(features) - return features diff --git a/pennylane/templates/embeddings/angle.py b/pennylane/templates/embeddings/angle.py index 9725071a55d..cc4fa7345f9 100644 --- a/pennylane/templates/embeddings/angle.py +++ b/pennylane/templates/embeddings/angle.py @@ -39,31 +39,16 @@ def _preprocess(features, wires): Returns: int: number of features """ + shape = qml.math.shape(features) - if qml.tape_mode_active(): + if len(shape) != 1: + raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") - shape = qml.math.shape(features) - - if len(shape) != 1: - raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") - - n_features = shape[0] - if n_features > len(wires): - raise ValueError( - f"Features must be of length {len(wires)} or less; got length {n_features}." - ) - - else: - - shp = check_shape( - features, - (len(wires),), - bound="max", - msg="Features must be of shape {} or smaller; " - "got {}.".format((len(wires),), get_shape(features)), + n_features = shape[0] + if n_features > len(wires): + raise ValueError( + f"Features must be of length {len(wires)} or less; got length {n_features}." ) - n_features = shp[0] - return n_features diff --git a/pennylane/templates/embeddings/basis.py b/pennylane/templates/embeddings/basis.py index e06a5031886..67325cd91c5 100644 --- a/pennylane/templates/embeddings/basis.py +++ b/pennylane/templates/embeddings/basis.py @@ -37,41 +37,19 @@ def _preprocess(features, wires): Returns: array: numpy array representation of the features tensor """ + shape = qml.math.shape(features) - if qml.tape_mode_active(): + if len(shape) != 1: + raise ValueError(f"Features must be one-dimensional; got shape {shape}.") - shape = qml.math.shape(features) + n_features = shape[0] + if n_features != len(wires): + raise ValueError(f"Features must be of length {len(wires)}; got length {n_features}.") - if len(shape) != 1: - raise ValueError(f"Features must be one-dimensional; got shape {shape}.") + features = list(qml.math.toarray(features)) - n_features = shape[0] - if n_features != len(wires): - raise ValueError(f"Features must be of length {len(wires)}; got length {n_features}.") - - features = list(qml.math.toarray(features)) - - if set(features) != {0, 1}: - raise ValueError(f"Basis state must only consist of 0s and 1s; got {features}") - - return features - - # non-tape mode - check_type( - features, - [Iterable], - msg="Features must be iterable; got type {}".format(type(features)), - ) - - expected_shape = (len(wires),) - check_shape( - features, - expected_shape, - msg="Features must be of shape {}; got {}" "".format(expected_shape, get_shape(features)), - ) - - if any([b not in [0, 1] for b in features]): - raise ValueError("Basis state must only consist of 0s and 1s; got {}".format(features)) + if set(features) != {0, 1}: + raise ValueError(f"Basis state must only consist of 0s and 1s; got {features}") return features diff --git a/pennylane/templates/embeddings/displacement.py b/pennylane/templates/embeddings/displacement.py index af5cbb74e18..4f7e0ff1cbf 100644 --- a/pennylane/templates/embeddings/displacement.py +++ b/pennylane/templates/embeddings/displacement.py @@ -43,51 +43,24 @@ def _preprocess(features, wires, method, c): Returns: tensor_like: 2-dimensional tensor containing the features and constants """ + shape = qml.math.shape(features) + constants = [c] * shape[0] - if qml.tape_mode_active(): - shape = qml.math.shape(features) - constants = [c] * shape[0] + if len(shape) != 1: + raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") - if len(shape) != 1: - raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") + n_features = shape[0] + if n_features != len(wires): + raise ValueError(f"Features must be of length {len(wires)}; got length {n_features}.") - n_features = shape[0] - if n_features != len(wires): - raise ValueError(f"Features must be of length {len(wires)}; got length {n_features}.") + if method == "amplitude": + pars = qml.math.stack([features, constants], axis=1) - if method == "amplitude": - pars = qml.math.stack([features, constants], axis=1) - - elif method == "phase": - pars = qml.math.stack([constants, features], axis=1) - - else: - raise ValueError(f"did not recognize method {method}") + elif method == "phase": + pars = qml.math.stack([constants, features], axis=1) else: - - expected_shape = (len(wires),) - check_shape( - features, - expected_shape, - bound="max", - msg="Features must be of shape {} or smaller; got {}." - "".format(expected_shape, get_shape(features)), - ) - - check_is_in_options( - method, - ["amplitude", "phase"], - msg="did not recognize option {} for 'method'" "".format(method), - ) - - constants = [c] * len(features) - - if method == "amplitude": - pars = list(zip(features, constants)) - - elif method == "phase": - pars = list(zip(constants, features)) + raise ValueError(f"did not recognize method {method}") return pars diff --git a/pennylane/templates/embeddings/iqp.py b/pennylane/templates/embeddings/iqp.py index b9890826d52..c04e6324c3c 100644 --- a/pennylane/templates/embeddings/iqp.py +++ b/pennylane/templates/embeddings/iqp.py @@ -47,58 +47,21 @@ def _preprocess(features, wires, pattern, n_repeats): Returns: list[Wires]: preprocessed pattern """ + shape = qml.math.shape(features) - if qml.tape_mode_active(): + if len(shape) != 1: + raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") - shape = qml.math.shape(features) - - if len(shape) != 1: - raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") - - n_features = shape[0] - if n_features != len(wires): - raise ValueError(f"Features must be of length {len(wires)}; got length {n_features}.") - - if pattern is None: - # default is an all-to-all pattern - pattern = [Wires(wire_pair) for wire_pair in combinations(wires, 2)] - else: - # convert wire pairs to Wires object - pattern = [Wires(wire_pair) for wire_pair in pattern] + n_features = shape[0] + if n_features != len(wires): + raise ValueError(f"Features must be of length {len(wires)}; got length {n_features}.") + if pattern is None: + # default is an all-to-all pattern + pattern = [Wires(wire_pair) for wire_pair in combinations(wires, 2)] else: - expected_shape = (len(wires),) - check_shape( - features, - expected_shape, - msg="Features must be of shape {}; got {}" - "".format(expected_shape, get_shape(features)), - ) - - check_type( - n_repeats, - [int], - msg="'n_repeats' must be an integer; got type {}".format(type(n_repeats)), - ) - - if pattern is None: - # default is an all-to-all pattern - pattern = [Wires(wire_pair) for wire_pair in combinations(wires, 2)] - else: - # do some checks - check_type( - pattern, - [Iterable, type(None)], - msg="'pattern' must be a list of pairs of wires; got {}".format(pattern), - ) - shape = get_shape(pattern) - if len(shape) != 2 or shape[1] != 2: - raise ValueError( - "'pattern' must be a list of pairs of wires; got {}".format(pattern) - ) - - # convert wire pairs to Wires object - pattern = [Wires(wire_pair) for wire_pair in pattern] + # convert wire pairs to Wires object + pattern = [Wires(wire_pair) for wire_pair in pattern] return pattern diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index 43b5f08b9f5..3ac285cb288 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -43,72 +43,31 @@ def _preprocess(features, wires, weights): Returns: int: number of times that embedding is repeated """ + shape = qml.math.shape(features) - if qml.tape_mode_active(): + if len(shape) != 1: + raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") - shape = qml.math.shape(features) - - if len(shape) != 1: - raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") - - n_features = shape[0] - if n_features > len(wires): - raise ValueError( - f"Features must be of length {len(wires)} or less; got length {n_features}." - ) - - shape = qml.math.shape(weights) - repeat = shape[0] + n_features = shape[0] + if n_features > len(wires): + raise ValueError( + f"Features must be of length {len(wires)} or less; got length {n_features}." + ) - if len(wires) == 1: - if shape != (repeat, 1): - raise ValueError(f"Weights tensor must be of shape {(repeat, 1)}; got {shape}") + shape = qml.math.shape(weights) + repeat = shape[0] - elif len(wires) == 2: - if shape != (repeat, 3): - raise ValueError(f"Weights tensor must be of shape {(repeat, 3)}; got {shape}") - else: - if shape != (repeat, 2 * len(wires)): - raise ValueError( - f"Weights tensor must be of shape {(repeat, 2*len(wires))}; got {shape}" - ) + if len(wires) == 1: + if shape != (repeat, 1): + raise ValueError(f"Weights tensor must be of shape {(repeat, 1)}; got {shape}") + elif len(wires) == 2: + if shape != (repeat, 3): + raise ValueError(f"Weights tensor must be of shape {(repeat, 3)}; got {shape}") else: - - expected_shape = (len(wires),) - check_shape( - features, - expected_shape, - bound="max", - msg="Features must be of shape {} or smaller; got {}" - "".format((len(wires),), get_shape(features)), - ) - - repeat = check_number_of_layers([weights]) - - if len(wires) == 1: - expected_shape = (repeat, 1) - check_shape( - weights, - expected_shape, - msg="Weights tensor must be of shape {}; got {}" - "".format(expected_shape, get_shape(features)), - ) - elif len(wires) == 2: - expected_shape = (repeat, 3) - check_shape( - weights, - expected_shape, - msg="Weights tensor must be of shape {}; got {}" - "".format(expected_shape, get_shape(features)), - ) - else: - expected_shape = (repeat, 2 * len(wires)) - check_shape( - weights, - expected_shape, - msg="Weights tensor must be of shape {}; got {}" - "".format(expected_shape, get_shape(features)), + if shape != (repeat, 2 * len(wires)): + raise ValueError( + f"Weights tensor must be of shape {(repeat, 2*len(wires))}; got {shape}" ) return repeat diff --git a/pennylane/templates/embeddings/squeezing.py b/pennylane/templates/embeddings/squeezing.py index e818f15ace0..397fca6d637 100644 --- a/pennylane/templates/embeddings/squeezing.py +++ b/pennylane/templates/embeddings/squeezing.py @@ -43,52 +43,24 @@ def _preprocess(features, wires, method, c): Returns: tensor_like: 2-dimensional tensor containing the features and constants """ + shape = qml.math.shape(features) + constants = [c] * shape[0] - if qml.tape_mode_active(): + if len(shape) != 1: + raise ValueError(f"Features must be one-dimensional; got shape {shape}.") - shape = qml.math.shape(features) - constants = [c] * shape[0] + n_features = shape[0] + if n_features != len(wires): + raise ValueError(f"Features must be of length {len(wires)}; got length {n_features}.") - if len(shape) != 1: - raise ValueError(f"Features must be one-dimensional; got shape {shape}.") + if method == "amplitude": + pars = qml.math.stack([features, constants], axis=1) - n_features = shape[0] - if n_features != len(wires): - raise ValueError(f"Features must be of length {len(wires)}; got length {n_features}.") - - if method == "amplitude": - pars = qml.math.stack([features, constants], axis=1) - - elif method == "phase": - pars = qml.math.stack([constants, features], axis=1) - - else: - raise ValueError(f"did not recognize method {method}") + elif method == "phase": + pars = qml.math.stack([constants, features], axis=1) else: - - expected_shape = (len(wires),) - check_shape( - features, - expected_shape, - bound="max", - msg="Features must be of shape {} or smaller; got {}." - "".format(expected_shape, get_shape(features)), - ) - - check_is_in_options( - method, - ["amplitude", "phase"], - msg="did not recognize option {} for 'method'" "".format(method), - ) - - constants = [c] * len(features) - - if method == "amplitude": - pars = list(zip(features, constants)) - - elif method == "phase": - pars = list(zip(constants, features)) + raise ValueError(f"did not recognize method {method}") return pars diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 5980ed61fb6..f697aa7f875 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -40,29 +40,15 @@ def _preprocess(weights, wires): Returns: int: number of times that the ansatz is repeated """ + shape = qml.math.shape(weights) + repeat = shape[0] - if qml.tape_mode_active(): + if len(shape) != 2: + raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") - shape = qml.math.shape(weights) - repeat = shape[0] - - if len(shape) != 2: - raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") - - if shape[1] != len(wires): - raise ValueError( - f"Weights tensor must have second dimension of length {len(wires)}; got {shape[1]}" - ) - - else: - - repeat = check_number_of_layers([weights]) - - expected_shape = (repeat, len(wires)) - check_shape( - weights, - expected_shape, - msg=f"Weights tensor must have second dimension of length {len(wires)}; got {get_shape(weights)[1]}", + if shape[1] != len(wires): + raise ValueError( + f"Weights tensor must have second dimension of length {len(wires)}; got {shape[1]}" ) return repeat diff --git a/pennylane/templates/layers/cv_neural_net.py b/pennylane/templates/layers/cv_neural_net.py index 1d37e16ba86..a84b15d955e 100644 --- a/pennylane/templates/layers/cv_neural_net.py +++ b/pennylane/templates/layers/cv_neural_net.py @@ -51,56 +51,33 @@ def _preprocess(theta_1, phi_1, varphi_1, r, phi_r, theta_2, phi_2, varphi_2, a, n_wires = len(wires) n_if = n_wires * (n_wires - 1) // 2 - if qml.tape_mode_active(): - - # check that first dimension is the same - weights_list = [theta_1, phi_1, varphi_1, r, phi_r, theta_2, phi_2, varphi_2, a, phi_a, k] - shapes = [qml.math.shape(w) for w in weights_list] - - first_dims = [s[0] for s in shapes] - if len(set(first_dims)) > 1: - raise ValueError( - f"The first dimension of all parameters needs to be the same, got {first_dims}" - ) - repeat = shapes[0][0] - - second_dims = [s[1] for s in shapes] - expected = [ - n_if, - n_if, - n_wires, - n_wires, - n_wires, - n_if, - n_if, - n_wires, - n_wires, - n_wires, - n_wires, - ] - if not all(e == d for e, d in zip(expected, second_dims)): - raise ValueError("Got unexpected shape for one or more parameters.") - - else: - weights_list = [theta_1, phi_1, varphi_1, r, phi_r, theta_2, phi_2, varphi_2, a, phi_a, k] - repeat = check_number_of_layers(weights_list) - - expected_shapes = [ - (repeat, n_if), - (repeat, n_if), - (repeat, n_wires), - (repeat, n_wires), - (repeat, n_wires), - (repeat, n_if), - (repeat, n_if), - (repeat, n_wires), - (repeat, n_wires), - (repeat, n_wires), - (repeat, n_wires), - ] - check_shapes( - weights_list, expected_shapes, msg="Got unexpected shape for one or more parameters" + # check that first dimension is the same + weights_list = [theta_1, phi_1, varphi_1, r, phi_r, theta_2, phi_2, varphi_2, a, phi_a, k] + shapes = [qml.math.shape(w) for w in weights_list] + + first_dims = [s[0] for s in shapes] + if len(set(first_dims)) > 1: + raise ValueError( + f"The first dimension of all parameters needs to be the same, got {first_dims}" ) + repeat = shapes[0][0] + + second_dims = [s[1] for s in shapes] + expected = [ + n_if, + n_if, + n_wires, + n_wires, + n_wires, + n_if, + n_if, + n_wires, + n_wires, + n_wires, + n_wires, + ] + if not all(e == d for e, d in zip(expected, second_dims)): + raise ValueError("Got unexpected shape for one or more parameters.") return repeat diff --git a/pennylane/templates/layers/particle_conserving_u1.py b/pennylane/templates/layers/particle_conserving_u1.py index 2f303f080e0..4c274b3907a 100644 --- a/pennylane/templates/layers/particle_conserving_u1.py +++ b/pennylane/templates/layers/particle_conserving_u1.py @@ -50,37 +50,23 @@ def _preprocess(weights, wires, init_state): "got a wire sequence with {} elements".format(len(wires)) ) - if qml.tape_mode_active(): + shape = qml.math.shape(weights) - shape = qml.math.shape(weights) + if len(shape) != 3: + raise ValueError(f"Weights tensor must be 3-dimensional; got shape {shape}") - if len(shape) != 3: - raise ValueError(f"Weights tensor must be 3-dimensional; got shape {shape}") - - if shape[1] != len(wires) - 1: - raise ValueError( - f"Weights tensor must have second dimension of length {len(wires) - 1}; got {shape[1]}" - ) - - if shape[2] != 2: - raise ValueError( - f"Weights tensor must have third dimension of length 2; got {shape[2]}" - ) - - repeat = shape[0] - - else: - repeat = get_shape(weights)[0] + if shape[1] != len(wires) - 1: + raise ValueError( + f"Weights tensor must have second dimension of length {len(wires) - 1}; got {shape[1]}" + ) - expected_shape = (repeat, len(wires) - 1, 2) - check_shape( - weights, - expected_shape, - msg="Weights tensor must be of shape {}; got {}".format( - expected_shape, get_shape(weights) - ), + if shape[2] != 2: + raise ValueError( + f"Weights tensor must have third dimension of length 2; got {shape[2]}" ) + repeat = shape[0] + nm_wires = [wires.subset([l, l + 1]) for l in range(0, len(wires) - 1, 2)] nm_wires += [wires.subset([l, l + 1]) for l in range(1, len(wires) - 1, 2)] # we can extract the numpy representation here diff --git a/pennylane/templates/layers/particle_conserving_u2.py b/pennylane/templates/layers/particle_conserving_u2.py index 823bd660004..99cf1fbd963 100644 --- a/pennylane/templates/layers/particle_conserving_u2.py +++ b/pennylane/templates/layers/particle_conserving_u2.py @@ -49,32 +49,18 @@ def _preprocess(weights, wires, init_state): "got a wire sequence with {} elements".format(len(wires)) ) - if qml.tape_mode_active(): + shape = qml.math.shape(weights) - shape = qml.math.shape(weights) + if len(shape) != 2: + raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") - if len(shape) != 2: - raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") - - if shape[1] != 2 * len(wires) - 1: - raise ValueError( - f"Weights tensor must have a second dimension of length {2 * len(wires) - 1}; got {shape[1]}" - ) - - repeat = shape[0] - - else: - repeat = weights.shape[0] - - expected_shape = (repeat, 2 * len(wires) - 1) - check_shape( - weights, - expected_shape, - msg="Weights tensor must be of shape {}; got {}".format( - expected_shape, get_shape(weights) - ), + if shape[1] != 2 * len(wires) - 1: + raise ValueError( + f"Weights tensor must have a second dimension of length {2 * len(wires) - 1}; got {shape[1]}" ) + repeat = shape[0] + nm_wires = [wires.subset([l, l + 1]) for l in range(0, len(wires) - 1, 2)] nm_wires += [wires.subset([l, l + 1]) for l in range(1, len(wires) - 1, 2)] diff --git a/pennylane/templates/layers/random.py b/pennylane/templates/layers/random.py index 4e4a7d24e67..2a523e17ceb 100644 --- a/pennylane/templates/layers/random.py +++ b/pennylane/templates/layers/random.py @@ -38,28 +38,12 @@ def _preprocess(weights): Returns: int: number of times that the ansatz is repeated """ + shape = qml.math.shape(weights) - if qml.tape_mode_active(): - - shape = qml.math.shape(weights) - - if len(shape) != 2: - raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") - - repeat = shape[0] - - else: - repeat = check_number_of_layers([weights]) - n_rots = get_shape(weights)[1] - - expected_shape = (repeat, n_rots) - check_shape( - weights, - expected_shape, - msg="'weights' must be of shape {}; got {}" - "".format(expected_shape, get_shape(weights)), - ) + if len(shape) != 2: + raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") + repeat = shape[0] return repeat diff --git a/pennylane/templates/layers/simplified_two_design.py b/pennylane/templates/layers/simplified_two_design.py index 5a55995b5bb..194a2b8e48b 100644 --- a/pennylane/templates/layers/simplified_two_design.py +++ b/pennylane/templates/layers/simplified_two_design.py @@ -40,51 +40,26 @@ def _preprocess(weights, initial_layer_weights, wires): Returns: int: number of times that the ansatz is repeated """ + shape = qml.math.shape(weights) + repeat = shape[0] - if qml.tape_mode_active(): - - shape = qml.math.shape(weights) - repeat = shape[0] - - if len(shape) > 1: - if shape[1] != len(wires) - 1: - raise ValueError( - f"Weights tensor must have second dimension of length {len(wires) - 1}; got {shape[1]}" - ) - - if shape[2] != 2: - raise ValueError( - f"Weights tensor must have third dimension of length 2; got {shape[2]}" - ) - - shape2 = qml.math.shape(initial_layer_weights) - if shape2 != (len(wires),): + if len(shape) > 1: + if shape[1] != len(wires) - 1: raise ValueError( - f"Initial layer weights must be of shape {(len(wires),)}; got {shape2}" + f"Weights tensor must have second dimension of length {len(wires) - 1}; got {shape[1]}" ) - else: - repeat = check_number_of_layers([weights]) + if shape[2] != 2: + raise ValueError( + f"Weights tensor must have third dimension of length 2; got {shape[2]}" + ) - expected_shape_initial = (len(wires),) - check_shape( - initial_layer_weights, - expected_shape_initial, - msg="Initial layer weights must be of shape {}; got {}" - "".format(expected_shape_initial, get_shape(initial_layer_weights)), + shape2 = qml.math.shape(initial_layer_weights) + if shape2 != (len(wires),): + raise ValueError( + f"Initial layer weights must be of shape {(len(wires),)}; got {shape2}" ) - if len(wires) in [0, 1]: - expected_shape_weights = (0,) - else: - expected_shape_weights = (repeat, len(wires) - 1, 2) - - check_shape( - weights, - expected_shape_weights, - msg="Weights tensor must be of shape {}; got {}" - "".format(expected_shape_weights, get_shape(weights)), - ) return repeat diff --git a/pennylane/templates/layers/strongly_entangling.py b/pennylane/templates/layers/strongly_entangling.py index 33d5c8cc5ba..8752345c093 100644 --- a/pennylane/templates/layers/strongly_entangling.py +++ b/pennylane/templates/layers/strongly_entangling.py @@ -42,43 +42,21 @@ def _preprocess(weights, wires, ranges): Returns: int, list[int]: number of times that the ansatz is repeated and preprocessed ranges """ + shape = qml.math.shape(weights) + repeat = shape[0] - if qml.tape_mode_active(): + if len(shape) != 3: + raise ValueError(f"Weights tensor must be 3-dimensional; got shape {shape}") - shape = qml.math.shape(weights) - repeat = shape[0] - - if len(shape) != 3: - raise ValueError(f"Weights tensor must be 3-dimensional; got shape {shape}") - - if shape[1] != len(wires): - raise ValueError( - f"Weights tensor must have second dimension of length {len(wires)}; got {shape[1]}" - ) - - if shape[2] != 3: - raise ValueError( - f"Weights tensor must have third dimension of length 3; got {shape[2]}" - ) - - else: - - repeat = check_number_of_layers([weights]) - - expected_shape = (repeat, len(wires), 3) - check_shape( - weights, - expected_shape, - msg="Weights tensor must be of shape {}; got {}" - "".format(expected_shape, get_shape(weights)), + if shape[1] != len(wires): + raise ValueError( + f"Weights tensor must have second dimension of length {len(wires)}; got {shape[1]}" ) - if len(wires) > 1: - if ranges is None: - # tile ranges with iterations of range(1, n_wires) - ranges = [(l % (len(wires) - 1)) + 1 for l in range(repeat)] - else: - ranges = [0] * repeat + if shape[2] != 3: + raise ValueError( + f"Weights tensor must have third dimension of length 3; got {shape[2]}" + ) return repeat, ranges diff --git a/pennylane/templates/state_preparations/arbitrary_state_preparation.py b/pennylane/templates/state_preparations/arbitrary_state_preparation.py index 99e0464a4e1..01f223dee2e 100644 --- a/pennylane/templates/state_preparations/arbitrary_state_preparation.py +++ b/pennylane/templates/state_preparations/arbitrary_state_preparation.py @@ -30,24 +30,10 @@ def _preprocess(weights, wires): weights (tensor_like): trainable parameters of the template wires (Wires): wires that template acts on """ - - if qml.tape_mode_active(): - - shape = qml.math.shape(weights) - if shape != (2 ** (len(wires) + 1) - 2,): - raise ValueError( - f"Weights tensor must be of shape {(2 ** (len(wires) + 1) - 2,)}; got {shape}." - ) - - else: - - expected_shape = (2 ** (len(wires) + 1) - 2,) - check_shape( - weights, - expected_shape, - msg="Weights tensor must be of shape {}; got {}.".format( - expected_shape, get_shape(weights) - ), + shape = qml.math.shape(weights) + if shape != (2 ** (len(wires) + 1) - 2,): + raise ValueError( + f"Weights tensor must be of shape {(2 ** (len(wires) + 1) - 2,)}; got {shape}." ) diff --git a/pennylane/templates/state_preparations/basis.py b/pennylane/templates/state_preparations/basis.py index 8148c73ae01..1867c5f1c91 100644 --- a/pennylane/templates/state_preparations/basis.py +++ b/pennylane/templates/state_preparations/basis.py @@ -34,41 +34,22 @@ def _preprocess(basis_state, wires): Returns: array: preprocessed basis state """ + shape = qml.math.shape(basis_state) - if qml.tape_mode_active(): + if len(shape) != 1: + raise ValueError(f"Basis state must be one-dimensional; got shape {shape}.") - shape = qml.math.shape(basis_state) + n_bits = shape[0] + if n_bits != len(wires): + raise ValueError(f"Basis state must be of length {len(wires)}; got length {n_bits}.") - if len(shape) != 1: - raise ValueError(f"Basis state must be one-dimensional; got shape {shape}.") + basis_state = list(qml.math.toarray(basis_state)) - n_bits = shape[0] - if n_bits != len(wires): - raise ValueError(f"Basis state must be of length {len(wires)}; got length {n_bits}.") - - basis_state = list(qml.math.toarray(basis_state)) - - if not all(bit in [0, 1] for bit in basis_state): - raise ValueError(f"Basis state must only consist of 0s and 1s; got {basis_state}") - - # we return the input as a list of values, since - # it is not differentiable - return basis_state - - expected_shape = (len(wires),) - check_shape( - basis_state, - expected_shape, - msg="Basis state must be of shape {}; got {}." - "".format(expected_shape, get_shape(basis_state)), - ) - - # basis_state is guaranteed to be a list of binary values - if any([b not in [0, 1] for b in basis_state]): - raise ValueError( - "Basis state must only contain values of 0 and 1; got {}".format(basis_state) - ) + if not all(bit in [0, 1] for bit in basis_state): + raise ValueError(f"Basis state must only consist of 0s and 1s; got {basis_state}") + # we return the input as a list of values, since + # it is not differentiable return basis_state diff --git a/pennylane/templates/state_preparations/mottonen.py b/pennylane/templates/state_preparations/mottonen.py index 0c7a5e63ebf..df81cc5b164 100644 --- a/pennylane/templates/state_preparations/mottonen.py +++ b/pennylane/templates/state_preparations/mottonen.py @@ -20,7 +20,6 @@ from pennylane.templates.decorator import template from pennylane.templates.utils import check_shape, get_shape -from pennylane.variable import Variable from pennylane.wires import Wires @@ -38,48 +37,24 @@ def _preprocess(state_vector, wires): Returns: tensor_like, tensor_like, Wires: amplitudes a, phases omega and preprocessed wires """ - n_wires = len(wires) - if qml.tape_mode_active(): - - shape = qml.math.shape(state_vector) - - if len(shape) != 1: - raise ValueError(f"State vector must be a one-dimensional vector; got shape {shape}.") - - n_amplitudes = shape[0] - if n_amplitudes != 2 ** len(wires): - raise ValueError( - f"State vector must be of length {2 ** len(wires)} or less; got length {n_amplitudes}." - ) + shape = qml.math.shape(state_vector) - norm = qml.math.sum(qml.math.abs(state_vector) ** 2) - if not qml.math.allclose(norm, 1.0, atol=1e-3): - raise ValueError("State vector has to be of length 1.0, got {}".format(norm)) - a = qml.math.abs(state_vector) - omega = qml.math.angle(state_vector) + if len(shape) != 1: + raise ValueError(f"State vector must be a one-dimensional vector; got shape {shape}.") - else: - - expected_shape = (2 ** n_wires,) - check_shape( - state_vector, - expected_shape, - msg="State vector must be of shape {}; got {}." - "".format(expected_shape, get_shape(state_vector)), + n_amplitudes = shape[0] + if n_amplitudes != 2 ** len(wires): + raise ValueError( + f"State vector must be of length {2 ** len(wires)} or less; got length {n_amplitudes}." ) - if isinstance(state_vector[0], Variable): - state_vector = np.array([s.val for s in state_vector]) - - # check if normalized - norm = np.sum(np.abs(state_vector) ** 2) - if not np.isclose(norm, 1.0, atol=1e-3): - raise ValueError("State vector has to be of length 1.0, got {}".format(norm)) - - a = np.absolute(state_vector) - omega = np.angle(state_vector) + norm = qml.math.sum(qml.math.abs(state_vector) ** 2) + if not qml.math.allclose(norm, 1.0, atol=1e-3): + raise ValueError("State vector has to be of length 1.0, got {}".format(norm)) + a = qml.math.abs(state_vector) + omega = qml.math.angle(state_vector) # change ordering of wires, since original code was written for IBM machines wires_reverse = wires[::-1] diff --git a/pennylane/templates/subroutines/arbitrary_unitary.py b/pennylane/templates/subroutines/arbitrary_unitary.py index 5776a2794b0..622fd527201 100644 --- a/pennylane/templates/subroutines/arbitrary_unitary.py +++ b/pennylane/templates/subroutines/arbitrary_unitary.py @@ -31,22 +31,10 @@ def _preprocess(weights, wires): weights (tensor_like): trainable parameters of the template wires (Wires): wires that template acts on """ - - if qml.tape_mode_active(): - - shape = qml.math.shape(weights) - if shape != (4 ** len(wires) - 1,): - raise ValueError( - f"Weights tensor must be of shape {(4 ** len(wires) - 1,)}; got {shape}." - ) - - else: - expected_shape = (4 ** len(wires) - 1,) - check_shape( - weights, - expected_shape, - msg="Weights tensor must be of shape {}; got {}." - "".format(expected_shape, get_shape(weights)), + shape = qml.math.shape(weights) + if shape != (4 ** len(wires) - 1,): + raise ValueError( + f"Weights tensor must be of shape {(4 ** len(wires) - 1,)}; got {shape}." ) diff --git a/pennylane/templates/subroutines/double_excitation_unitary.py b/pennylane/templates/subroutines/double_excitation_unitary.py index 5175d2666d2..8335d1289f3 100644 --- a/pennylane/templates/subroutines/double_excitation_unitary.py +++ b/pennylane/templates/subroutines/double_excitation_unitary.py @@ -50,21 +50,9 @@ def _preprocess(weight, wires1, wires2): "got {}".format(len(wires2)) ) - if qml.tape_mode_active(): - - shape = qml.math.shape(weight) - if shape != (): - raise ValueError(f"Weight must be a scalar; got shape {shape}.") - - else: - - expected_shape = () - check_shape( - weight, - expected_shape, - msg="Weight must be a scalar; got shape {}".format(expected_shape, get_shape(weight)), - ) - + shape = qml.math.shape(weight) + if shape != (): + raise ValueError(f"Weight must be a scalar; got shape {shape}.") def _layer1(weight, s, r, q, p, set_cnot_wires): r"""Implement the first layer of the circuit to exponentiate the double-excitation diff --git a/pennylane/templates/subroutines/interferometer.py b/pennylane/templates/subroutines/interferometer.py index ee9bedff5da..ea90e566698 100644 --- a/pennylane/templates/subroutines/interferometer.py +++ b/pennylane/templates/subroutines/interferometer.py @@ -41,26 +41,17 @@ def _preprocess(theta, phi, varphi, wires): n_wires = len(wires) n_if = n_wires * (n_wires - 1) // 2 - if qml.tape_mode_active(): + shape = qml.math.shape(theta) + if shape != (n_if,): + raise ValueError(f"Theta must be of shape {(n_if,)}; got {shape}.") - shape = qml.math.shape(theta) - if shape != (n_if,): - raise ValueError(f"Theta must be of shape {(n_if,)}; got {shape}.") + shape = qml.math.shape(phi) + if shape != (n_if,): + raise ValueError(f"Phi must be of shape {(n_if,)}; got {shape}.") - shape = qml.math.shape(phi) - if shape != (n_if,): - raise ValueError(f"Phi must be of shape {(n_if,)}; got {shape}.") - - shape_varphi = qml.math.shape(varphi) - if shape_varphi != (n_wires,): - raise ValueError(f"Varphi must be of shape {(n_wires,)}; got {shape_varphi}.") - - else: - weights_list = [theta, phi, varphi] - - expected_shapes = [(n_if,), (n_if,), (n_wires,)] - check_shapes(weights_list, expected_shapes, msg="wrong shape of weight input(s) detected") - shape_varphi = get_shape(varphi) + shape_varphi = qml.math.shape(varphi) + if shape_varphi != (n_wires,): + raise ValueError(f"Varphi must be of shape {(n_wires,)}; got {shape_varphi}.") return shape_varphi diff --git a/pennylane/templates/subroutines/single_excitation_unitary.py b/pennylane/templates/subroutines/single_excitation_unitary.py index 1eb5adb5559..53e5a45fc72 100644 --- a/pennylane/templates/subroutines/single_excitation_unitary.py +++ b/pennylane/templates/subroutines/single_excitation_unitary.py @@ -40,19 +40,9 @@ def _preprocess(weight, wires): if len(wires) < 2: raise ValueError("expected at least two wires; got {}".format(len(wires))) - if qml.tape_mode_active(): - - shape = qml.math.shape(weight) - if shape != (): - raise ValueError(f"Weight must be a scalar tensor {()}; got shape {shape}.") - - else: - expected_shape = () - check_shape( - weight, - expected_shape, - msg="Weight must be a scalar; got shape {}".format(expected_shape, get_shape(weight)), - ) + shape = qml.math.shape(weight) + if shape != (): + raise ValueError(f"Weight must be a scalar tensor {()}; got shape {shape}.") @template diff --git a/pennylane/templates/subroutines/uccsd.py b/pennylane/templates/subroutines/uccsd.py index b617450ed62..f5b81767e68 100644 --- a/pennylane/templates/subroutines/uccsd.py +++ b/pennylane/templates/subroutines/uccsd.py @@ -63,22 +63,10 @@ def _preprocess(init_state, weights, s_wires, d_wires): ) ) - if qml.tape_mode_active(): - - shape = qml.math.shape(weights) - if shape != (len(s_wires) + len(d_wires),): - raise ValueError( - f"Weights tensor must be of shape {(len(s_wires) + len(d_wires),)}; got {shape}." - ) - - else: - expected_shape = (len(s_wires) + len(d_wires),) - check_shape( - weights, - expected_shape, - msg="Weights tensor must be of shape {}; got {}".format( - expected_shape, get_shape(weights) - ), + shape = qml.math.shape(weights) + if shape != (len(s_wires) + len(d_wires),): + raise ValueError( + f"Weights tensor must be of shape {(len(s_wires) + len(d_wires),)}; got {shape}." ) # we can extract the numpy representation here diff --git a/pennylane/templates/utils.py b/pennylane/templates/utils.py index e586d0a91aa..63b37bcf722 100644 --- a/pennylane/templates/utils.py +++ b/pennylane/templates/utils.py @@ -19,26 +19,6 @@ from collections.abc import Iterable import numpy as np -from pennylane.variable import Variable - - -def check_no_variable(arg, msg): - """Checks that ``arg`` does not represent or contain a :func:`~.pennylane.Variable` object. - - This ensures that the user has not passed ``arg`` to the qnode as a - primary argument. - - Args: - arg: argument to check - msg (str): error message to display - """ - - if isinstance(arg, Variable): - raise ValueError(msg) - - if isinstance(arg, Iterable): - if any([isinstance(a_, Variable) for a_ in arg]): - raise ValueError(msg) def check_wires(wires): @@ -81,7 +61,7 @@ def get_shape(inpt): """ # avoids incorrect assignment of shape - if isinstance(inpt, (float, int, complex, Variable)): + if isinstance(inpt, (float, int, complex)): shape = () else: diff --git a/pennylane/tape/transforms/__init__.py b/pennylane/transforms/__init__.py similarity index 76% rename from pennylane/tape/transforms/__init__.py rename to pennylane/transforms/__init__.py index df1f9816068..f46925145d3 100644 --- a/pennylane/tape/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This subpackage contains tape transforms. These are functions that transform one or more tapes to -other tapes are also provided. +This subpackage contains tape and QNode transforms. """ +from .classical_jacobian import classical_jacobian +from .draw import draw +from .measurement_grouping import measurement_grouping from .metric_tensor import metric_tensor diff --git a/pennylane/transforms/classical_jacobian.py b/pennylane/transforms/classical_jacobian.py new file mode 100644 index 00000000000..4dddce43b56 --- /dev/null +++ b/pennylane/transforms/classical_jacobian.py @@ -0,0 +1,57 @@ +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Contains the classical Jacobian transform +""" +import numpy as np +import pennylane as qml + + +def classical_jacobian(qnode): + """Function to extract the Jacobian + matrix of the classical part of a QNode""" + + def classical_preprocessing(*args, **kwargs): + """Returns the trainable gate parameters for + a given QNode input""" + qnode.construct(args, kwargs) + return qml.math.stack(qnode.qtape.get_parameters()) + + if qnode.interface == "autograd": + return qml.jacobian(classical_preprocessing) + + if qnode.interface == "torch": + import torch + + def _jacobian(*args, **kwargs): # pylint: disable=unused-argument + return torch.autograd.functional.jacobian(classical_preprocessing, args) + + return _jacobian + + if qnode.interface == "jax": + import jax + + return jax.jacobian(classical_preprocessing) + + if qnode.interface == "tf": + import tensorflow as tf + + def _jacobian(*args, **kwargs): + with tf.GradientTape() as tape: + tape.watch(args) + gate_params = classical_preprocessing(*args, **kwargs) + + return tape.jacobian(gate_params, args) + + return _jacobian diff --git a/pennylane/transforms/draw.py b/pennylane/transforms/draw.py new file mode 100644 index 00000000000..b97de92bb5f --- /dev/null +++ b/pennylane/transforms/draw.py @@ -0,0 +1,90 @@ +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Contains the drawing function. +""" + + +def draw(qnode, charset="unicode", wire_order=None, show_all_wires=False): + """Create a function that draws the given qnode. + + Args: + qnode (.QNode): the input QNode that is to be drawn. + charset (str, optional): The charset that should be used. Currently, "unicode" and + "ascii" are supported. + wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit + show_all_wires (bool): If True, all wires, including empty wires, are printed. + + Returns: + A function that has the same arguement signature as ``qnode``. When called, + the function will draw the QNode. + + **Example** + + Given the following definition of a QNode, + + .. code-block:: python3 + + @qml.qnode(dev) + def circuit(a, w): + qml.Hadamard(0) + qml.CRX(a, wires=[0, 1]) + qml.Rot(*w, wires=[1]) + qml.CRX(-a, wires=[0, 1]) + return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + + We can draw the it like such: + + >>> drawer = qml.draw(circuit) + >>> drawer(a=2.3, w=[1.2, 3.2, 0.7]) + 0: ──H──╭C────────────────────────────╭C─────────╭┤ ⟨Z ⊗ Z⟩ + 1: ─────╰RX(2.3)──Rot(1.2, 3.2, 0.7)──╰RX(-2.3)──╰┤ ⟨Z ⊗ Z⟩ + + Circuit drawing works with devices with custom wire labels: + + .. code-block:: python3 + + dev = qml.device('default.qubit', wires=["a", -1, "q2"]) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(wires=-1) + qml.CNOT(wires=["a", "q2"]) + qml.RX(0.2, wires="a") + return qml.expval(qml.PauliX(wires="q2")) + + When printed, the wire order matches the order defined on the device: + + >>> drawer = qml.draw(circuit) + >>> drawer() + a: ─────╭C──RX(0.2)──┤ + -1: ──H──│────────────┤ + q2: ─────╰X───────────┤ ⟨X⟩ + + We can use the ``wire_order`` argument to change the wire order: + + >>> drawer = qml.draw(circuit, wire_order=["q2", "a", -1]) + >>> drawer() + q2: ──╭X───────────┤ ⟨X⟩ + a: ──╰C──RX(0.2)──┤ + -1: ───H───────────┤ + """ + @wraps(qnode) + def wrapper(*args, **kwargs): + qnode.construct(args, kwargs) + _wire_order = wire_order or qnode.device.wires + _wire_order = qml.wires.Wires(_wire_order) + return qnode.qtape.draw(charset, wire_order=_wire_order, show_all_wires=show_all_wires) + + return wrapper diff --git a/pennylane/transforms/measurement_grouping.py b/pennylane/transforms/measurement_grouping.py new file mode 100644 index 00000000000..1f52d03752b --- /dev/null +++ b/pennylane/transforms/measurement_grouping.py @@ -0,0 +1,40 @@ +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Contains the measurement grouping transform +""" +import numpy as np +import pennylane as qml + + +def measurement_grouping(tape, obs_list, coeffs_list): + obs_groupings, coeffs_groupings = qml.grouping.group_observables(obs_list, coeffs_list) + tapes = [] + + for obs in obs_groupings: + + with tape.__class__() as new_tape: + for op in tape.operations: + op.queue() + + for o in obs: + qml.expval(o) + + new_tape = new_tape.expand(stop_at=lambda obj: True) + tapes.append(new_tape) + + def processing_fn(res): + return qml.math.sum([qml.math.dot(c, r) for c, r in zip(coeffs_groupings, res)]) + + return tapes, processing_fn diff --git a/pennylane/tape/transforms/metric_tensor.py b/pennylane/transforms/metric_tensor.py similarity index 60% rename from pennylane/tape/transforms/metric_tensor.py rename to pennylane/transforms/metric_tensor.py index d339ca2b70f..e415f91bbc0 100644 --- a/pennylane/tape/transforms/metric_tensor.py +++ b/pennylane/transforms/metric_tensor.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Contains the metric tensor tape transform +Contains the metric tensor transform """ import numpy as np import pennylane as qml @@ -28,7 +28,7 @@ def _stopping_critera(obj): return False -def metric_tensor(tape, diag_approx=False, wrt=None): +def metric_tensor_tape(tape, diag_approx=False, wrt=None): """Returns a list of tapes, and a classical processing function, for computing the block diagronal metric tensor approximation of an input tape on hardware. @@ -74,7 +74,7 @@ def metric_tensor(tape, diag_approx=False, wrt=None): We can use the ``metric_tensor`` transform to generate a new tapes and a classical processing function for computing the metric tensor. - >>> mt_tapes, fn = qml.tape.transforms.metric_tensor(tape) + >>> mt_tapes, fn = qml.transforms.metric_tensor_tape(tape) >>> print(mt_tapes) [, ] >>> print(mt_tapes[0].draw()) @@ -173,3 +173,106 @@ def processing_fn(probs): return qml.math.block_diag(gs) return metric_tensor_tapes, processing_fn + + + +def metric_tensor(qnode, diag_approx=False, only_construct=False): + """Returns a function that returns the value of the metric tensor + of a given QNode. + + .. note:: + + Currently, only the :class:`~.RX`, :class:`~.RY`, :class:`~.RZ`, and + :class:`~.PhaseShift` parametrized gates are supported. + All other parametrized gates will be decomposed if possible. + + Args: + qnode (.QNode or .ExpvalCost): QNode(s) to compute the metric tensor of + diag_approx (bool): iff True, use the diagonal approximation + only_construct (bool): Iff True, construct the circuits used for computing + the metric tensor but do not execute them, and return the tapes. + + Returns: + func: Function which accepts the same arguments as the QNode. When called, this + function will return the metric tensor. + + **Example** + + Consider the following QNode: + + .. code-block:: python + + dev = qml.device("default.qubit", wires=3) + + @qml.qnode(dev, interface="autograd") + def circuit(weights): + # layer 1 + qml.RX(weights[0, 0], wires=0) + qml.RX(weights[0, 1], wires=1) + + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=[1, 2]) + + # layer 2 + qml.RZ(weights[1, 0], wires=0) + qml.RZ(weights[1, 1], wires=2) + + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=[1, 2]) + return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), qml.expval(qml.PauliY(2)) + + We can use the ``metric_tensor`` function to generate a new function, that returns the + metric tensor of this QNode: + + >>> met_fn = qml.metric_tensor(circuit) + >>> weights = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], requires_grad=True) + >>> met_fn(weights) + tensor([[0.25 , 0. , 0. , 0. ], + [0. , 0.25 , 0. , 0. ], + [0. , 0. , 0.0025, 0.0024], + [0. , 0. , 0.0024, 0.0123]], requires_grad=True) + + The returned metric tensor is also fully differentiable, in all interfaces. + For example, differentiating the ``(3, 2)`` element: + + >>> grad_fn = qml.grad(lambda x: met_fn(x)[3, 2]) + >>> grad_fn(weights) + array([[ 0.04867729, -0.00049502, 0. ], + [ 0. , 0. , 0. ]]) + """ + if qnode.__class__.__name__ == "ExpvalCost": + if qnode._multiple_devices: # pylint: disable=protected-access + warnings.warn( + "ExpvalCost was instantiated with multiple devices. Only the first device " + "will be used to evaluate the metric tensor." + ) + + qnode = qnode.qnodes.qnodes[0] + + def _metric_tensor_fn(*args, **kwargs): + jac = qml.math.stack(qml.transforms.classical_jacobian(qnode)(*args, **kwargs)) + jac = qml.math.reshape(jac, [qnode.qtape.num_params, -1]) + + wrt, perm = np.nonzero(qml.math.toarray(jac)) + perm = np.argsort(np.argsort(perm)) + + qnode.construct(args, kwargs) + + metric_tensor_tapes, processing_fn = metric_tensor_tape( + qnode.qtape, + diag_approx=diag_approx, + wrt=wrt.tolist() if qnode.diff_options["method"] == "backprop" else None, + ) + + if only_construct: + return metric_tensor_tapes + + res = [t.execute(device=qnode.device) for t in metric_tensor_tapes] + mt = processing_fn(res) + + # permute rows ad columns + mt = qml.math.gather(mt, perm) + mt = qml.math.gather(qml.math.T(mt), perm) + return mt + + return _metric_tensor_fn diff --git a/pennylane/utils.py b/pennylane/utils.py index a8d8d5be372..af5a0254123 100644 --- a/pennylane/utils.py +++ b/pennylane/utils.py @@ -326,35 +326,24 @@ def circuit2(): + ",".join(string_reps) ) - if qml.tape_mode_active(): - for op in operation_list: - try: - # remove the queued operation to be inverted - # from the existing queuing context - qml.tape.QueuingContext.remove(op) - except KeyError: - # operation to be inverted does not - # exist on the queuing context - pass - - with qml.tape.QuantumTape() as tape: - for o in operation_list: - o.queue() - if o.inverse: - o.inv() - - tape.inv() - return tape - - inv_ops = [op.inv() for op in reversed(copy.deepcopy(operation_list))] - for op in operation_list: - qml.QueuingContext.remove(op) - - for inv_op in inv_ops: - qml.QueuingContext.append(inv_op) - - return inv_ops + try: + # remove the queued operation to be inverted + # from the existing queuing context + qml.tape.QueuingContext.remove(op) + except KeyError: + # operation to be inverted does not + # exist on the queuing context + pass + + with qml.tape.QuantumTape() as tape: + for o in operation_list: + o.queue() + if o.inverse: + o.inv() + + tape.inv() + return tape def expand(matrix, original_wires, expanded_wires): diff --git a/pennylane/variable.py b/pennylane/variable.py deleted file mode 100644 index 8c9bf165ba6..00000000000 --- a/pennylane/variable.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the :class:`Variable` class, which is used to track -and identify :class:`~pennylane.qnode.QNode` parameters. - -Description ------------ - -The first time a QNode is evaluated (either by calling :meth:`~.QNode.evaluate`, -:meth:`~.QNode.__call__`, or :meth:`~.QNode.jacobian`), the :meth:`~.QNode.construct` -method is called, which performs a 'just-in-time' circuit construction -on the :mod:`~pennylane._device.Device`. As part of this construction, all arguments -and keyword arguments are wrapped in a `Variable` as follows: - -* All positional arguments in ``*args``, including those with multiple dimensions, are - flattened to a single list, and each element wrapped as a Variable instance, - indexed by its position in the list. - - This allows PennyLane to inspect the shape and type of arguments - the user wishes to pass. The list can then be unflattened back to the original - shape of ``*args``. - - -* The same is done for each keyword argument in ``**kwargs``, the only - difference being that the name of each contained Variable corresponds - with the keyword name. - -As a result, the device stores a list of operations and expectations, with all -free parameters stored as Variable instances. - -.. note:: - The QNode can be differentiated with respect to positional arguments, - but *not* with respect to keyword arguments. This makes keyword arguments - a natural location for data placeholders. - -.. important:: - If the user defines a keyword argument, then they always have to pass the - corresponding variable as a keyword argument, otherwise it won't register. - -For each successive QNode execution, the user-provided values for the positional and keyword -arguments are stored in :attr:`Variable.positional_arg_values` and -:attr:`Variable.kwarg_values` respectively; the values are -then returned by :meth:`Variable.val`, using the Variable's ``idx`` attribute, and, for -keyword arguments, its ``name``, to return the correct value to the operation. - -.. note:: - The :meth:`Operation.parameters() ` - property automates the process of unpacking the Variable value. - The attribute :meth:`Variable.val` should not need to be accessed outside of advanced usage. -""" -import copy - - -class Variable: - """A reference to dynamically track and update circuit parameters. - - Represents a free quantum circuit parameter (with a non-fixed value), - or a placeholder for data/other hard-coded data. - - Each time the circuit is executed, it is given a vector of flattened positional argument values, - and a dictionary mapping keyword-only argument names to vectors of their flattened values. - Each element of these vectors corresponds to a Variable instance. - Positional arguments are represented by nameless Variables, whereas for keyword-only - arguments :attr:`Variable.name` contains the argument name. - In both cases :attr:`Variable.idx` is an index into the argument value vector. - - The Variable has an optional scalar multiplier for the argument it represents. - - .. note:: Variables currently do not implement any arithmetic - operations other than scalar multiplication. - - Args: - idx (int): index into the value vector, >= 0 - name (None, str): name of the argument - """ - - # pylint: disable=too-few-public-methods - - #: array[float]: current positional parameter values, set in :meth:`.BaseQNode._set_variables` - positional_arg_values = None - - #: dict[str->array[float]]: current auxiliary parameter values, set in :meth:`.BaseQNode._set_variables` - kwarg_values = None - - def __init__(self, idx, name=None, is_kwarg=False): - self.idx = idx #: int: parameter index - self.name = name #: str: parameter name - self.idx = idx #: int: parameter index - self.mult = 1 #: int, float: parameter scalar multiplier - self.is_kwarg = is_kwarg - - def __repr__(self): - temp = " * {}".format(self.mult) if self.mult != 1.0 else "" - return "".format(self.name, self.idx, temp) - - def __str__(self): - temp = ", * {}".format(self.mult) if self.mult != 1.0 else "" - return "Variable: name = {}, idx = {}{}".format(self.name, self.idx, temp) - - def __eq__(self, other): - if not isinstance(other, Variable): - return False - - return ( - self.name == other.name - and self.idx == other.idx - and self.is_kwarg == other.is_kwarg - and self.mult == other.mult - ) - - def __neg__(self): - """Unary negation.""" - temp = copy.copy(self) - temp.mult = -temp.mult - return temp - - def __mul__(self, scalar): - """Right multiplication by scalars.""" - temp = copy.copy(self) - temp.mult *= scalar - return temp - - def __truediv__(self, scalar): - """Right division by scalars. Left division is not allowed.""" - temp = copy.copy(self) - temp.mult /= scalar - return temp - - __rmul__ = __mul__ # Left multiplication by scalars. - - @property - def val(self): - """Current numerical value of the Variable. - - Returns: - float: current value of the Variable - """ - # pylint: disable=unsubscriptable-object - if not self.is_kwarg: - # The variable is a placeholder for a positional argument - return Variable.positional_arg_values[self.idx] * self.mult - - # The variable is a placeholder for a keyword argument - values = Variable.kwarg_values[self.name] - return values[self.idx] * self.mult - - def render(self, show_name_only=False): - """Returns a string representation of the Variable. - - Args: - show_name_only (bool, optional): Render the name instead of the value. - - Returns: - str: A string representation of the Variable - """ - if not show_name_only: - if self.is_kwarg and Variable.kwarg_values and self.name in Variable.kwarg_values: - return f"{self.val:.3g}" - - if ( - not self.is_kwarg - and Variable.positional_arg_values is not None - and len(Variable.positional_arg_values) > self.idx - ): - return f"{self.val:.3g}" - - if self.mult != 1: - return f"{self.mult:.3g}*{self.name}" - - return self.name diff --git a/pennylane/vqe/vqe.py b/pennylane/vqe/vqe.py index 2df03ea913a..44ce6ab42d7 100644 --- a/pennylane/vqe/vqe.py +++ b/pennylane/vqe/vqe.py @@ -486,7 +486,6 @@ def __init__( self._multiple_devices = isinstance(device, Sequence) """Bool: Records if multiple devices are input""" - tape_mode = qml.tape_mode_active() self._optimize = optimize self.qnodes = qml.map( @@ -494,12 +493,6 @@ def __init__( ) if self._optimize: - if not tape_mode: - raise ValueError( - "Observable optimization is only supported in tape mode. Tape " - "mode can be enabled with the command:\n" - "qml.enable_tape()" - ) if self._multiple_devices: raise ValueError("Using multiple devices is not supported when optimize=True") diff --git a/tests/circuit_drawer/test_circuit_drawer.py b/tests/circuit_drawer/test_circuit_drawer.py index 0fa6c29dfdc..f0d4440810f 100644 --- a/tests/circuit_drawer/test_circuit_drawer.py +++ b/tests/circuit_drawer/test_circuit_drawer.py @@ -24,10 +24,7 @@ from pennylane.circuit_drawer.grid import Grid, _transpose from pennylane.wires import Wires -from pennylane.tape.measure import state - - -pytestmark = pytest.mark.usefixtures("tape_mode") +from pennylane.measure import state class TestFunctions: @@ -666,16 +663,6 @@ def drawn_qubit_circuit_with_interesting_wires(): class TestCircuitDrawerIntegration: """Test that QNodes are properly drawn.""" - def test_qubit_circuit_with_variable_names( - self, parameterized_qubit_qnode, drawn_parameterized_qubit_circuit_with_variable_names - ): - """Test that a parametrized qubit circuit renders correctly with variable names.""" - if qml.tape_mode_active(): - pytest.skip("show_variable_names not supported in tape mode") - - output = parameterized_qubit_qnode.draw(show_variable_names=True) - assert output == drawn_parameterized_qubit_circuit_with_variable_names - def test_qubit_circuit_with_values( self, parameterized_qubit_qnode, drawn_parameterized_qubit_circuit_with_values ): @@ -683,19 +670,6 @@ def test_qubit_circuit_with_values( output = parameterized_qubit_qnode.draw(show_variable_names=False) assert output == drawn_parameterized_qubit_circuit_with_values - def test_wide_qubit_circuit_with_variable_names( - self, - parameterized_wide_qubit_qnode, - drawn_parameterized_wide_qubit_qnode_with_variable_names, - ): - """Test that a wide parametrized qubit circuit renders correctly with variable names.""" - if qml.tape_mode_active(): - pytest.skip("show_variable_names not supported in tape mode") - - output = parameterized_wide_qubit_qnode.draw(show_variable_names=True) - - assert output == drawn_parameterized_wide_qubit_qnode_with_variable_names - def test_wide_qubit_circuit_with_values( self, parameterized_wide_qubit_qnode, drawn_parameterized_wide_qubit_qnode_with_values ): @@ -718,17 +692,6 @@ def test_wide_cv_circuit(self, wide_cv_qnode, drawn_wide_cv_qnode): assert output == drawn_wide_cv_qnode - def test_cv_circuit_with_variable_names( - self, parameterized_cv_qnode, drawn_parameterized_cv_qnode_with_variable_names - ): - """Test that a parametrized CV circuit renders correctly with variable names.""" - if qml.tape_mode_active(): - pytest.skip("show_variable_names not supported in tape mode") - - output = parameterized_cv_qnode.draw(show_variable_names=True) - - assert output == drawn_parameterized_cv_qnode_with_variable_names - def test_cv_circuit_with_values( self, parameterized_cv_qnode, drawn_parameterized_cv_qnode_with_values ): @@ -752,8 +715,6 @@ def test_qubit_circuit_with_probs( assert output == drawn_qubit_circuit_with_probs - # This test is expected to fail until the new quantum-tape core has been fully merged - @pytest.mark.xfail def test_qubit_circuit_with_state( self, qubit_circuit_with_state, drawn_qubit_circuit_with_state ): @@ -787,23 +748,11 @@ def qfunc(a, w): + " 1: -----+RX(2.3)--Rot(1.2, 3.2, 0.7)--+RX(-2.3)--+| \n" ) - if not qml.tape_mode_active(): - assert qfunc.draw(show_variable_names=True) == ( - " 0: ──H──╭C─────────────────────────────╭C─────────╭┤ ⟨Z ⊗ Z⟩ \n" - + " 1: ─────╰RX(a)──Rot(w[0], w[1], w[2])──╰RX(-1*a)──╰┤ ⟨Z ⊗ Z⟩ \n" - ) - class TestWireOrdering: """Tests for wire ordering functionality""" - @pytest.fixture - def tape_only(self): - """Ensures tests only run in tape mode""" - if not qml.tape_mode_active(): - pytest.skip("Tests only run in tape mode") - - def test_default_ordering(self, tape_only): + def test_default_ordering(self): """Test that the default wire ordering matches the device""" dev = qml.device('default.qubit', wires=["a", -1, "q2"]) @@ -825,7 +774,7 @@ def circuit(): assert res == "\n".join(expected) - def test_wire_reordering(self, tape_only): + def test_wire_reordering(self): """Test that wires are correctly reordered""" dev = qml.device('default.qubit', wires=["a", -1, "q2"]) @@ -847,7 +796,7 @@ def circuit(): assert res == "\n".join(expected) - def test_include_empty_wires(self, tape_only): + def test_include_empty_wires(self): """Test that empty wires are correctly included""" dev = qml.device('default.qubit', wires=[-1, "a", "q2", 0]) @@ -869,7 +818,7 @@ def circuit(): assert res == "\n".join(expected) - def test_show_all_wires_error(self, tape_only): + def test_show_all_wires_error(self): """Test that show_all_wires will raise an error if the provided wire order does not contain all wires on the device""" @@ -886,7 +835,7 @@ def circuit(): with pytest.raises(ValueError, match="must contain all wires"): circuit.draw(show_all_wires=True, wire_order=[-1, "a"]) - def test_missing_wire(self, tape_only): + def test_missing_wire(self): """Test that wires not specifically mentioned in the wire reordering are appended at the bottom of the circuit drawing""" @@ -931,7 +880,7 @@ def circuit(): assert res == "\n".join(expected) - def test_invalid_wires(self, tape_only): + def test_invalid_wires(self): """Test that an exception is raised if a wire in the wire ordering does not exist on the device""" dev = qml.device('default.qubit', wires=["a", -1, "q2"]) diff --git a/tests/circuit_drawer/test_grid.py b/tests/circuit_drawer/test_grid.py index e9236da3c90..bb7ddc05788 100644 --- a/tests/circuit_drawer/test_grid.py +++ b/tests/circuit_drawer/test_grid.py @@ -21,9 +21,6 @@ from pennylane.circuit_drawer.grid import _transpose -pytestmark = pytest.mark.usefixtures("tape_mode") - - class TestFunctions: """Test the helper functions.""" diff --git a/tests/circuit_drawer/test_representation_resolver.py b/tests/circuit_drawer/test_representation_resolver.py index d57788d25f3..df4fc75eaa2 100644 --- a/tests/circuit_drawer/test_representation_resolver.py +++ b/tests/circuit_drawer/test_representation_resolver.py @@ -20,11 +20,7 @@ import pennylane as qml from pennylane.circuit_drawer import RepresentationResolver -from pennylane.variable import Variable -from pennylane.tape.measure import state - - -pytestmark = pytest.mark.usefixtures("tape_mode") +from pennylane.measure import state @pytest.fixture @@ -39,26 +35,6 @@ def ascii_representation_resolver(): return RepresentationResolver(charset=qml.circuit_drawer.AsciiCharSet) -@pytest.fixture -def unicode_representation_resolver_varnames(): - """An instance of a RepresentationResolver with unicode charset and show_variable_names=True.""" - return RepresentationResolver(show_variable_names=True) - - -@pytest.fixture -def variable(monkeypatch): - """A mocked Variable instance for a non-keyword variable.""" - monkeypatch.setattr(Variable, "positional_arg_values", [0, 1, 2, 3]) - yield Variable(2, "test") - - -@pytest.fixture -def kwarg_variable(monkeypatch): - """A mocked Variable instance for a keyword variable.""" - monkeypatch.setattr(Variable, "kwarg_values", {"kwarg_test": [0, 1, 2, 3]}) - yield Variable(1, "kwarg_test", True) - - class TestRepresentationResolver: """Test the RepresentationResolver class.""" @@ -81,52 +57,6 @@ def test_single_parameter_representation(self, unicode_representation_resolver, """Test that single parameters are properly resolved.""" assert unicode_representation_resolver.single_parameter_representation(par) == expected - def test_single_parameter_representation_variable( - self, unicode_representation_resolver, variable - ): - """Test that variables are properly resolved.""" - - assert unicode_representation_resolver.single_parameter_representation(variable) == "2" - - def test_single_parameter_representation_kwarg_variable( - self, unicode_representation_resolver, kwarg_variable - ): - """Test that kwarg variables are properly resolved.""" - - assert ( - unicode_representation_resolver.single_parameter_representation(kwarg_variable) == "1" - ) - - @pytest.mark.parametrize("par,expected", [(3, "3"), (5.236422, "5.24"),]) - def test_single_parameter_representation_varnames( - self, unicode_representation_resolver_varnames, par, expected - ): - """Test that single parameters are properly resolved when show_variable_names is True.""" - assert ( - unicode_representation_resolver_varnames.single_parameter_representation(par) - == expected - ) - - def test_single_parameter_representation_variable_varnames( - self, unicode_representation_resolver_varnames, variable - ): - """Test that variables are properly resolved when show_variable_names is True.""" - - assert ( - unicode_representation_resolver_varnames.single_parameter_representation(variable) - == "test" - ) - - def test_single_parameter_representation_kwarg_variable_varnames( - self, unicode_representation_resolver_varnames, kwarg_variable - ): - """Test that kwarg variables are properly resolved when show_variable_names is True.""" - - assert ( - unicode_representation_resolver_varnames.single_parameter_representation(kwarg_variable) - == "kwarg_test" - ) - @pytest.mark.parametrize( "op,wire,target", [ diff --git a/tests/circuit_graph/test_circuit_graph.py b/tests/circuit_graph/test_circuit_graph.py index 5782f26fd62..b03371f62c9 100644 --- a/tests/circuit_graph/test_circuit_graph.py +++ b/tests/circuit_graph/test_circuit_graph.py @@ -25,7 +25,7 @@ @pytest.fixture -def queue(): +def ops(): """A fixture of a complex example of operations that depend on previous operations.""" return [ qml.RX(0.43, wires=0), @@ -48,15 +48,9 @@ def obs(): @pytest.fixture -def ops(queue, obs): - """Queue of Operations followed by Observables.""" - return queue + obs - - -@pytest.fixture -def circuit(ops): +def circuit(ops, obs): """A fixture of a circuit generated based on the queue and obs fixtures above.""" - circuit = CircuitGraph(ops, {}, Wires([0, 1, 2])) + circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) return circuit @@ -89,51 +83,55 @@ def test_no_dependence(self): ops = [qml.RX(0.43, wires=0), qml.RY(0.35, wires=1)] - res = CircuitGraph(ops, {}, Wires([0, 1])).graph + res = CircuitGraph(ops, [], Wires([0, 1])).graph assert len(res) == 2 assert not res.edges() - def test_dependence(self, ops): + def test_dependence(self, ops, obs): """Test a more complex example containing operations that do depend on the result of previous operations""" - circuit = CircuitGraph(ops, {}, Wires([0, 1, 2])) + circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) graph = circuit.graph assert len(graph) == 9 assert len(graph.edges()) == 9 + queue = ops + obs + # all ops should be nodes in the graph - for k in ops: + for k in queue: assert k in graph.nodes # all nodes in the graph should be ops for k in graph.nodes: - assert k is ops[k.queue_idx] + assert k is queue[k.queue_idx] # Finally, checking the adjacency of the returned DAG: assert set(graph.edges()) == set( - (ops[a], ops[b]) + (queue[a], queue[b]) for a, b in [(0, 3), (1, 3), (2, 4), (3, 5), (3, 6), (4, 5), (5, 7), (5, 8), (6, 8),] ) - def test_ancestors_and_descendants_example(self, ops): + def test_ancestors_and_descendants_example(self, ops, obs): """ Test that the ``ancestors`` and ``descendants`` methods return the expected result. """ - circuit = CircuitGraph(ops, {}, Wires([0, 1, 2])) + circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) + + queue = ops + obs - ancestors = circuit.ancestors([ops[6]]) + ancestors = circuit.ancestors([queue[6]]) assert len(ancestors) == 3 for o_idx in (0, 1, 3): - assert ops[o_idx] in ancestors + assert queue[o_idx] in ancestors - descendants = circuit.descendants([ops[6]]) - assert descendants == set([ops[8]]) + descendants = circuit.descendants([queue[6]]) + assert descendants == set([queue[8]]) - def test_update_node(self, ops): + def test_update_node(self, ops, obs): """Changing nodes in the graph.""" - circuit = CircuitGraph(ops, {}, Wires([0, 1, 2])) + circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) new = qml.RX(0.1, wires=0) circuit.update_node(ops[0], new) assert circuit.operations[0] is new @@ -142,9 +140,9 @@ def test_observables(self, circuit, obs): """Test that the `observables` property returns the list of observables in the circuit.""" assert circuit.observables == obs - def test_operations(self, circuit, queue): + def test_operations(self, circuit, ops): """Test that the `operations` property returns the list of operations in the circuit.""" - assert circuit.operations == queue + assert circuit.operations == ops def test_op_indices(self, circuit): """Test that for the given circuit, this method will fetch the correct operation indices for @@ -163,7 +161,7 @@ def test_layers(self, parameterized_circuit, wires): dev = qml.device("default.gaussian", wires=wires) qnode = qml.QNode(parameterized_circuit, dev) - qnode.construct((0.1, 0.2, 0.3, 0.4, 0.5, 0.6), {}) + qnode(0.1, 0.2, 0.3, 0.4, 0.5, 0.6) circuit = qnode.qtape.graph layers = circuit.parametrized_layers ops = circuit.operations @@ -182,7 +180,7 @@ def test_iterate_layers(self, parameterized_circuit, wires): dev = qml.device("default.gaussian", wires=wires) qnode = qml.QNode(parameterized_circuit, dev) - qnode.construct((0.1, 0.2, 0.3, 0.4, 0.5, 0.6), {}) + qnode(0.1, 0.2, 0.3, 0.4, 0.5, 0.6) circuit = qnode.qtape.graph result = list(circuit.iterate_parametrized_layers()) @@ -201,21 +199,3 @@ def test_iterate_layers(self, parameterized_circuit, wires): assert set(result[2][1]) == set(circuit.operations[5:]) assert result[2][2] == (6, 7) assert set(result[2][3]) == set(circuit.observables[1:]) - - def test_diagonalizing_gates(self): - """Tests that the diagonalizing gates are correct for a circuit""" - circuit = CircuitGraph([qml.expval(qml.PauliX(0)), qml.var(qml.PauliZ(1))], {}, Wires([0, 1])) - diag_gates = circuit.diagonalizing_gates - - assert len(diag_gates) == 1 - assert isinstance(diag_gates[0], qml.Hadamard) - assert diag_gates[0].wires == Wires([0]) - - def test_is_sampled(self): - """Test that circuit graphs with sampled observables properly return - True for CircuitGraph.is_sampled""" - circuit = CircuitGraph([qml.expval(qml.PauliX(0)), qml.var(qml.PauliZ(1))], {}, Wires([0, 1])) - assert not circuit.is_sampled - - circuit = CircuitGraph([qml.expval(qml.PauliX(0)), qml.sample(qml.PauliZ(1))], {}, Wires([0, 1])) - assert circuit.is_sampled diff --git a/tests/circuit_graph/test_circuit_graph_hash.py b/tests/circuit_graph/test_circuit_graph_hash.py index bd92f9b11d1..3b2a9a2e855 100644 --- a/tests/circuit_graph/test_circuit_graph_hash.py +++ b/tests/circuit_graph/test_circuit_graph_hash.py @@ -20,14 +20,9 @@ import pennylane as qml from pennylane.operation import Tensor from pennylane.circuit_graph import CircuitGraph -from pennylane.qnodes import BaseQNode -from pennylane.variable import Variable from pennylane.wires import Wires -pytestmark = pytest.mark.usefixtures("tape_mode") - - class TestCircuitGraphHash: """Test the creation of a hash on a CircuitGraph""" @@ -57,104 +52,6 @@ def test_serialize_numeric_arguments(self, queue, observable_queue, expected_str assert circuit_graph_1.serialize() == circuit_graph_2.serialize() assert expected_string == circuit_graph_1.serialize() - - variable = Variable(1) - - symbolic_queue = [ - ([qml.RX(variable, wires=[0])], - [], - 'RX!V1![0]|||' - ), - - ] - - - @pytest.mark.parametrize("queue, observable_queue, expected_string", symbolic_queue) - def test_serialize_symbolic_argument(self, queue, observable_queue, expected_string): - """Tests that the same hash is created for two circuitgraphs that have symbolic arguments.""" - circuit_graph_1 = CircuitGraph(queue + observable_queue, {}, Wires([0])) - circuit_graph_2 = CircuitGraph(queue + observable_queue, {}, Wires([0])) - - assert circuit_graph_1.serialize() == circuit_graph_2.serialize() - assert expected_string == circuit_graph_1.serialize() - - - variable = Variable(1) - - symbolic_queue = [ - ([ - qml.RX(variable, wires=[0]), - qml.RX(0.3, wires=[1]), - qml.RX(variable, wires=[2]) - ], - [], - 'RX!V1![0]RX!0.3![1]RX!V1![2]|||' - ), - - ] - - - @pytest.mark.parametrize("queue, observable_queue, expected_string", symbolic_queue) - def test_serialize_numeric_and_symbolic_argument(self, queue, observable_queue, expected_string): - """Tests that the same hash is created for two circuitgraphs that have both numeric and symbolic arguments.""" - - circuit_graph_1 = CircuitGraph(queue + observable_queue, {}, Wires([0, 1, 2])) - circuit_graph_2 = CircuitGraph(queue + observable_queue, {}, Wires([0, 1, 2])) - - assert circuit_graph_1.serialize() == circuit_graph_2.serialize() - assert expected_string == circuit_graph_1.serialize() - - variable = Variable(1) - - many_symbolic_queue = [ - ([ - qml.RX(variable, wires=[0]), - qml.RX(variable, wires=[1]) - ], - [], - 'RX!V1![0]' + - 'RX!V1![1]' + - '|||' - ), - - ] - - @pytest.mark.parametrize("queue, observable_queue, expected_string", many_symbolic_queue) - def test_serialize_symbolic_argument_multiple_times(self, queue, observable_queue, expected_string): - """Tests that the same hash is created for two circuitgraphs that have the same symbolic argument - used multiple times.""" - circuit_graph_1 = CircuitGraph(queue + observable_queue, {}, Wires([0, 1])) - circuit_graph_2 = CircuitGraph(queue + observable_queue, {}, Wires([0, 1])) - - assert circuit_graph_1.serialize() == circuit_graph_2.serialize() - assert expected_string == circuit_graph_1.serialize() - - variable1 = Variable(1) - variable2 = Variable(2) - - multiple_symbolic_queue = [ - ([ - qml.RX(variable1, wires=[0]), - qml.RX(variable2, wires=[1]) - ], - [], - 'RX!V1![0]' + - 'RX!V2![1]' + - '|||' - ), - ] - - @pytest.mark.parametrize("queue, observable_queue, expected_string", multiple_symbolic_queue) - def test_serialize_multiple_symbolic_arguments(self, queue, observable_queue, expected_string): - """Tests that the same hash is created for two circuitgraphs that have multiple symbolic arguments.""" - - circuit_graph_1 = CircuitGraph(queue + observable_queue, {}, Wires([0, 1])) - circuit_graph_2 = CircuitGraph(queue + observable_queue, {}, Wires([0, 1])) - - assert circuit_graph_1.serialize() == circuit_graph_2.serialize() - assert expected_string == circuit_graph_1.serialize() - - observable1 = qml.PauliZ(0) observable1.return_type = not None @@ -209,7 +106,7 @@ def circuit1(): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([], {}) circuit_hash_1 = node1.circuit.hash @@ -219,7 +116,7 @@ def circuit2(): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([], {}) circuit_hash_2 = node2.circuit.hash @@ -239,7 +136,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -249,7 +146,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -270,7 +167,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -281,7 +178,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -305,7 +202,7 @@ def circuit1(a, b): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([a, b], {}) circuit_hash_1 = node1.circuit.hash @@ -315,7 +212,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -337,7 +234,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -348,7 +245,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -368,7 +265,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -377,7 +274,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = dev.circuit_hash @@ -396,7 +293,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -405,7 +302,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.var(qml.PauliZ(0) @ qml.PauliX(1)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -414,7 +311,7 @@ def circuit3(x, y): qml.CNOT(wires=[0, 1]) return qml.sample(qml.PauliZ(0) @ qml.PauliX(1)) - node3 = BaseQNode(circuit1, dev) + node3 = qml.QNode(circuit1, dev) node3.evaluate([x, y], {}) circuit_hash_3 = node3.circuit.hash @@ -435,7 +332,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.Hermitian(matrix, wires=[0]) @ qml.PauliX(1)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -444,7 +341,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.Hermitian(matrix, wires=[0]) @ qml.PauliX(1)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -466,7 +363,7 @@ def circuit1(): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([], {}) circuit_hash_1 = node1.circuit.hash @@ -478,7 +375,7 @@ def circuit2(): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([], {}) circuit_hash_2 = node2.circuit.hash @@ -494,7 +391,7 @@ def circuit1(): qml.RX(a, wires=[0]) return qml.expval(qml.PauliZ(0)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([], {}) circuit_hash_1 = node1.circuit.hash @@ -502,7 +399,7 @@ def circuit2(): qml.RY(a, wires=[0]) return qml.expval(qml.PauliZ(0)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([], {}) circuit_hash_2 = node2.circuit.hash @@ -524,7 +421,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -535,7 +432,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -556,7 +453,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) # <------------- qml.PauliZ(0) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -567,7 +464,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) # <------------- qml.PauliZ(0) @ qml.PauliX(1) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -587,7 +484,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -596,7 +493,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -616,7 +513,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -625,7 +522,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -645,7 +542,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) #<------ wires = [0, 1] return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -654,7 +551,7 @@ def circuit2(x, y): qml.CNOT(wires=[1, 0]) #<------ wires = [1, 0] return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -674,7 +571,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) # <----- (0) @ (1) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -683,7 +580,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(2)) # <----- (0) @ (2) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -705,7 +602,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -716,7 +613,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash @@ -740,7 +637,7 @@ def circuit1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.Hermitian(matrix_1, wires=[0]) @ qml.PauliX(1)) - node1 = BaseQNode(circuit1, dev) + node1 = qml.QNode(circuit1, dev) node1.evaluate([x, y], {}) circuit_hash_1 = node1.circuit.hash @@ -749,7 +646,7 @@ def circuit2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.Hermitian(matrix_2, wires=[0]) @ qml.PauliX(1)) - node2 = BaseQNode(circuit2, dev) + node2 = qml.QNode(circuit2, dev) node2.evaluate([x, y], {}) circuit_hash_2 = node2.circuit.hash diff --git a/tests/circuit_graph/test_tape_circuit_graph.py b/tests/circuit_graph/test_tape_circuit_graph.py deleted file mode 100644 index d27ae192c94..00000000000 --- a/tests/circuit_graph/test_tape_circuit_graph.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane.circuit_graph` module. -""" -# pylint: disable=no-self-use,too-many-arguments,protected-access - -import pytest -import numpy as np - -import pennylane as qml -from pennylane.tape.circuit_graph import TapeCircuitGraph as CircuitGraph -from pennylane.wires import Wires - - -pytestmark = pytest.mark.usefixtures("in_tape_mode") - - -@pytest.fixture -def ops(): - """A fixture of a complex example of operations that depend on previous operations.""" - return [ - qml.RX(0.43, wires=0), - qml.RY(0.35, wires=1), - qml.RZ(0.35, wires=2), - qml.CNOT(wires=[0, 1]), - qml.Hadamard(wires=2), - qml.CNOT(wires=[2, 0]), - qml.PauliX(wires=1), - ] - - -@pytest.fixture -def obs(): - """A fixture of observables to go after the ops fixture.""" - return [ - qml.expval(qml.PauliX(wires=0)), - qml.expval(qml.Hermitian(np.identity(4), wires=[1, 2])), - ] - - -@pytest.fixture -def circuit(ops, obs): - """A fixture of a circuit generated based on the queue and obs fixtures above.""" - circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) - return circuit - - -@pytest.fixture -def parameterized_circuit(wires): - def qfunc(a, b, c, d, e, f): - qml.Rotation(a, wires=wires[0]), - qml.Rotation(b, wires=wires[1]), - qml.Rotation(c, wires=wires[2]), - qml.Beamsplitter(d, 1, wires=[wires[0], wires[1]]) - qml.Rotation(1, wires=wires[0]), - qml.Rotation(e, wires=wires[1]), - qml.Rotation(f, wires=wires[2]), - - return [ - qml.expval(qml.ops.NumberOperator(wires=wires[0])), - qml.expval(qml.ops.NumberOperator(wires=wires[1])), - qml.expval(qml.ops.NumberOperator(wires=wires[2])), - ] - - return qfunc - - -class TestCircuitGraph: - """Test conversion of queues to DAGs""" - - def test_no_dependence(self): - """Test case where operations do not depend on each other. - This should result in a graph with no edges.""" - - ops = [qml.RX(0.43, wires=0), qml.RY(0.35, wires=1)] - - res = CircuitGraph(ops, [], Wires([0, 1])).graph - assert len(res) == 2 - assert not res.edges() - - def test_dependence(self, ops, obs): - """Test a more complex example containing operations - that do depend on the result of previous operations""" - - circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) - graph = circuit.graph - assert len(graph) == 9 - assert len(graph.edges()) == 9 - - all_ops = ops + obs - - # all ops should be nodes in the graph - for k in all_ops: - assert k in graph.nodes - - # all nodes in the graph should be ops - for k in graph.nodes: - assert k is all_ops[k.queue_idx] - - # Finally, checking the adjacency of the returned DAG: - assert set(graph.edges()) == set( - (all_ops[a], all_ops[b]) - for a, b in [(0, 3), (1, 3), (2, 4), (3, 5), (3, 6), (4, 5), (5, 7), (5, 8), (6, 8),] - ) - - def test_ancestors_and_descendants_example(self, ops, obs): - """ - Test that the ``ancestors`` and ``descendants`` methods return the expected result. - """ - circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) - all_ops = ops + obs - - ancestors = circuit.ancestors([all_ops[6]]) - assert len(ancestors) == 3 - for o_idx in (0, 1, 3): - assert all_ops[o_idx] in ancestors - - descendants = circuit.descendants([all_ops[6]]) - assert descendants == set([all_ops[8]]) - - def test_update_node(self, ops, obs): - """Changing nodes in the graph.""" - - circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) - new = qml.RX(0.1, wires=0) - circuit.update_node(ops[0], new) - assert circuit.operations[0] is new - - def test_observables(self, circuit, obs): - """Test that the `observables` property returns the list of observables in the circuit.""" - assert circuit.observables == obs - - def test_operations(self, circuit, ops): - """Test that the `operations` property returns the list of operations in the circuit.""" - assert circuit.operations == ops - - def test_op_indices(self, circuit): - """Test that for the given circuit, this method will fetch the correct operation indices for - a given wire""" - op_indices_for_wire_0 = [0, 3, 5, 7] - op_indices_for_wire_1 = [1, 3, 6, 8] - op_indices_for_wire_2 = [2, 4, 5, 8] - - assert circuit.wire_indices(0) == op_indices_for_wire_0 - assert circuit.wire_indices(1) == op_indices_for_wire_1 - assert circuit.wire_indices(2) == op_indices_for_wire_2 - - @pytest.mark.parametrize("wires", [['a', 'q1', 3]]) - def test_layers(self, parameterized_circuit, wires): - """A test of a simple circuit with 3 layers and 6 parameters""" - - dev = qml.device("default.gaussian", wires=wires) - qnode = qml.QNode(parameterized_circuit, dev) - qnode(0.1, 0.2, 0.3, 0.4, 0.5, 0.6) - circuit = qnode.qtape.graph - layers = circuit.parametrized_layers - ops = circuit.operations - - assert len(layers) == 3 - assert layers[0].ops == [ops[x] for x in [0, 1, 2]] - assert layers[0].param_inds == [0, 1, 2] - assert layers[1].ops == [ops[3]] - assert layers[1].param_inds == [3] - assert layers[2].ops == [ops[x] for x in [5, 6]] - assert layers[2].param_inds == [6, 7] - - @pytest.mark.parametrize("wires", [['a', 'q1', 3]]) - def test_iterate_layers(self, parameterized_circuit, wires): - """A test of the different layers, their successors and ancestors using a simple circuit""" - - dev = qml.device("default.gaussian", wires=wires) - qnode = qml.QNode(parameterized_circuit, dev) - qnode(0.1, 0.2, 0.3, 0.4, 0.5, 0.6) - circuit = qnode.qtape.graph - result = list(circuit.iterate_parametrized_layers()) - - assert len(result) == 3 - assert set(result[0][0]) == set([]) - assert set(result[0][1]) == set(circuit.operations[:3]) - assert result[0][2] == (0, 1, 2) - assert set(result[0][3]) == set(circuit.operations[3:] + circuit.observables) - - assert set(result[1][0]) == set(circuit.operations[:2]) - assert set(result[1][1]) == set([circuit.operations[3]]) - assert result[1][2] == (3,) - assert set(result[1][3]) == set(circuit.operations[4:6] + circuit.observables[:2]) - - assert set(result[2][0]) == set(circuit.operations[:4]) - assert set(result[2][1]) == set(circuit.operations[5:]) - assert result[2][2] == (6, 7) - assert set(result[2][3]) == set(circuit.observables[1:]) diff --git a/tests/collections/test_collections.py b/tests/collections/test_collections.py index 448475f8795..c667f792ab2 100644 --- a/tests/collections/test_collections.py +++ b/tests/collections/test_collections.py @@ -38,9 +38,6 @@ Variable = None -pytestmark = pytest.mark.usefixtures("tape_mode") - - class TestMap: """Test for mapping ansatz over observables or devices, to return a QNode collection""" diff --git a/tests/collections/test_qnode_collection.py b/tests/collections/test_qnode_collection.py index 242d2a40547..330d5321888 100644 --- a/tests/collections/test_qnode_collection.py +++ b/tests/collections/test_qnode_collection.py @@ -39,9 +39,6 @@ Variable = None -pytestmark = pytest.mark.usefixtures("tape_mode") - - class TestConstruction: """Tests for the QNodeCollection construction""" diff --git a/tests/conftest.py b/tests/conftest.py index b36df94e564..f381f81d8fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -164,54 +164,3 @@ def mock_device(monkeypatch): def tear_down_hermitian(): yield None qml.Hermitian._eigs = {} - - -@pytest.fixture -def non_tape_mode_only(): - """Run the test in tape mode""" - qml.disable_tape() - yield - qml.enable_tape() - - -@pytest.fixture -def in_tape_mode(): - return - - -@pytest.fixture(params=[False, True]) -def tape_mode(request, mocker): - """Tests using this fixture will be run twice, once in tape mode and once without.""" - - if request.param: - # Several attributes and methods on the old QNode have a new location on the new QNode/tape. - # Here, we dynamically mock so that the tests do not have to be modified to support both - # tape and non-tape mode. Once tape mode is default, we can make the equivalent - # changes directly in the tests. - mocker.patch("pennylane.tape.QNode.ops", property(lambda self: self.qtape.operations + self.qtape.observables), create=True) - mocker.patch("pennylane.tape.QNode.h", property(lambda self: self.diff_options["h"]), create=True) - mocker.patch("pennylane.tape.QNode.order", property(lambda self: self.diff_options["order"]), create=True) - mocker.patch("pennylane.tape.QNode.circuit", property(lambda self: self.qtape.graph), create=True) - - def patched_jacobian(self, args, **kwargs): - method = kwargs.get("method", "best") - - if method == "A": - method = "analytic" - elif method == "F": - method = "numeric" - - kwargs["method"] = method - dev = kwargs["options"]["device"] - - return self.qtape.jacobian(dev, **kwargs) - - - mocker.patch("pennylane.tape.QNode.jacobian", patched_jacobian, create=True) - - else: - qml.disable_tape() - - yield - - qml.enable_tape() diff --git a/tests/devices/test_caching.py b/tests/devices/test_caching.py index 93c839d99cd..3f508f28826 100644 --- a/tests/devices/test_caching.py +++ b/tests/devices/test_caching.py @@ -16,8 +16,8 @@ import pytest import pennylane as qml -from pennylane.tape.measure import expval -from pennylane.tape import qnode, QNode +from pennylane.measure import expval +from pennylane import qnode, QNode from pennylane.devices import DefaultQubit @@ -260,25 +260,3 @@ def qfunc(x, y): assert calls2 == 5 assert calls3 == 10 assert g is not None - - def test_non_tape_mode(self): - """Tests that an exception is raised when attempting to use caching outside of tape mode""" - dev = qml.device("default.qubit", wires=3, cache=10) - - try: - qml.disable_tape() - - def qfunc(x, y): - """Simple quantum function""" - qml.RX(x, wires=0) - qml.RX(y, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(wires=1)) - - qn = qml.QNode(qfunc, dev) - - with pytest.raises(ValueError, match="Caching is only available when using tape mode"): - qn(0.1, 0.2) - - finally: - qml.enable_tape() diff --git a/tests/devices/test_default_qubit.py b/tests/devices/test_default_qubit.py index 9031d228ad9..09fa7de042f 100644 --- a/tests/devices/test_default_qubit.py +++ b/tests/devices/test_default_qubit.py @@ -1901,7 +1901,6 @@ def test_partial_subsystem(self, mocker): spy.assert_called() -@pytest.mark.usefixtures("tape_mode") class TestInverseDecomposition: """Integration tests for decompositions of inverse gates""" @@ -1918,7 +1917,7 @@ def test_s(): return qml.probs(wires=0) test_s() - operations = test_s.qtape.operations if qml.tape_mode_active() else test_s.ops + operations = test_s.qtape.operations assert "S" in [i.name for i in operations] expected = np.array([1., 1.j]) / np.sqrt(2) @@ -1931,7 +1930,7 @@ def test_s_inverse(): return qml.probs(wires=0) test_s_inverse() - operations = test_s_inverse.qtape.operations if qml.tape_mode_active() else test_s_inverse.ops + operations = test_s_inverse.qtape.operations assert "S.inv" in [i.name for i in operations] expected = np.array([1., -1.j]) / np.sqrt(2) @@ -1953,7 +1952,7 @@ def test_s(): return qml.probs(wires=0) test_s() - operations = test_s.qtape.operations if qml.tape_mode_active() else test_s.ops + operations = test_s.qtape.operations assert "S" not in [i.name for i in operations] assert "PhaseShift" in [i.name for i in operations] @@ -1967,7 +1966,7 @@ def test_s_inverse(): return qml.probs(wires=0) test_s_inverse() - operations = test_s_inverse.qtape.operations if qml.tape_mode_active() else test_s_inverse.ops + operations = test_s_inverse.qtape.operations assert "S.inv" not in [i.name for i in operations] assert "PhaseShift.inv" in [i.name for i in operations] diff --git a/tests/devices/test_default_qubit_jax.py b/tests/devices/test_default_qubit_jax.py index bbc09b3f9c7..1c76e7b6005 100644 --- a/tests/devices/test_default_qubit_jax.py +++ b/tests/devices/test_default_qubit_jax.py @@ -6,8 +6,6 @@ import pennylane as qml from pennylane.devices.default_qubit_jax import DefaultQubitJax -pytestmark = pytest.mark.usefixtures("tape_mode") - class TestQNodeIntegration: """Integration tests for default.qubit.jax. This test ensures it integrates @@ -66,8 +64,6 @@ def circuit(x): return qml.expval(qml.PauliY(0)) expected = -jnp.sin(p) - if not qml.tape_mode_active(): - assert isinstance(circuit, qml.qnodes.PassthruQNode) assert jnp.isclose(circuit(p), expected, atol=tol, rtol=0) def test_qubit_circuit_with_jit(self, tol): @@ -116,8 +112,6 @@ def circuit(): def test_correct_state_returned(self, tol): """Test that the device state is correct after applying a quantum function on the device""" - if not qml.tape_mode_active(): - pytest.skip("Only supported in tape mode") dev = qml.device("default.qubit.jax", wires=2) @qml.qnode(dev, interface="jax", diff_method="backprop") @@ -204,8 +198,6 @@ def circuit(p): qml.RX(p[2] / 2, wires=0) return qml.expval(qml.PauliZ(0)) - if not qml.tape_mode_active(): - assert isinstance(circuit, qml.qnodes.PassthruQNode) res = circuit(weights) expected = jnp.cos(3 * x) * jnp.cos(y) * jnp.cos(z / 2) - jnp.sin(3 * x) * jnp.sin(z / 2) @@ -341,13 +333,9 @@ def circuit(x, weights, w=None): return qml.expval(qml.PauliX(w)) # Check that the correct QNode type is being used. - if not qml.tape_mode_active(): - if diff_method == "backprop": - assert isinstance(circuit, qml.qnodes.PassthruQNode) - assert not hasattr(circuit, "jacobian") - else: - assert not isinstance(circuit, qml.qnodes.PassthruQNode) - assert hasattr(circuit, "jacobian") + if diff_method == "backprop": + assert isinstance(circuit, qml.qnodes.PassthruQNode) + assert not hasattr(circuit, "jacobian") def cost(params): """Perform some classical processing""" @@ -389,7 +377,7 @@ def circuit(x, w=None): qml.RZ(x, wires=w) return qml.expval(qml.PauliX(w)) - error_type = qml.QuantumFunctionError if qml.tape_mode_active() else ValueError + error_type = qml.QuantumFunctionError with pytest.raises( error_type, match="default.qubit.jax only supports diff_method='backprop' when using the jax interface", @@ -425,8 +413,6 @@ def ansatz(weights, **kwargs): obs_list = [qml.PauliX(0) @ qml.PauliY(1), qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliZ(1)] qnodes = qml.map(ansatz, obs_list, dev, interface="jax") - if not qml.tape_mode_active(): - assert qnodes.interface == "jax" weights = jnp.array([0.1, 0.2]) @@ -438,8 +424,6 @@ def cost(weights): def test_non_backprop_error(self): """Test that an error is raised in tape mode if the diff method is not backprop""" - if not qml.tape_mode_active(): - pytest.skip("Test only applies in tape mode") dev = qml.device("default.qubit.jax", wires=2) diff --git a/tests/interfaces/test_autograd.py b/tests/interfaces/test_autograd.py deleted file mode 100644 index 25111b31390..00000000000 --- a/tests/interfaces/test_autograd.py +++ /dev/null @@ -1,1064 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane` :class:`to_autograd` class. -""" - -import autograd -import autograd.numpy as anp # only to be used inside classical computational nodes -import pytest -import numpy as np - -import pennylane as qml -from pennylane.qnodes.base import QuantumFunctionError -from pennylane.qnodes.qubit import QubitQNode -from pennylane.qnodes.cv import CVQNode - -from pennylane.interfaces.autograd import to_autograd - - -alpha = 0.5 # displacement in tests -hbar = 2 -mag_alphas = np.linspace(0, 1.5, 5) -thetas = np.linspace(-2*np.pi, 2*np.pi, 8) -sqz_vals = np.linspace(0., 1., 5) - - -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - - -class TestAutogradDetails: - """Test configuration details of the autograd interface""" - - def test_interface_str(self, qubit_device_2_wires): - """Test that the interface string is correctly identified - as numpy""" - def circuit(x, y, z): - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1)) - - circuit = to_autograd(QubitQNode(circuit, qubit_device_2_wires)) - assert circuit.interface == "autograd" - - -class TestAutogradJacobianCV: - """Tests involving Autograd functions grad and jacobian for CV circuits.""" - - @pytest.mark.parametrize('theta', thetas) - def test_rotation_gradient(self, theta, tol): - """Tests that the automatic gradient of a phase space rotation is correct.""" - - def circuit(y): - qml.Displacement(alpha, 0., wires=[0]) - qml.Rotation(y, wires=[0]) - return qml.expval(qml.X(0)) - - dev = qml.device('default.gaussian', wires=1) - circuit = to_autograd(QubitQNode(circuit, dev)) - grad_fn = autograd.grad(circuit) - - autograd_val = grad_fn(theta) - # qfunc evalutes to hbar * alpha * cos(theta) - manualgrad_val = - hbar * alpha * np.sin(theta) - assert autograd_val == pytest.approx(manualgrad_val, abs=tol) - - @pytest.mark.parametrize('theta', thetas) - def test_beamsplitter_gradient(self, theta, tol): - """Tests that the automatic gradient of a beamsplitter is correct.""" - - def circuit(y): - qml.Displacement(alpha, 0., wires=[0]) - qml.Beamsplitter(y, 0, wires=[0, 1]) - return qml.expval(qml.X(0)) - - dev = qml.device('default.gaussian', wires=2) - circuit = to_autograd(CVQNode(circuit, dev)) - grad_fn = autograd.grad(circuit) - - autograd_val = grad_fn(theta) - # qfunc evalutes to hbar * alpha * cos(theta) - manualgrad_val = - hbar * alpha * np.sin(theta) - assert autograd_val == pytest.approx(manualgrad_val, abs=tol) - - @pytest.mark.parametrize('mag', mag_alphas) - @pytest.mark.parametrize('theta', thetas) - def test_displacement_gradient(self, mag, theta, tol): - """Tests that the automatic gradient of a phase space displacement is correct.""" - - def circuit(r, phi): - qml.Displacement(r, phi, wires=[0]) - return qml.expval(qml.X(0)) - - dev = qml.device('default.gaussian', wires=1) - circuit = to_autograd(CVQNode(circuit, dev)) - grad_fn = autograd.grad(circuit) - - #alpha = mag * np.exp(1j * theta) - autograd_val = grad_fn(mag, theta) - # qfunc evalutes to hbar * Re(alpha) - manualgrad_val = hbar * np.cos(theta) - assert autograd_val == pytest.approx(manualgrad_val, abs=tol) - - @pytest.mark.parametrize('r', sqz_vals) - def test_squeeze_gradient(self, r, tol): - """Tests that the automatic gradient of a phase space squeezing is correct.""" - - def circuit(y): - qml.Displacement(alpha, 0., wires=[0]) - qml.Squeezing(y, 0., wires=[0]) - return qml.expval(qml.X(0)) - - dev = qml.device('default.gaussian', wires=1) - circuit = to_autograd(CVQNode(circuit, dev)) - grad_fn = autograd.grad(circuit) - - autograd_val = grad_fn(r) - # qfunc evaluates to -exp(-r) * hbar * Re(alpha) - manualgrad_val = -np.exp(-r) * hbar * alpha - assert autograd_val == pytest.approx(manualgrad_val, abs=tol) - - @pytest.mark.parametrize('r', sqz_vals[1:]) # formula is not valid for r=0 - def test_number_state_gradient(self, r, tol): - """Tests that the automatic gradient of a squeezed state with number state expectation is correct.""" - - def circuit(y): - qml.Squeezing(y, 0., wires=[0]) - return qml.expval(qml.FockStateProjector(np.array([2, 0]), wires=[0, 1])) - - dev = qml.device('default.gaussian', wires=2) - circuit = to_autograd(CVQNode(circuit, dev)) - grad_fn = autograd.grad(circuit) - - # (d/dr) |<2|S(r)>|^2 = 0.5 tanh(r)^3 (2 csch(r)^2 - 1) sech(r) - autograd_val = grad_fn(r) - manualgrad_val = 0.5*np.tanh(r)**3 * (2/(np.sinh(r)**2)-1) / np.cosh(r) - assert autograd_val == pytest.approx(manualgrad_val, abs=tol) - - - -class TestAutogradJacobianQubit: - """Tests involving Autograd functions grad and jacobian for qubit circuits.""" - - @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, qubit_device_2_wires): - """Tests that qnodes using positional arguments return - correct gradients for multiple expectation values.""" - par = [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)) - - circuit = to_autograd(QubitQNode(circuit, qubit_device_2_wires)) - - # compare our manual Jacobian computation to theoretical result - expected_jac = self.expected_jacobian(*par) - res = circuit.jacobian(par) - assert expected_jac == pytest.approx(res, abs=tol) - - # compare our manual Jacobian computation to autograd - # not sure if this is the intended usage of jacobian - jac0 = autograd.jacobian(circuit, 0) - jac1 = autograd.jacobian(circuit, 1) - jac2 = autograd.jacobian(circuit, 2) - res = np.stack([jac0(*par), jac1(*par), jac2(*par)]).T - - assert expected_jac == pytest.approx(res, abs=tol) - - #compare with what we get if argnum is a list - jac = autograd.jacobian(circuit, argnum=[0, 1, 2]) - #res2 = jac(*par) # FIXME this call gives a TypeError inside Autograd - #assert res == pytest.approx(res2, abs=tol) - - def test_multiple_expectation_jacobian_array(self, tol, qubit_device_2_wires): - """Tests that qnodes using an array argument return correct gradients - for multiple expectation values.""" - par = np.array([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)) - - circuit = to_autograd(QubitQNode(circuit, qubit_device_2_wires)) - - expected_jac = self.expected_jacobian(*par) - res = circuit.jacobian([par]) - assert expected_jac == pytest.approx(res, abs=tol) - - jac = autograd.jacobian(circuit, 0) - res = jac(par) - assert expected_jac == pytest.approx(res, abs=tol) - - - def test_array_parameters_autograd(self, tol, qubit_device_2_wires): - """Test that gradients of array parameters give - same results as positional arguments.""" - - par = [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) - - circuit1 = to_autograd(QubitQNode(circuit1, qubit_device_2_wires)) - grad1 = autograd.grad(circuit1, argnum=[0, 1, 2]) - - # three positional parameters - jac = circuit1.jacobian(par) - ag = grad1(*par) - ag = np.array([ag]) - assert jac == pytest.approx(ag, abs=tol) - - circuit2 = to_autograd(QubitQNode(circuit2, qubit_device_2_wires)) - grad2 = autograd.grad(circuit2, argnum=[0, 1]) - - # one scalar, one array - temp = [par[0], np.array(par[1:])] - jac = circuit2.jacobian(temp) - ag = grad2(*temp) - ag = np.r_[ag][np.newaxis, :] - assert jac == pytest.approx(ag, abs=tol) - - circuit3 = to_autograd(QubitQNode(circuit3, qubit_device_2_wires)) - grad3 = autograd.grad(circuit3, argnum=0) - - # one array - temp = [np.array(par)] - jac = circuit3.jacobian(temp) - ag = grad3(*temp)[np.newaxis, :] - assert jac == pytest.approx(ag, abs=tol) - - - 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 = to_autograd(QubitQNode(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 = autograd.grad(cost, argnum=[0, 1, 2]) - computed_grad = cost_grad(*args) - - assert cost(*args) == pytest.approx(cost_target, abs=tol) - - assert computed_grad[0] == pytest.approx(grad_target[0], abs=tol) - assert computed_grad[1] == pytest.approx(grad_target[1], abs=tol) - assert computed_grad[2] == pytest.approx(grad_target[2], abs=tol) - - 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 = to_autograd(QubitQNode(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 = autograd.grad(cost, argnum=[0, 1, 2]) - computed_grad = cost_grad(*args) - - assert cost(*args) == pytest.approx(cost_target, abs=tol) - - assert computed_grad[0] == pytest.approx(grad_target[0], abs=tol) - assert computed_grad[1] == pytest.approx(grad_target[1], abs=tol) - assert computed_grad[2] == pytest.approx(grad_target[2], abs=tol) - - 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 = to_autograd(QubitQNode(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 = autograd.grad(cost, argnum=[0, 1, 2]) - computed_grad = cost_grad(*args) - - assert cost(*args) == pytest.approx(cost_target, abs=tol) - - assert computed_grad[0] == pytest.approx(grad_target[0], abs=tol) - assert computed_grad[1] == pytest.approx(grad_target[1], abs=tol) - assert computed_grad[2] == pytest.approx(grad_target[2], abs=tol) - - def test_qfunc_gradients(self, qubit_device_2_wires, tol): - "Tests that the various ways of computing the gradient of a qfunc all agree." - - def circuit(x, y, z): - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.RY(-1.6, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[1, 0]) - qml.RX(z, wires=[0]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - qnode = to_autograd(QubitQNode(circuit, qubit_device_2_wires)) - params = np.array([0.1, -1.6, np.pi / 5]) - - # manual gradients - 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, argnum=[0, 1, 2]) - grad_auto = np.array([grad_fn(*params)]) - - # gradients computed with different methods must agree - assert grad_fd1 == pytest.approx(grad_fd2, abs=tol) - assert grad_fd1 == pytest.approx(grad_angle, abs=tol) - assert grad_fd1 == pytest.approx(grad_auto, abs=tol) - - def test_hybrid_gradients(self, qubit_device_2_wires, tol): - "Tests that the various ways of computing the gradient of a hybrid computation all agree." - - # input data is the first parameter - def classifier_circuit(in_data, x): - qml.RX(in_data, wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.RY(-1.6, wires=[0]) - qml.RY(in_data, wires=[1]) - qml.CNOT(wires=[1, 0]) - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - classifier = to_autograd(QubitQNode(classifier_circuit, qubit_device_2_wires)) - - param = -0.1259 - in_data = np.array([-0.1, -0.88, np.exp(0.5)]) - out_data = np.array([1.5, np.pi / 3, 0.0]) - - def error(p): - "Total square error of classifier predictions." - ret = 0 - for d_in, d_out in zip(in_data, out_data): - square_diff = (classifier(d_in, p) - d_out) ** 2 - ret = ret + square_diff - return ret - - def d_error(p, grad_method): - "Gradient of error, computed manually." - ret = 0 - 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, wrt=[1], method=grad_method) - return ret - - y0 = error(param) - grad = autograd.grad(error) - grad_auto = grad(param) - - grad_fd1 = d_error(param, 'F') - grad_angle = d_error(param, 'A') - - # gradients computed with different methods must agree - assert grad_fd1 == pytest.approx(grad_angle, abs=tol) - assert grad_fd1 == pytest.approx(grad_auto, abs=tol) - assert grad_angle == pytest.approx(grad_auto, abs=tol) - - - def test_hybrid_gradients_autograd_numpy(self, qubit_device_2_wires, tol): - "Test the gradient of a hybrid computation requiring autograd.numpy functions." - - def circuit(x, y): - "Quantum node." - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.RY(y, wires=[0]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - quantum = to_autograd(QubitQNode(circuit, qubit_device_2_wires)) - - def classical(p): - "Classical node, requires autograd.numpy functions." - return anp.exp(anp.sum(quantum(p[0], anp.log(p[1])))) - - def d_classical(a, b, method): - "Gradient of classical computed symbolically, can use normal numpy functions." - val = classical((a, b)) - J = quantum.jacobian((a, np.log(b)), method=method) - return val * np.array([J[0, 0] + J[1, 0], (J[0, 1] + J[1, 1]) / b]) - - param = np.array([-0.1259, 1.53]) - y0 = classical(param) - grad_classical = autograd.jacobian(classical) - grad_auto = grad_classical(param) - - grad_fd1 = d_classical(*param, 'F') - grad_angle = d_classical(*param, 'A') - - # gradients computed with different methods must agree - assert grad_fd1 == pytest.approx(grad_angle, abs=tol) - assert grad_fd1 == pytest.approx(grad_auto, abs=tol) - assert grad_angle == pytest.approx(grad_auto, abs=tol) - - -class TestParameterHandlingIntegration: - """Test that the parameter handling for differentiable/non-differentiable - parameters works correctly.""" - - def test_no_differentiable_parameter(self): - """Test that the QNode can still be evaluated even when no parameters - are differentiable""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd") - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - return qml.expval(qml.PauliZ(0)) - - x = qml.numpy.array(1., requires_grad=False) - y = qml.numpy.array(2., requires_grad=False) - z = qml.numpy.array(3., requires_grad=False) - - circuit(x, y, z) - assert circuit.get_trainable_args() == set() - - grad_fn = qml.grad(circuit) - - with pytest.warns(UserWarning, match="Output seems independent of input"): - res = grad_fn(x, y, z) - - assert not res - assert circuit.get_trainable_args() == set() - - def test_differentiable_parameter_first(self): - """Test that a differentiable parameter used as the first - argument is correctly evaluated by QNode.jacobian, and that - all other non-differentiable parameters are ignored""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd") - def circuit(weights, data1, data2): - # non-differentiable quantum function - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - # differentiable quantum function - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - # non-differentiable quantum function - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # differentiating the circuit wrt the weights - grad_fn = qml.grad(circuit) - - # input weights - weights = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - - # input data - data1 = qml.numpy.array([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - data2 = qml.numpy.array([1, 1], requires_grad=False) - - res = grad_fn(weights, data1, data2) - - # we do not check for correctness, just that the output - # is the correct shape - assert len(res) == 1 - assert res[0].shape == weights.shape - - # check that the first arg was marked as non-differentiable - assert circuit.get_trainable_args() == {0} - - # Check that the gradient was not computed for the - # non-differentiable elements of `data1` and `data2`. - # First, extract the variable indices that the jacobian method - # 'skipped' (those with grad_method="0"): - non_diff_var_indices = sorted([k for k, v in circuit.par_to_grad_method.items() if v == "0"]) - - # Check that these indices corresponds to the elements of data1 and data2 - # within the flattenened list [weights, data1, data2] - assert non_diff_var_indices == [18, 19, 20, 21, 22, 23] - - def test_differentiable_parameter_middle(self): - """Test that a differentiable parameter provided as the middle - argument is correctly evaluated by QNode.jacobian, and that - all other non-differentiable parameters are ignored""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd") - def circuit(data1, weights, data2): - # non-differentiable quantum function - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - # differentiable quantum function - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - # non-differentiable quantum function - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # differentiating the circuit wrt the weights - grad_fn = qml.grad(circuit) - - # input weights - weights = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - - # input data - data1 = qml.numpy.array([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - data2 = qml.numpy.array([1, 1], requires_grad=False) - - res = grad_fn(data1, weights, data2) - - # we do not check for correctness, just that the output - # is the correct shape - assert len(res) == 1 - assert res[0].shape == weights.shape - - # check that the second arg was marked as non-differentiable - assert circuit.get_trainable_args() == {1} - - # Check that the gradient was not computed for the - # non-differentiable elements of `data1` and `data2`. - # First, extract the variable indices that the jacobian method - # 'skipped' (those with grad_method="0"): - non_diff_var_indices = sorted([k for k, v in circuit.par_to_grad_method.items() if v == "0"]) - - # Check that these indices corresponds to the elements of data1 and data2 - # within the flattenened list [data1, weights, data2] - assert non_diff_var_indices == [0, 1, 2, 3, 22, 23] - - def test_differentiable_parameter_last(self): - """Test that a differentiable parameter used as the last - argument is correctly evaluated by QNode.jacobian, and that - all other non-differentiable parameters are ignored""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd") - def circuit(data1, data2, weights): - # non-differentiable quantum function - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - # differentiable quantum function - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - # non-differentiable quantum function - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # differentiating the circuit wrt the weights - grad_fn = qml.grad(circuit) - - # input weights - weights = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - - # input data - data1 = qml.numpy.array([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - data2 = qml.numpy.array([1, 1], requires_grad=False) - - res = grad_fn(data1, data2, weights) - - # we do not check for correctness, just that the output - # is the correct shape - assert len(res) == 1 - assert res[0].shape == weights.shape - - # check that the last arg was marked as non-differentiable - assert circuit.get_trainable_args() == {2} - - # Check that the parameter shift was not performed for the - # non-differentiable elements of `data1` and `data2`. - # First, extract the variable indices that the jacobian method - # 'skipped' (those with grad_method="0"): - non_diff_var_indices = sorted([k for k, v in circuit.par_to_grad_method.items() if v == "0"]) - - # Check that these indices corresponds to the elements of data1 and data2 - # within the flattenened list [data1, data2, weights] - assert non_diff_var_indices == [0, 1, 2, 3, 4, 5] - - def test_multiple_differentiable_and_non_differentiable_parameters(self): - """Test that multiple differentiable and non-differentiable parameters - works as expected""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd") - def circuit(data1, weights1, data2, weights2): - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - qml.templates.StronglyEntanglingLayers(weights1, wires=[0, 1]) - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - qml.templates.StronglyEntanglingLayers(weights2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # differentiating the circuit wrt the weights - grad_fn = qml.grad(circuit) - - # input weights - weights1 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - weights2 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=4) - - # input data - data1 = qml.numpy.array([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - data2 = qml.numpy.array([1, 1], requires_grad=False) - - res = grad_fn(data1, weights1, data2, weights2) - - # we do not check for correctness, just that the output - # is the correct shape - assert len(res) == 2 - assert res[0].shape == weights1.shape - assert res[1].shape == weights2.shape - - # check that the gradient was only computed for the - # differentiable elements of `weights`, not the data input - assert circuit.get_trainable_args() == {1, 3} - - def test_gradient_non_differentiable_exception(self): - """Test that an exception is raised if non-differentiable data is - differentiated""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd") - def circuit(data1): - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - grad_fn = qml.grad(circuit, argnum=[0]) - data1 = qml.numpy.array([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - - with pytest.raises(qml.numpy.NonDifferentiableError, match="is non-differentiable"): - grad_fn(data1) - - def test_no_differentiable_parameters(self): - """If there are no differentiable parameters, the output of the gradient - function is an empty tuple, and a warning is emitted.""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd") - def circuit(data1): - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - grad_fn = qml.grad(circuit) - data1 = qml.numpy.array([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - - with pytest.warns(UserWarning, match="Output seems independent of input"): - res = grad_fn(data1) - - assert res == tuple() - - def test_chained_qnodes(self): - """Test that the gradient of chained QNodes works without error""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd") - def circuit1(weights): - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - @qml.qnode(dev, interface="autograd") - def circuit2(data, weights): - qml.templates.AngleEmbedding(data, wires=[0, 1]) - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - return qml.expval(qml.PauliX(0)) - - def cost(weights): - w1, w2 = weights - c1 = circuit1(w1) - c2 = circuit2(c1, w2) - return qml.numpy.sum(c2) ** 2 - - w1 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - w2 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=4) - - weights = [w1, w2] - - grad_fn = qml.grad(cost) - res = grad_fn(weights) - - assert len(res[0]) == 2 - - def test_gradient_value(self, tol): - """Test that the returned gradient value for a qubit QNode is correct, - when one of the arguments is non-differentiable.""" - dev = qml.device("default.qubit", wires=3) - - @qml.qnode(dev) - def circuit(a, b, c): - qml.RX(a, wires=0) - qml.RX(b, wires=1) - qml.RX(c, wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - return qml.expval(qml.PauliX(0) @ qml.PauliY(2)) - - dcircuit = qml.grad(circuit) - - theta = 0.5 - phi = 0.1 - - # explicitly mark varphi as non-differentiable - varphi = qml.numpy.array(0.23, requires_grad=False) - - res = dcircuit(theta, phi, varphi) - expected = np.array([ - np.cos(theta) * np.sin(phi) * np.sin(varphi), - np.sin(theta) * np.cos(phi) * np.sin(varphi) - ]) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - # check that the gradient was not applied to varphi - assert circuit.get_trainable_args() == {0, 1} - - def test_chained_gradient_value(self, mocker, tol): - """Test that the returned gradient value for two chained qubit QNodes - is correct.""" - dev1 = qml.device("default.qubit", wires=3) - - @qml.qnode(dev1) - def circuit1(a, b, c): - qml.RX(a, wires=0) - qml.RX(b, wires=1) - qml.RX(c, wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(2)) - - dev2 = qml.device("default.qubit", wires=2) - - @qml.qnode(dev2) - def circuit2(data, weights): - qml.RX(data[0], wires=0) - qml.RX(data[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.RZ(weights[0], wires=0) - qml.RZ(weights[1], wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliX(0) @ qml.PauliY(1)) - - def cost(a, b, c, weights): - return circuit2(circuit1(a, b, c), weights) - - grad_fn = qml.grad(cost) - - # Set the first parameter of circuit1 as non-differentiable. - a = qml.numpy.array(0.4, requires_grad=False) - - # the remaining free parameters are all differentiable - b = 0.5 - c = 0.1 - weights = qml.numpy.array([0.2, 0.3]) - - res = grad_fn(a, b, c, weights) - - # Output should have shape [dcost/db, dcost/dc, dcost/dw], - # where b,c are scalars, and w is a vector of length 2. - assert len(res) == 3 - assert res[0].shape == tuple() # scalar - assert res[1].shape == tuple() # scalar - assert res[2].shape == (2,) # vector - - cacbsc = np.cos(a)*np.cos(b)*np.sin(c) - - expected = np.array([ - # analytic expression for dcost/db - -np.cos(a)*np.sin(b)*np.sin(c)*np.cos(cacbsc)*np.sin(weights[0])*np.sin(np.cos(a)), - # analytic expression for dcost/dc - np.cos(a)*np.cos(b)*np.cos(c)*np.cos(cacbsc)*np.sin(weights[0])*np.sin(np.cos(a)), - # analytic expression for dcost/dw[0] - np.sin(cacbsc)*np.cos(weights[0])*np.sin(np.cos(a)), - # analytic expression for dcost/dw[1] - 0 - ]) - - # np.hstack 'flattens' the ragged gradient array allowing it - # to be compared with the expected result - assert np.allclose(np.hstack(res), expected, atol=tol, rtol=0) - - # Check that the gradient was computed - # for all parameters in circuit2 - assert circuit2.get_trainable_args() == {0, 1} - - # check that the parameter-shift rule was not applied - # to the first parameter of circuit1 - assert circuit1.get_trainable_args() == {1, 2} - - def test_non_diff_not_a_variable(self): - """Test that an argument marked as non-differentiable - is not wrapped as a variable.""" - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev, interface="autograd") - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - - assert isinstance(x, qml.variable.Variable) - assert isinstance(y, np.ndarray) - assert isinstance(z, qml.variable.Variable) - - return qml.expval(qml.PauliZ(0)) - - x = 1 - y = qml.numpy.array(2, requires_grad=False) - z = 3 - - res = circuit(x, y, z) - - assert circuit.get_trainable_args() == {0, 2} - - assert circuit.arg_vars[0] != x - assert circuit.arg_vars[1] == y - assert circuit.arg_vars[2] != z - - a = 0.6 - b = 0.2 - test_data = [ - ([0, 1], np.cos(2*a) * np.cos(b), [-2 * np.cos(b) * np.sin(2*a), -np.cos(2*a) * np.sin(b)]), - ([1, 0], -np.cos(b) * np.sin(b), [0, -np.cos(b) ** 2 + np.sin(b) ** 2]), - ] - - @pytest.mark.parametrize("w, expected_res, expected_grad", test_data) - def test_non_diff_wires_argument(self, w, expected_res, expected_grad, tol): - """Test that passing wires as a non-differentiable positional - argument works correctly.""" - dev = qml.device("default.qubit", wires=[qml.numpy.array(0, requires_grad=False), - qml.numpy.array(1, requires_grad=False)]) - - @qml.qnode(dev, interface="autograd") - def circuit(wires, params): - qml.Hadamard(wires=wires[0]) - qml.CNOT(wires=[wires[0], wires[1]]) - qml.RX(params[0], wires=wires[0]) - qml.RY(params[1], wires=wires[1]) - qml.CNOT(wires=[wires[1], wires[0]]) - qml.RX(params[0], wires=wires[0]) - qml.RY(params[1], wires=wires[1]) - return qml.expval(qml.PauliZ(qml.numpy.array(0, requires_grad=False))) - - params = qml.numpy.array([0.6, 0.2]) - wires = qml.numpy.array(w, requires_grad=False) - - res = circuit(wires, params) - - assert circuit.get_trainable_args() == {1} - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - grad_fn = qml.grad(circuit) - res_grad = grad_fn(wires, params) - - assert circuit.get_trainable_args() == {1} - assert np.allclose(res_grad, expected_grad, atol=tol, rtol=0) - - def test_call_changing_trainability(self): - """Test that trainability properly changes between QNode calls""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd") - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - return qml.expval(qml.PauliZ(0)) - - x = qml.numpy.array(1, requires_grad=True) - y = qml.numpy.array(2, requires_grad=False) - z = qml.numpy.array(3, requires_grad=True) - - res = circuit(x, y, z) - - assert circuit.get_trainable_args() == {0, 2} - - x.requires_grad = False - y.requires_grad = True - - res = circuit(x, y, z) - - assert circuit.get_trainable_args() == {1, 2} - - def test_grad_changing_trainability(self): - """Test that trainability properly changes between QNode gradient calls""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd") - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - return qml.expval(qml.PauliZ(0)) - - x = qml.numpy.array(1., requires_grad=True) - y = qml.numpy.array(2., requires_grad=False) - z = qml.numpy.array(3., requires_grad=True) - - grad_fn = qml.grad(circuit) - res = grad_fn(x, y, z) - - assert circuit.get_trainable_args() == {0, 2} - - x.requires_grad = False - y.requires_grad = True - - res = grad_fn(x, y, z) - - assert circuit.get_trainable_args() == {1, 2} - - def test_immutability(self): - """Test that changing parameter differentiability raises an exception - on immutable QNodes.""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="autograd", mutable=False) - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - return qml.expval(qml.PauliZ(0)) - - x = qml.numpy.array(1., requires_grad=True) - y = qml.numpy.array(2., requires_grad=False) - z = qml.numpy.array(3., requires_grad=True) - - grad_fn = qml.grad(circuit) - grad_fn(x, y, z) - assert circuit.get_trainable_args() == {0, 2} - - # change values and compute the gradient again - grad_fn(2*x, -y, z) - assert circuit.get_trainable_args() == {0, 2} - - # attempting to change differentiability raises an error - x.requires_grad = False - y.requires_grad = True - - with pytest.raises(qml.QuantumFunctionError, match="cannot be modified"): - grad_fn(x, y, z) - - -class TestConversion: - """Integration tests to make sure that to_autograd() correctly converts - QNodes with/without pre-existing interfaces""" - - @pytest.fixture - def qnode(self, interface, tf_support, torch_support): - """Returns a simple QNode corresponding to cos(x), - with interface as determined by the interface fixture""" - if interface == "tf" and not tf_support: - pytest.skip("Skipped, no tf support") - - if interface == "torch" and not torch_support: - pytest.skip("Skipped, no torch support") - - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev, interface=interface) - def circuit(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - return circuit - - @pytest.mark.parametrize("interface", ["autograd"]) - def test_autograd_conversion(self, qnode, tol): - """Tests that the to_autograd() function ignores QNodes that already - have the autograd interface.""" - converted_qnode = to_autograd(qnode) - assert converted_qnode is qnode - - x = 0.4 - res = converted_qnode(x) - assert np.allclose(res, np.cos(x), atol=tol, rtol=0) - - grad_fn = qml.grad(converted_qnode) - res = grad_fn(x) - assert np.allclose(res, -np.sin(x), atol=tol, rtol=0) - - @pytest.mark.parametrize("interface", [None, "torch", "tf"]) - def test_tf_conversion(self, interface, qnode, tol): - """Tests that the to_autograd() function correctly converts qnodes with pre-existing - or no interfaces.""" - assert qnode.interface == interface - - converted_qnode = to_autograd(qnode) - - x = 0.4 - res = converted_qnode(x) - assert np.allclose(res, np.cos(x), atol=tol, rtol=0) - - grad_fn = qml.grad(converted_qnode) - res = grad_fn(x) - assert np.allclose(res, -np.sin(x), atol=tol, rtol=0) diff --git a/tests/tape/interfaces/test_qnode_autograd.py b/tests/interfaces/test_qnode_autograd.py similarity index 100% rename from tests/tape/interfaces/test_qnode_autograd.py rename to tests/interfaces/test_qnode_autograd.py diff --git a/tests/tape/interfaces/test_qnode_tf.py b/tests/interfaces/test_qnode_tf.py similarity index 100% rename from tests/tape/interfaces/test_qnode_tf.py rename to tests/interfaces/test_qnode_tf.py diff --git a/tests/tape/interfaces/test_qnode_torch.py b/tests/interfaces/test_qnode_torch.py similarity index 100% rename from tests/tape/interfaces/test_qnode_torch.py rename to tests/interfaces/test_qnode_torch.py diff --git a/tests/tape/interfaces/test_tape_autograd.py b/tests/interfaces/test_tape_autograd.py similarity index 100% rename from tests/tape/interfaces/test_tape_autograd.py rename to tests/interfaces/test_tape_autograd.py diff --git a/tests/tape/interfaces/test_tape_tf.py b/tests/interfaces/test_tape_tf.py similarity index 100% rename from tests/tape/interfaces/test_tape_tf.py rename to tests/interfaces/test_tape_tf.py diff --git a/tests/tape/interfaces/test_tape_torch.py b/tests/interfaces/test_tape_torch.py similarity index 100% rename from tests/tape/interfaces/test_tape_torch.py rename to tests/interfaces/test_tape_torch.py diff --git a/tests/interfaces/test_tf.py b/tests/interfaces/test_tf.py deleted file mode 100644 index 7c365e863a5..00000000000 --- a/tests/interfaces/test_tf.py +++ /dev/null @@ -1,1334 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane.interface.tf` QNode interface. -""" - -import pytest - -import numpy as np - -tf = pytest.importorskip("tensorflow", minversion="1.15") -from tensorflow import Variable - -if tf.__version__[0] == "1": - tf.enable_eager_execution() - -import pennylane as qml - -from pennylane.qnodes import QuantumFunctionError -from pennylane.interfaces.tf import to_tf, unflatten_tf - -from gate_data import CNOT, Rotx, Roty, Rotz, I, Y, Z - -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - -def expZ(state): - return np.abs(state[0]) ** 2 - np.abs(state[1]) ** 2 - - -class TestTFQNodeExceptions(): - """TFQNode basic tests.""" - - def test_qnode_fails_on_wrong_return_type(self, qubit_device_2_wires): - """The qfunc must return only Expectations""" - @qml.qnode(qubit_device_2_wires, interface='tf') - def qf(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)), 0.3 - - with pytest.raises(QuantumFunctionError, match='must return either'): - qf(Variable(0.5)) - - def test_qnode_fails_on_expval_not_returned(self, qubit_device_2_wires): - """All expectation values in the qfunc must be returned""" - - @qml.qnode(qubit_device_2_wires, interface='tf') - def qf(x): - qml.RX(x, wires=[0]) - ex = qml.expval(qml.PauliZ(1)) - return qml.expval(qml.PauliZ(0)) - - with pytest.raises(QuantumFunctionError, match='All measured observables'): - qf(Variable(0.5)) - - def test_qnode_fails_on_wrong_expval_order(self, qubit_device_2_wires): - """Expvals must be returned in the order they were created in""" - - @qml.qnode(qubit_device_2_wires, interface='tf') - def qf(x): - qml.RX(x, wires=[0]) - ex = qml.expval(qml.PauliZ(1)) - return qml.expval(qml.PauliZ(0)), ex - - with pytest.raises(QuantumFunctionError, match='All measured observables'): - qf(Variable(0.5)) - - def test_qnode_fails_on_gates_after_measurements(self, qubit_device_2_wires): - """Gates have to precede measurements""" - - @qml.qnode(qubit_device_2_wires, interface='tf') - def qf(x): - qml.RX(x, wires=[0]) - ev = qml.expval(qml.PauliZ(1)) - qml.RY(0.5, wires=[0]) - return ev - - with pytest.raises(QuantumFunctionError, match='gates must precede'): - qf(Variable(0.5)) - - def test_qnode_fails_on_multiple_measurements_of_same_wire(self, qubit_device_2_wires): - """A wire can only be measured once""" - - @qml.qnode(qubit_device_2_wires, interface='tf') - def qf(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)) - - with pytest.raises(QuantumFunctionError, match='can only be measured once'): - qf(Variable(0.5)) - - def test_qnode_fails_on_qfunc_with_too_many_wires(self, qubit_device_2_wires): - """The device must have sufficient wires for the qfunc""" - - @qml.qnode(qubit_device_2_wires, interface='tf') - def qf(x): - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 2]) - return qml.expval(qml.PauliZ(0)) - - with pytest.raises(QuantumFunctionError, match='applied to invalid wire'): - qf(Variable(0.5)) - - def test_qnode_fails_on_combination_of_cv_and_qbit_ops(self, qubit_device_1_wire): - """CV and discrete operations must not be mixed""" - - @qml.qnode(qubit_device_1_wire, interface='tf') - def qf(x): - qml.RX(x, wires=[0]) - qml.Displacement(0.5, 0, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - with pytest.raises(QuantumFunctionError, match='Continuous and discrete'): - qf(Variable(0.5)) - - def test_qnode_fails_for_cv_ops_on_qubit_device(self, qubit_device_1_wire): - """A qubit device cannot execute CV operations""" - - @qml.qnode(qubit_device_1_wire, interface='tf') - def qf(x): - qml.Displacement(0.5, 0, wires=[0]) - return qml.expval(qml.X(0)) - - 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): - """A qubit device cannot measure CV observables""" - - @qml.qnode(qubit_device_1_wire, interface='tf') - def qf(x): - return qml.expval(qml.X(0)) - - with pytest.raises(QuantumFunctionError, match='Device default.qubit is a qubit device; CV operations are not allowed.'): - qf(Variable(0.5)) - - -class TestTFQNodeParameterHandling: - """Test that the TFQNode properly handles the parameters of qfuncs""" - - def test_qnode_fanout(self, qubit_device_1_wire, tol): - """Tests that qnodes can compute the correct function when the same parameter is used in multiple gates.""" - - @qml.qnode(qubit_device_1_wire, interface='tf') - def circuit(reused_param, other_param): - qml.RX(reused_param, wires=[0]) - qml.RZ(other_param, wires=[0]) - qml.RX(reused_param, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - thetas = tf.linspace(-2*np.pi, 2*np.pi, 7) - - for reused_param in thetas: - for theta in thetas: - other_param = theta ** 2 / 11 - y_eval = circuit(reused_param, other_param) - Rx = Rotx(reused_param.numpy()) - Rz = Rotz(other_param.numpy()) - zero_state = np.array([1.,0.]) - final_state = (Rx @ Rz @ Rx @ zero_state) - y_true = expZ(final_state) - - assert np.allclose(y_eval, y_true, atol=tol, rtol=0) - - def test_qnode_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 TensorFlow. - Test case for a circuit that returns a scalar.""" - - # The objective of this test is not to check if the results are correctly calculated, - # but to check that the interoperability of the different return types works. - @qml.qnode(qubit_device_1_wire, interface='tf') - 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)) # returns a scalar - - grad_target = (np.array(1.), np.array([[0.5, 0.43879, 0], [0, -0.43879, 0]]), np.array(-0.4)) - cost_target = 1.03257 - - args = (Variable(0.46), Variable([[2., 3., 0.3], [7., 4., 2.1]]), Variable(-0.13)) - - def cost(x, array, y): - c = tf.cast(circuit(tf.constant(0.111), array, tf.constant(4.5)), tf.float32) - - return c +0.5*array[0,0] +x -0.4*y - - with tf.GradientTape() as tape: - cost_res = cost(*args) - grad_res = np.array([i.numpy() for i in tape.gradient(cost_res, [args[0], args[2]])]) - - assert np.allclose(cost_res.numpy(), cost_target, atol=tol, rtol=0) - assert np.allclose(grad_res, np.fromiter(grad_target[::2], dtype=np.float32), 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 TensorFlow - Test case for a circuit that returns a 1-vector.""" - - # The objective of this test is not to check if the results are correctly calculated, - # but to check that the interoperability of the different return types works. - @qml.qnode(qubit_device_1_wire, interface='tf') - 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)), # note the comma, returns a 1-vector - - grad_target = (np.array(1.), np.array([[0.5, 0.43879, 0], [0, -0.43879, 0]]), np.array(-0.4)) - cost_target = 1.03257 - - args = (Variable(0.46), Variable([[2., 3., 0.3], [7., 4., 2.1]]), Variable(-0.13)) - - def cost(x, array, y): - c = tf.cast(circuit(tf.constant(0.111), array, tf.constant(4.5)), tf.float32) - c = c[0] # get a scalar - return c +0.5*array[0,0] +x -0.4*y - - with tf.GradientTape() as tape: - cost_res = cost(*args) - grad_res = np.array([i.numpy() for i in tape.gradient(cost_res, [args[0], args[2]])]) - - assert np.allclose(cost_res.numpy(), cost_target, atol=tol, rtol=0) - assert np.allclose(grad_res, np.fromiter(grad_target[::2], dtype=np.float32), 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 TensorFlow - Test case for a circuit that returns a 2-vector.""" - - # The objective of this test is not to check if the results are correctly calculated, - # but to check that the interoperability of the different return types works. - @qml.qnode(qubit_device_2_wires, interface='tf') - 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)) # returns a 2-vector - - grad_target = (np.array(1.), np.array([[0.5, 0.43879, 0], [0, -0.43879, 0]]), np.array(-0.4)) - cost_target = 1.03257 - - args = (Variable(0.46), Variable([[2., 3., 0.3], [7., 4., 2.1]]), Variable(-0.13)) - - def cost(x, array, y): - c = tf.cast(circuit(tf.constant(0.111), array, tf.constant(4.5)), tf.float32) - c = c[0] # get a scalar - return c +0.5*array[0,0] +x -0.4*y - - with tf.GradientTape() as tape: - cost_res = cost(*args) - grad_res = np.array([i.numpy() for i in tape.gradient(cost_res, [args[0], args[2]])]) - - assert np.allclose(cost_res.numpy(), cost_target, atol=tol, rtol=0) - assert np.allclose(grad_res, np.fromiter(grad_target[::2], dtype=np.float32), atol=tol, rtol=0) - - - def test_array_parameters_evaluate(self, qubit_device_2_wires, tol): - """Test that array parameters gives same result as positional arguments.""" - a, b, c = tf.constant(0.5), tf.constant(0.54), tf.constant(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, interface='tf') - def circuit1(x, y, z): - return ansatz(x, y, z) - - @qml.qnode(qubit_device_2_wires, interface='tf') - def circuit2(x, array): - return ansatz(x, array[0], array[1]) - - @qml.qnode(qubit_device_2_wires, interface='tf') - def circuit3(array): - return ansatz(*array) - - positional_res = circuit1(a, b, c) - array_res1 = circuit2(a, Variable([b, c])) - array_res2 = circuit3(Variable([a, b, c])) - - assert np.allclose(positional_res.numpy(), array_res1.numpy(), atol=tol, rtol=0) - assert np.allclose(positional_res.numpy(), array_res2.numpy(), 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 = Variable(0.5), Variable(0.54), Variable(0.3) - - @qml.qnode(qubit_device_2_wires, interface='tf') - 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)) - - res = circuit(a, b, c) - - out_state = np.kron(Rotx(c.numpy()), I) @ np.kron(Roty(b.numpy()), I) @ CNOT \ - @ np.kron(Rotz(b.numpy()), I) @ np.kron(Rotx(a.numpy()), I) @ np.array([1, 0, 0, 0]) - - ex0 = np.vdot(out_state, np.kron(Y, I) @ out_state) - ex1 = np.vdot(out_state, np.kron(I, Z) @ out_state) - ex = np.array([ex0, ex1]) - - assert np.allclose(ex, res.numpy(), atol=tol, rtol=0) - - def test_multiple_keywordargs_used(self, qubit_device_2_wires, tol): - """Tests that qnodes use multiple keyword arguments.""" - - @qml.qnode(qubit_device_2_wires, interface='tf') - 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)) - - c = circuit(tf.constant(1.), x=np.pi, y=np.pi) - - assert np.allclose(c.numpy(), [-1., -1.], 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)) - - circuit = qml.QNode(circuit, qubit_device_2_wires).to_tf() - - c = circuit(tf.constant(1.), x=[np.pi, np.pi]) - assert np.allclose(c.numpy(), [-1., -1.], 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.RY(x, wires=0) - return qml.expval(qml.PauliZ(q)) - - circuit = qml.QNode(circuit, qubit_device_2_wires).to_tf() - - c = circuit(tf.constant(np.pi), q=1) - assert np.allclose(c, 1., atol=tol, rtol=0) - - c = circuit(tf.constant(np.pi)) - assert np.allclose(c.numpy(), -1., 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)) - - circuit = qml.QNode(circuit, qubit_device_1_wire).to_tf() - - c = circuit(tf.constant(1.), x=np.pi) - assert np.allclose(c.numpy(), -1., atol=tol, rtol=0) - - def test_mixture_numpy_tensors(self, qubit_device_2_wires, tol): - """Tests that qnodes work with python types and tensors.""" - - @qml.qnode(qubit_device_2_wires, interface='tf') - def circuit(w, x, y): - qml.RX(x, wires=[0]) - qml.RX(y, wires=[1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - c = circuit(tf.constant(1.), np.pi, np.pi).numpy() - assert np.allclose(c, [-1., -1.], atol=tol, rtol=0) - - def test_keywordarg_updated_in_multiple_calls(self, qubit_device_2_wires): - """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)) - - circuit = qml.QNode(circuit, qubit_device_2_wires).to_tf() - - c1 = circuit(tf.constant(0.1), x=tf.constant(0.)) - c2 = circuit(tf.constant(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)) - - circuit = qml.QNode(circuit, qubit_device_2_wires).to_tf() - - def classnode(w, x=None): - return circuit(w, x=x) - - c = classnode(tf.constant(0.), x=np.pi) - assert np.allclose(c.numpy(), [1., -1.], atol=tol, rtol=0) - - def test_keywordarg_gradient(self, qubit_device_2_wires, tol): - """Tests that qnodes' keyword arguments work with gradients""" - - def circuit(x, y, input_state=np.array([0, 0])): - qml.BasisState(input_state, wires=[0, 1]) - qml.RX(x, wires=[0]) - qml.RY(y, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - circuit = qml.QNode(circuit, qubit_device_2_wires).to_tf() - - x = 0.543 - y = 0.45632 - expected_grad = np.array([np.sin(x)*np.cos(y), np.sin(y)*np.cos(x)]) - - x_t = Variable(x) - y_t = Variable(y) - - # test first basis state against analytic result - with tf.GradientTape() as tape: - c = circuit(x_t, y_t, input_state=np.array([0, 0])) - grads = np.array(tape.gradient(c, [x_t, y_t])) - - assert np.allclose(grads, -expected_grad, atol=tol, rtol=0) - - # test third basis state against analytic result - with tf.GradientTape() as tape: - c = circuit(x_t, y_t, input_state=np.array([1, 0])) - grads = np.array(tape.gradient(c, [x_t, y_t])) - - assert np.allclose(grads, expected_grad, atol=tol, rtol=0) - - # test first basis state via the default keyword argument against analytic result - with tf.GradientTape() as tape: - c = circuit(x_t, y_t) - grads = np.array(tape.gradient(c, [x_t, y_t])) - - assert np.allclose(grads, -expected_grad, atol=tol, rtol=0) - - -class TestIntegration: - """Integration tests to ensure the TensorFlow QNode agrees with the NumPy QNode""" - - @pytest.fixture - def qnodes(self, qubit_device_2_wires): - """Two QNodes to be used for the gradient tests""" - - @qml.qnode(qubit_device_2_wires, interface='autograd') - def circuit(phi, theta): - qml.RX(phi[0], wires=0) - qml.RY(phi[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(theta[0], wires=0) - return qml.expval(qml.PauliZ(0)) - - @qml.qnode(qubit_device_2_wires, interface='tf') - def circuit_tf(phi, theta): - qml.RX(phi[0], wires=0) - qml.RY(phi[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(theta[0], wires=0) - return qml.expval(qml.PauliZ(0)) - - return circuit, circuit_tf - - def test_qnode_evaluation_agrees(self, qnodes, tol): - """Tests that simple example is consistent.""" - circuit, circuit_tf = qnodes - - phi = [0.5, 0.1] - theta = [0.2] - - phi_t = Variable(phi) - theta_t = Variable(theta) - - autograd_eval = circuit(phi, theta) - tf_eval = circuit_tf(phi_t, theta_t) - assert np.allclose(autograd_eval, tf_eval.numpy(), atol=tol, rtol=0) - - def test_qnode_gradient_agrees(self, qnodes, tol): - """Tests that simple gradient example is consistent.""" - circuit, circuit_tf = qnodes - - phi = [0.5, 0.1] - theta = [0.2] - - phi_t = Variable(phi) - theta_t = Variable(theta) - - dcircuit = qml.grad(circuit, [0, 1]) - autograd_grad = dcircuit(phi, theta) - - with tf.GradientTape() as g: - g.watch([phi_t, theta_t]) - y = circuit_tf(phi_t, theta_t) - tf_grad = g.gradient(y, [phi_t, theta_t]) - - assert np.allclose(autograd_grad[0], tf_grad[0], atol=tol, rtol=0) - assert np.allclose(autograd_grad[1], tf_grad[1], atol=tol, rtol=0) - - def test_qnode_jacobian_agrees(self, qnodes, tol): - """Tests that simple jacobian example is consistent.""" - circuit, circuit_tf = qnodes - - phi = [0.5, 0.1] - theta = [0.2] - - phi_t = Variable(phi) - theta_t = Variable(theta) - - jac = qml.grad(circuit, [0, 1]) - autograd_jac = jac(phi, theta) - - with tf.GradientTape() as g: - g.watch([phi_t, theta_t]) - y = circuit_tf(phi_t, theta_t) - - tf_jac = g.jacobian(y, [phi_t, theta_t]) - - assert np.allclose(autograd_jac[0], tf_jac[0], atol=tol, rtol=0) - assert np.allclose(autograd_jac[1], tf_jac[1], atol=tol, rtol=0) - - -gradient_test_data = [ - (0.5, -0.1), - (0.0, np.pi), - (-3.6, -3.6), - (1.0, 2.5), -] - - -class TestTFGradients: - """Integration tests involving gradients of QNodes and hybrid computations using the tf interface""" - - @pytest.fixture - def qnodes(self): - """Two QNodes to be used for the gradient tests""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf") - def f(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - @qml.qnode(dev, interface="tf") - def g(y): - qml.RY(y, wires=0) - return qml.expval(qml.PauliX(0)) - - return f, g - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_addition_qnodes_gradient(self, qnodes, x, y): - """Test the gradient of addition of two QNode circuits""" - f, g = qnodes - - def add(a, b): - return a + b - - xt = Variable(x) - yt = Variable(y) - - # addition - with tf.GradientTape() as tape: - tape.watch([xt, yt]) - a = f(xt) - b = g(yt) - y = add(a, b) - grad = tape.gradient(y, [a, b]) - - assert grad[0].numpy() == 1.0 - assert grad[1].numpy() == 1.0 - - # same tensor added to itself - - with tf.GradientTape() as tape: - tape.watch([xt, yt]) - a = f(xt) - y = add(a, a) - grad = tape.gradient(y, [a, a]) - - assert grad[0].numpy() == 2.0 - assert grad[1].numpy() == 2.0 - - # different qnodes with same input parameter added together - - with tf.GradientTape() as tape: - tape.watch([xt, yt]) - a = f(xt) - b = g(xt) - y = add(a, b) - grad = tape.gradient(y, [a, b]) - - assert grad[0].numpy() == 1.0 - assert grad[1].numpy() == 1.0 - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_subtraction_qnodes_gradient(self, qnodes, x, y): - """Test the gradient of subtraction of two QNode circuits""" - f, g = qnodes - - def subtract(a, b): - return a - b - - xt = Variable(x) - yt = Variable(y) - - # subtraction - with tf.GradientTape() as tape: - tape.watch([xt, yt]) - a = f(xt) - b = g(yt) - y = subtract(a, b) - grad = tape.gradient(y, [a, b]) - - assert grad[0].numpy() == 1.0 - assert grad[1].numpy() == -1.0 - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_multiplication_qnodes_gradient(self, qnodes, x, y): - """Test the gradient of multiplication of two QNode circuits""" - f, g = qnodes - - def mult(a, b): - return a * b - - xt = Variable(x) - yt = Variable(y) - - # multiplication - with tf.GradientTape() as tape: - tape.watch([xt, yt]) - a = f(xt) - b = g(yt) - y = mult(a, b) - grad = tape.gradient(y, [a, b]) - - assert grad[0].numpy() == b.numpy() - assert grad[1].numpy() == a.numpy() - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_division_qnodes_gradient(self, qnodes, x, y, tf_tol): - """Test the gradient of division of two QNode circuits""" - f, g = qnodes - - def div(a, b): - return a / b - - xt = Variable(x) - yt = Variable(y) - - # division - with tf.GradientTape() as tape: - tape.watch([xt, yt]) - a = f(xt) - b = g(yt) - y = div(a, b) - grad = tape.gradient(y, [a, b]) - - assert grad[0].numpy() == 1 / b.numpy() - res = grad[1].numpy() - exp = -a.numpy() / b.numpy() ** 2 - assert np.allclose(res, exp, atol=tf_tol, rtol=0) - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_composition_qnodes_gradient(self, qnodes, x, y): - """Test the gradient of composition of two QNode circuits""" - f, g = qnodes - - xt = Variable(x) - yt = Variable(y) - - # compose function with xt as input - with tf.GradientTape() as tape: - tape.watch([xt]) - y = f(xt) - grad1 = tape.gradient(y, xt) - - with tf.GradientTape() as tape: - tape.watch([xt]) - y = f(xt) - grad2 = tape.gradient(y, xt) - - assert tf.equal(grad1, grad2) - - # compose function with a as input - with tf.GradientTape() as tape: - tape.watch([xt]) - a = f(xt) - y = f(a) - grad1 = tape.gradient(y, a) - - with tf.GradientTape() as tape: - tape.watch([xt]) - a = f(xt) - y = f(a) - grad2 = tape.gradient(y, a) - - assert tf.equal(grad1, grad2) - - # compose function with b as input - with tf.GradientTape() as tape: - tape.watch([xt]) - b = g(xt) - y = g(b) - grad1 = tape.gradient(y, b) - - with tf.GradientTape() as tape: - tape.watch([xt]) - b = g(xt) - y = g(b) - grad2 = tape.gradient(y, b) - - assert tf.equal(grad1, grad2) - - -class TestUnflattenTF: - """Tests for pennylane.interfaces.tf.unflatten_tf""" - - flat = tf.constant([i for i in range(12)]) - - def test_model_number(self): - """Test that the function simply splits flat between its first and remaining elements - when the model is a number""" - unflattened = unflatten_tf(self.flat, 0) - assert tf.equal(unflattened[0], 0) - assert all(tf.equal(unflattened[1], tf.constant([i for i in range(1, 12)]))) - - def test_model_tensor(self): - """Test that function correctly takes the first elements of flat and reshapes it into the - model tensor, while leaving the remaining elements as a flat tensor""" - model = tf.ones((3, 3)) - unflattened = unflatten_tf(self.flat, model) - - target = tf.reshape(self.flat[:9], (3, 3)) - remaining = self.flat[-3:] - - assert np.allclose(unflattened[0].numpy(), target.numpy()) - assert np.allclose(unflattened[1].numpy(), remaining.numpy()) - - def test_model_iterable(self): - """Test that the function correctly unflattens when the model is a list of numbers, - which should result in unflatten_tf returning a list of tensors""" - model = [1] * 12 - unflattened = unflatten_tf(self.flat, model) - - assert all([i.numpy().shape == () for i in unflattened[0]]) - assert unflattened[1].numpy().size == 0 - - def test_model_nested_tensor(self): - """Test that the function correctly unflattens when the model is a nested tensor, - which should result in unflatten_tf returning a list of tensors of the same shape""" - model = [tf.ones(3), tf.ones((2, 2)), tf.ones((3, 1)), tf.ones((1, 2))] - unflattened = unflatten_tf(self.flat, model) - - assert all( - [u.numpy().shape == model[i].numpy().shape for i, u in enumerate(unflattened[0])] - ) - assert unflattened[1].numpy().size == 0 - - -class TestParameterHandlingIntegration: - """Test that the parameter handling for differentiable/non-differentiable - parameters works correctly.""" - - def test_differentiable_parameter_first(self): - """Test that a differentiable parameter used as the first - argument is correctly evaluated by QNode.jacobian, and that - all other non-differentiable parameters are ignored""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf") - def circuit(weights, data1, data2): - # non-differentiable quantum function - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - # differentiable quantum function - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - # non-differentiable quantum function - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # differentiating the circuit wrt the weights - # input weights - weights = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - weights = tf.Variable(weights) - - # input data - data1 = tf.constant([0, 1, 1, 0], dtype=tf.float64) / np.sqrt(2) - data2 = tf.Variable([1, 1], dtype=tf.float64, trainable=False) - - with tf.GradientTape() as tape: - loss = circuit(weights, data1, data2) - - grad = tape.gradient(loss, weights) - - # we do not check for correctness, just that the output - # is the correct shape - assert grad.shape == weights.shape - - # check that the first arg was marked as non-differentiable - assert circuit.get_trainable_args() == {0} - - # Check that the gradient was not computed for the - # non-differentiable elements of `data1` and `data2`. - # First, extract the variable indices that the jacobian method - # 'skipped' (those with grad_method="0"): - non_diff_var_indices = sorted([k for k, v in circuit.par_to_grad_method.items() if v == "0"]) - - # Check that these indices corresponds to the elements of data1 and data2 - # within the flattenened list [weights, data1, data2] - assert non_diff_var_indices == [18, 19, 20, 21, 22, 23] - - def test_differentiable_parameter_middle(self): - """Test that a differentiable parameter provided as the middle - argument is correctly evaluated by QNode.jacobian, and that - all other non-differentiable parameters are ignored""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf") - def circuit(data1, weights, data2): - # non-differentiable quantum function - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - # differentiable quantum function - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - # non-differentiable quantum function - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # input weights - weights = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - weights = tf.Variable(weights) - - # input data - data1 = tf.constant([0, 1, 1, 0], dtype=tf.float64) / np.sqrt(2) - data2 = tf.constant([1, 1], dtype=tf.float64) - - with tf.GradientTape() as tape: - loss = circuit(data1, weights, data2) - - grad = tape.gradient(loss, weights) - - # we do not check for correctness, just that the output - # is the correct shape - assert grad.shape == weights.shape - - # check that the second arg was marked as non-differentiable - assert circuit.get_trainable_args() == {1} - - # Check that the gradient was not computed for the - # non-differentiable elements of `data1` and `data2`. - # First, extract the variable indices that the jacobian method - # 'skipped' (those with grad_method="0"): - non_diff_var_indices = sorted([k for k, v in circuit.par_to_grad_method.items() if v == "0"]) - - # Check that these indices corresponds to the elements of data1 and data2 - # within the flattenened list [data1, weights, data2] - assert non_diff_var_indices == [0, 1, 2, 3, 22, 23] - - def test_differentiable_parameter_last(self): - """Test that a differentiable parameter used as the last - argument is correctly evaluated by QNode.jacobian, and that - all other non-differentiable parameters are ignored""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf") - def circuit(data1, data2, weights): - # non-differentiable quantum function - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - # differentiable quantum function - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - # non-differentiable quantum function - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # input weights - weights = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - weights = tf.Variable(weights) - - # input data - data1 = tf.constant([0, 1, 1, 0], dtype=tf.float64) / np.sqrt(2) - data2 = tf.constant([1, 1], dtype=tf.float64) - - with tf.GradientTape() as tape: - loss = circuit(data1, data2, weights) - - grad = tape.gradient(loss, weights) - - # we do not check for correctness, just that the output - # is the correct shape - assert grad.shape == weights.shape - - # check that the last arg was marked as non-differentiable - assert circuit.get_trainable_args() == {2} - - # Check that the gradient was not computed for the - # non-differentiable elements of `data1` and `data2`. - # First, extract the variable indices that the jacobian method - # 'skipped' (those with grad_method="0"): - non_diff_var_indices = sorted([k for k, v in circuit.par_to_grad_method.items() if v == "0"]) - - # Check that these indices corresponds to the elements of data1 and data2 - # within the flattenened list [data1, data2, weights] - assert non_diff_var_indices == [0, 1, 2, 3, 4, 5] - - def test_multiple_differentiable_and_non_differentiable_parameters(self): - """Test that multiple differentiable and non-differentiable parameters - works as expected""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf") - def circuit(data1, weights1, data2, weights2): - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - qml.templates.StronglyEntanglingLayers(weights1, wires=[0, 1]) - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - qml.templates.StronglyEntanglingLayers(weights2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # input weights - weights1 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - weights2 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=4) - - weights1 = tf.Variable(weights1) - weights2 = tf.Variable(weights2) - - # input data - data1 = tf.constant([0, 1, 1, 0], dtype=tf.float64) / np.sqrt(2) - data2 = tf.Variable([1, 1], dtype=tf.float64, trainable=False) - - with tf.GradientTape() as tape: - loss = circuit(data1, weights1, data2, weights2) - - grad = tape.gradient(loss, [weights1, weights2]) - - # we do not check for correctness, just that the output - # is the correct shape - assert grad[0].shape == weights1.shape - assert grad[1].shape == weights2.shape - - # check that the gradient was only computed for the - # differentiable elements of `weights`, not the data input - assert circuit.get_trainable_args() == {1, 3} - - def test_gradient_non_differentiable_none(self, mocker): - """Test that the gradient of a non-differentiable tensor is None""" - spy = mocker.spy(qml.qnodes.JacobianQNode, "jacobian") - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf") - def circuit(weights): - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - weights = tf.constant(qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3)) - - with tf.GradientTape() as tape: - loss = circuit(weights) - - assert circuit.get_trainable_args() == set() - - grad = tape.gradient(loss, weights) - assert grad is None - - spy.assert_not_called() - - def test_non_differentiable_watch(self): - """Test that watching a non-differentiable tensor makes it differentiable""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf") - def circuit(weights): - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - weights = tf.constant(qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3)) - - with tf.GradientTape() as tape: - tape.watch([weights]) - loss = circuit(weights) - - grad = tape.gradient(loss, weights) - assert grad is not None - assert grad.shape == weights.shape - - assert circuit.get_trainable_args() == {0} - - def test_chained_qnodes(self): - """Test that the gradient of chained QNodes works without error""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf") - def circuit1(weights): - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - @qml.qnode(dev, interface="tf") - def circuit2(data, weights): - qml.templates.AngleEmbedding(data, wires=[0, 1]) - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - return qml.expval(qml.PauliX(0)) - - def cost(weights): - w1, w2 = weights - c1 = circuit1(w1) - c2 = circuit2(c1, w2) - return tf.reduce_sum(c2) ** 2 - - # input weights - w1 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - w2 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=4) - - w1 = tf.Variable(w1) - w2 = tf.Variable(w2) - - weights = [w1, w2] - - with tf.GradientTape() as tape: - loss = cost(weights) - - grad = tape.gradient(loss, weights) - - assert len(grad) == 2 - assert grad[0].shape == w1.shape - assert grad[1].shape == w2.shape - - def test_gradient_value(self, tol): - """Test that the returned gradient value for a qubit QNode is correct, - when one of the arguments is non-differentiable.""" - dev = qml.device("default.qubit", wires=3) - - @qml.qnode(dev, interface="tf") - def circuit(a, b, c): - qml.RX(a, wires=0) - qml.RX(b, wires=1) - qml.RX(c, wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - return qml.expval(qml.PauliX(0) @ qml.PauliY(2)) - - theta = tf.Variable(0.5) - phi = tf.Variable(0.1) - - # varphi is non-differentiable - varphi = tf.constant(0.23) - - with tf.GradientTape() as tape: - loss = circuit(theta, phi, varphi) - - grad = tape.gradient(loss, [theta, phi, varphi]) - - expected = [ - tf.cos(theta) * tf.sin(phi) * tf.sin(varphi), - tf.sin(theta) * tf.cos(phi) * tf.sin(varphi), - ] - - assert np.allclose(grad[:2], expected, atol=tol, rtol=0) - assert grad[2] is None - - # check that the gradient was not computed for varphi - assert circuit.get_trainable_args() == {0, 1} - - def test_chained_gradient_value(self, tol): - """Test that the returned gradient value for two chained qubit QNodes - is correct.""" - dev1 = qml.device("default.qubit", wires=3) - - @qml.qnode(dev1, interface="tf") - def circuit1(a, b, c): - qml.RX(a, wires=0) - qml.RX(b, wires=1) - qml.RX(c, wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(2)) - - dev2 = qml.device("default.qubit", wires=2) - - @qml.qnode(dev2, interface="tf") - def circuit2(data, weights): - qml.RX(data[0], wires=0) - qml.RX(data[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.RZ(weights[0], wires=0) - qml.RZ(weights[1], wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliX(0) @ qml.PauliY(1)) - - def cost(a, b, c, weights): - return circuit2(circuit1(a, b, c), weights) - - # Set the first parameter of circuit1 as non-differentiable. - a = tf.constant(0.4) - - # the remaining free parameters are all differentiable - b = tf.Variable(0.5) - c = tf.Variable(0.1) - weights = tf.Variable([0.2, 0.3]) - - with tf.GradientTape() as tape: - loss = cost(a, b, c, weights) - - grad = tape.gradient(loss, [b, c, weights]) - - # Output should have shape [dcost/db, dcost/dc, dcost/dw], - # where b,c are scalars, and w is a vector of length 2. - assert len(grad) == 3 - assert grad[0].shape == tuple() # scalar - assert grad[1].shape == tuple() # scalar - assert grad[2].shape == (2,) # vector - - cacbsc = tf.cos(a)*tf.cos(b)*tf.sin(c) - - expected = [ - # analytic expression for dcost/db - -tf.cos(a)*tf.sin(b)*tf.sin(c)*tf.cos(cacbsc)*tf.sin(weights[0])*tf.sin(tf.cos(a)), - # analytic expression for dcost/dc - tf.cos(a)*tf.cos(b)*tf.cos(c)*tf.cos(cacbsc)*tf.sin(weights[0])*tf.sin(tf.cos(a)), - # analytic expression for dcost/dw[0] - tf.sin(cacbsc)*tf.cos(weights[0])*tf.sin(tf.cos(a)), - # analytic expression for dcost/dw[1] - 0 - ] - - # np.hstack 'flattens' the ragged gradient array allowing it - # to be compared with the expected result - assert np.allclose(np.hstack(grad), expected, atol=tol, rtol=0) - - # Check that the gradient was computed - # for all parameters in circuit2 - assert circuit2.get_trainable_args() == {0, 1} - - # check that the gradient was not computed - # for the first parameter of circuit1 - assert circuit1.get_trainable_args() == {1, 2} - - def test_non_diff_not_a_variable(self): - """Test that an argument marked as non-differentiable - is not wrapped as a variable.""" - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev, interface="tf") - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - - assert isinstance(x, qml.variable.Variable) - assert isinstance(y, np.float32) - assert isinstance(z, qml.variable.Variable) - - return qml.expval(qml.PauliZ(0)) - - x = tf.Variable(1.) - y = tf.constant(2.) - z = tf.Variable(3.) - - with tf.GradientTape() as tape: - res = circuit(x, y, z) - - assert circuit.get_trainable_args() == {0, 2} - - # the calls to `numpy()` are required so that TF 1.15 does not - # automatically cast the TensorFlow tensors on comparison. - assert circuit.arg_vars[0].val == x.numpy() - assert circuit.arg_vars[1] == y.numpy() - assert circuit.arg_vars[2].val == z.numpy() - - a = 0.6 - b = 0.2 - test_data = [ - (np.array([0, 1]), np.cos(2*a) * np.cos(b), [-2 * np.cos(b) * np.sin(2*a), -np.cos(2*a) * np.sin(b)]), - (np.array([1, 0]), -np.cos(b) * np.sin(b), [0, -np.cos(b) ** 2 + np.sin(b) ** 2]), - ] - - @pytest.mark.parametrize("w, expected_res, expected_grad", test_data) - def test_non_diff_wires_argument(self, w, expected_res, expected_grad, tol): - """Test that passing wires as a non-differentiable positional - argument works correctly, and results in the expected QNode result and gradient.""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf") - def circuit(wires, params): - qml.Hadamard(wires=wires[0]) - qml.CNOT(wires=[wires[0], wires[1]]) - qml.RX(params[0], wires=wires[0]) - qml.RY(params[1], wires=wires[1]) - qml.CNOT(wires=[wires[1], wires[0]]) - qml.RX(params[0], wires=wires[0]) - qml.RY(params[1], wires=wires[1]) - return qml.expval(qml.PauliZ(0)) - - params = tf.Variable([0.6, 0.2]) - - with tf.GradientTape() as tape: - res = circuit(w, params) - - assert circuit.get_trainable_args() == {1} - assert np.allclose(res, expected_res, atol=tol, rtol=0) - - grad = tape.gradient(res, params) - - assert circuit.get_trainable_args() == {1} - assert np.allclose(grad, expected_grad, atol=tol, rtol=0) - - def test_call_changing_trainability(self): - """Test that trainability properly changes between QNode calls""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf") - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - return qml.expval(qml.PauliZ(0)) - - x = tf.Variable(1.) - y = tf.constant(2.) - z = tf.Variable(3.) - - with tf.GradientTape() as tape: - res = circuit(x, y, z) - - assert circuit.get_trainable_args() == {0, 2} - - x = 1. - - with tf.GradientTape() as tape: - tape.watch([y, z]) - res = circuit(x, y, z) - - assert circuit.get_trainable_args() == {1, 2} - - def test_immutability(self): - """Test that changing parameter differentiability raises an exception - on immutable QNodes.""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="tf", mutable=False) - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - return qml.expval(qml.PauliZ(0)) - - x = tf.Variable(1.) - y = 2. - z = tf.Variable(3.) - - with tf.GradientTape() as tape: - # with TensorFlow 2.3, variables used outside a tape - # context now register as non-differentiable by default. - res = circuit(x, y, z) - - assert circuit.get_trainable_args() == {0, 2} - - # change values and compute the gradient again - with tf.GradientTape() as tape: - res = circuit(2*x, -y, z) - - assert circuit.get_trainable_args() == {0, 2} - - # attempting to change differentiability raises an error - x = 1. - y = tf.Variable(y) - - with pytest.raises(qml.QuantumFunctionError, match="cannot be modified"): - circuit(x, y, z) - - -class TestConversion: - """Integration tests to make sure that to_tf() correctly converts - QNodes with/without pre-existing interfaces""" - - @pytest.fixture - def qnode(self, interface, torch_support): - """Returns a simple QNode corresponding to cos(x), - with interface as determined by the interface fixture""" - if interface == "torch" and not torch_support: - pytest.skip("Skipped, no torch support") - - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev, interface=interface) - def circuit(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - return circuit - - @pytest.mark.parametrize("interface", ["tf"]) - def test_tf_conversion(self, qnode, tol): - """Tests that the to_tf() function ignores QNodes that already - have the TF interface.""" - converted_qnode = to_tf(qnode) - assert converted_qnode is qnode - - x = tf.Variable(0.4) - - with tf.GradientTape() as tape: - res = converted_qnode(x) - - assert np.allclose(res, tf.cos(x), atol=tol, rtol=0) - - grad = tape.gradient(res, x) - assert np.allclose(grad, -tf.sin(x), atol=tol, rtol=0) - - @pytest.mark.parametrize("interface", [None, "torch", "autograd"]) - def test_other_conversion(self, qnode, tol): - """Tests that the to_tf() function correctly converts both torch and autograd qnodes - and QNodes with no interface.""" - converted_qnode = to_tf(qnode) - assert converted_qnode is not qnode - assert converted_qnode._qnode is getattr(qnode, "_qnode", qnode) - - x = tf.Variable(0.4) - - with tf.GradientTape() as tape: - res = converted_qnode(x) - - assert np.allclose(res, tf.cos(x), atol=tol, rtol=0) - - grad = tape.gradient(res, x) - assert np.allclose(grad, -tf.sin(x), atol=tol, rtol=0) diff --git a/tests/interfaces/test_torch.py b/tests/interfaces/test_torch.py deleted file mode 100644 index 788e6cfc323..00000000000 --- a/tests/interfaces/test_torch.py +++ /dev/null @@ -1,1271 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane.interface.torch` QNode interface. -""" - -import pytest - -import numpy as np - -torch = pytest.importorskip("torch", minversion="1.1") -from torch.autograd import Variable - -import pennylane as qml - -from pennylane.utils import _flatten, unflatten -from pennylane.qnodes import QNode, QuantumFunctionError -from pennylane._device import DeviceError -from pennylane.interfaces.torch import to_torch, unflatten_torch - -from gate_data import CNOT, Rotx, Roty, Rotz, I, Y, Z - -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - -def expZ(state): - return np.abs(state[0]) ** 2 - np.abs(state[1]) ** 2 - - -class TestTorchQNodeExceptions(): - """TorchQNode basic tests.""" - - def test_qnode_fails_on_wrong_return_type(self, qubit_device_2_wires): - """The qfunc must return only Expectations""" - @qml.qnode(qubit_device_2_wires, interface='torch') - def qf(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)), 0.3 - - with pytest.raises(QuantumFunctionError, match='must return either'): - qf(torch.tensor(0.5)) - - def test_qnode_fails_on_expval_not_returned(self, qubit_device_2_wires): - """All expectation values in the qfunc must be returned""" - - @qml.qnode(qubit_device_2_wires, interface='torch') - def qf(x): - qml.RX(x, wires=[0]) - ex = qml.expval(qml.PauliZ(1)) - return qml.expval(qml.PauliZ(0)) - - with pytest.raises(QuantumFunctionError, match='All measured observables'): - qf(torch.tensor(0.5)) - - def test_qnode_fails_on_wrong_expval_order(self, qubit_device_2_wires): - """Expvals must be returned in the order they were created in""" - - @qml.qnode(qubit_device_2_wires, interface='torch') - def qf(x): - qml.RX(x, wires=[0]) - ex = qml.expval(qml.PauliZ(1)) - return qml.expval(qml.PauliZ(0)), ex - - with pytest.raises(QuantumFunctionError, match='All measured observables'): - qf(torch.tensor(0.5)) - - def test_qnode_fails_on_gates_after_measurements(self, qubit_device_2_wires): - """Gates have to precede measurements""" - - @qml.qnode(qubit_device_2_wires, interface='torch') - def qf(x): - qml.RX(x, wires=[0]) - ev = qml.expval(qml.PauliZ(1)) - qml.RY(0.5, wires=[0]) - return ev - - with pytest.raises(QuantumFunctionError, match='gates must precede'): - qf(torch.tensor(0.5)) - - def test_qnode_fails_on_multiple_measurements_of_same_wire(self, qubit_device_2_wires): - """A wire can only be measured once""" - - @qml.qnode(qubit_device_2_wires, interface='torch') - def qf(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)) - - with pytest.raises(QuantumFunctionError, match='can only be measured once'): - qf(torch.tensor(0.5)) - - def test_qnode_fails_on_qfunc_with_too_many_wires(self, qubit_device_2_wires): - """The device must have sufficient wires for the qfunc""" - - @qml.qnode(qubit_device_2_wires, interface='torch') - def qf(x): - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 2]) - return qml.expval(qml.PauliZ(0)) - - with pytest.raises(QuantumFunctionError, match='applied to invalid wire'): - qf(torch.tensor(0.5)) - - def test_qnode_fails_on_combination_of_cv_and_qbit_ops(self, qubit_device_1_wire): - """CV and discrete operations must not be mixed""" - - @qml.qnode(qubit_device_1_wire, interface='torch') - def qf(x): - qml.RX(x, wires=[0]) - qml.Displacement(0.5, 0, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - with pytest.raises(QuantumFunctionError, match='Continuous and discrete'): - qf(torch.tensor(0.5)) - - def test_qnode_fails_for_cv_ops_on_qubit_device(self, qubit_device_1_wire): - """A qubit device cannot execute CV operations""" - - @qml.qnode(qubit_device_1_wire, interface='torch') - def qf(x): - qml.Displacement(0.5, 0, wires=[0]) - return qml.expval(qml.X(0)) - - 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): - """A qubit device cannot measure CV observables""" - - @qml.qnode(qubit_device_1_wire, interface='torch') - def qf(x): - return qml.expval(qml.X(0)) - - with pytest.raises(QuantumFunctionError, match='Device default.qubit is a qubit device; CV operations are not allowed.'): - qf(torch.tensor(0.5)) - - -class TestTorchQNodeParameterHandling: - """Test that the TorchQNode properly handles the parameters of qfuncs""" - - def test_qnode_fanout(self, qubit_device_1_wire, tol): - """Tests that qnodes can compute the correct function when the same parameter is used in multiple gates.""" - - @qml.qnode(qubit_device_1_wire, interface='torch') - def circuit(reused_param, other_param): - qml.RX(reused_param, wires=[0]) - qml.RZ(other_param, wires=[0]) - qml.RX(reused_param, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - thetas = torch.linspace(-2*np.pi, 2*np.pi, 7) - - for reused_param in thetas: - for theta in thetas: - other_param = theta ** 2 / 11 - y_eval = circuit(reused_param, other_param) - Rx = Rotx(reused_param.numpy()) - Rz = Rotz(other_param.numpy()) - zero_state = np.array([1.,0.]) - final_state = (Rx @ Rz @ Rx @ zero_state) - y_true = expZ(final_state) - - assert np.allclose(y_eval, y_true, atol=tol, rtol=0) - - def test_qnode_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 PyTorch. - Test case for a circuit that returns a scalar.""" - - # The objective of this test is not to check if the results are correctly calculated, - # but to check that the interoperability of the different return types works. - @qml.qnode(qubit_device_1_wire, interface='torch') - 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)) # returns a scalar - - grad_target = (np.array(1.), np.array([[0.5, 0.43879, 0], [0, -0.43879, 0]]), np.array(-0.4)) - cost_target = 1.03257 - - args = (torch.tensor(0.46), torch.tensor([[2., 3., 0.3], [7., 4., 2.1]]), torch.tensor(-0.13)) - for i in args: - i.requires_grad = True - - def cost(x, array, y): - c = torch.as_tensor(circuit(torch.tensor(0.111), array, torch.tensor(4.5)), dtype=torch.float32) - return c +0.5*array[0,0] +x -0.4*y - - cost_res = cost(*args) - cost_res.backward() - - assert np.allclose(cost_res.detach().numpy(), cost_target, atol=tol, rtol=0) - - for i in range(3): - assert np.allclose(args[i].grad.detach().numpy(), grad_target[i], 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 PyTorch. - Test case for a circuit that returns a 1-vector.""" - - # The objective of this test is not to check if the results are correctly calculated, - # but to check that the interoperability of the different return types works. - @qml.qnode(qubit_device_1_wire, interface='torch') - 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)), # note the comma, returns a 1-vector - - grad_target = (np.array(1.), np.array([[0.5, 0.43879, 0], [0, -0.43879, 0]]), np.array(-0.4)) - cost_target = 1.03257 - - args = (torch.tensor(0.46), torch.tensor([[2., 3., 0.3], [7., 4., 2.1]]), torch.tensor(-0.13)) - for i in args: - i.requires_grad = True - - def cost(x, array, y): - c = torch.as_tensor(circuit(torch.tensor(0.111), array, torch.tensor(4.5)), dtype=torch.float32) - c = c[0] # get a scalar - return c +0.5*array[0,0] +x -0.4*y - - cost_res = cost(*args) - cost_res.backward() - - assert np.allclose(cost_res.detach().numpy(), cost_target, atol=tol, rtol=0) - - for i in range(3): - assert np.allclose(args[i].grad.detach().numpy(), grad_target[i], 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 PyTorch. - Test case for a circuit that returns a 2-vector.""" - - # The objective of this test is not to check if the results are correctly calculated, - # but to check that the interoperability of the different return types works. - @qml.qnode(qubit_device_2_wires, interface='torch') - 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)) # returns a 2-vector - - grad_target = (np.array(1.), np.array([[0.5, 0.43879, 0], [0, -0.43879, 0]]), np.array(-0.4)) - cost_target = 1.03257 - - args = (torch.tensor(0.46), torch.tensor([[2., 3., 0.3], [7., 4., 2.1]]), torch.tensor(-0.13)) - for i in args: - i.requires_grad = True - - def cost(x, array, y): - c = torch.as_tensor(circuit(torch.tensor(0.111), array, torch.tensor(4.5)), dtype=torch.float32) - c = c[0] # get a scalar - return c +0.5*array[0,0] +x -0.4*y - - cost_res = cost(*args) - cost_res.backward() - - assert np.allclose(cost_res.detach().numpy(), cost_target, atol=tol, rtol=0) - - for i in range(3): - assert np.allclose(args[i].grad.detach().numpy(), grad_target[i], atol=tol, rtol=0) - - - def test_array_parameters_evaluate(self, qubit_device_2_wires, tol): - """Test that array parameters gives same result as positional arguments.""" - a, b, c = torch.tensor(0.5), torch.tensor(0.54), torch.tensor(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, interface='torch') - def circuit1(x, y, z): - return ansatz(x, y, z) - - @qml.qnode(qubit_device_2_wires, interface='torch') - def circuit2(x, array): - return ansatz(x, array[0], array[1]) - - @qml.qnode(qubit_device_2_wires, interface='torch') - def circuit3(array): - return ansatz(*array) - - positional_res = circuit1(a, b, c) - array_res1 = circuit2(a, torch.tensor([b, c])) - array_res2 = circuit3(torch.tensor([a, b, c])) - - assert np.allclose(positional_res.numpy(), array_res1.numpy(), atol=tol, rtol=0) - assert np.allclose(positional_res.numpy(), array_res2.numpy(), 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 = torch.tensor(0.5), torch.tensor(0.54), torch.tensor(0.3) - - @qml.qnode(qubit_device_2_wires, interface='torch') - 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)) - - res = circuit(a, b, c) - - out_state = np.kron(Rotx(c.numpy()), I) @ np.kron(Roty(b.numpy()), I) @ CNOT \ - @ np.kron(Rotz(b.numpy()), I) @ np.kron(Rotx(a.numpy()), I) @ np.array([1, 0, 0, 0]) - - ex0 = np.vdot(out_state, np.kron(Y, I) @ out_state) - ex1 = np.vdot(out_state, np.kron(I, Z) @ out_state) - ex = np.array([ex0, ex1]) - - assert np.allclose(ex, res.numpy(), atol=tol, rtol=0) - - def test_multiple_keywordargs_used(self, qubit_device_2_wires, tol): - """Tests that qnodes use multiple keyword arguments.""" - - @qml.qnode(qubit_device_2_wires, interface='torch') - 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)) - - c = circuit(torch.tensor(1.), x=np.pi, y=np.pi) - - assert np.allclose(c.numpy(), [-1., -1.], 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)) - - circuit = qml.QNode(circuit, qubit_device_2_wires).to_torch() - - c = circuit(torch.tensor(1.), x=[np.pi, np.pi]) - assert np.allclose(c.numpy(), [-1., -1.], 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.RY(x, wires=0) - return qml.expval(qml.PauliZ(q)) - - circuit = qml.QNode(circuit, qubit_device_2_wires).to_torch() - - c = circuit(torch.tensor(np.pi), q=1) - assert np.allclose(c, 1., atol=tol, rtol=0) - - c = circuit(torch.tensor(np.pi)) - assert np.allclose(c.numpy(), -1., 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)) - - circuit = qml.QNode(circuit, qubit_device_1_wire).to_torch() - - c = circuit(torch.tensor(1.), x=np.pi) - assert np.allclose(c.numpy(), -1., atol=tol, rtol=0) - - def test_mixture_numpy_tensors(self, qubit_device_2_wires, tol): - """Tests that qnodes work with python types and tensors.""" - - @qml.qnode(qubit_device_2_wires, interface='torch') - def circuit(w, x, y): - qml.RX(x, wires=[0]) - qml.RX(y, wires=[1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - c = circuit(torch.tensor(1.), np.pi, np.pi).detach().numpy() - assert np.allclose(c, [-1., -1.], atol=tol, rtol=0) - - def test_keywordarg_updated_in_multiple_calls(self, qubit_device_2_wires): - """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)) - - circuit = qml.QNode(circuit, qubit_device_2_wires).to_torch() - - c1 = circuit(torch.tensor(0.1), x=torch.tensor(0.)) - c2 = circuit(torch.tensor(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)) - - circuit = qml.QNode(circuit, qubit_device_2_wires).to_torch() - - def classnode(w, x=None): - return circuit(w, x=x) - - c = classnode(torch.tensor(0.), x=np.pi) - assert np.allclose(c.numpy(), [1., -1.], atol=tol, rtol=0) - - def test_keywordarg_gradient(self, qubit_device_2_wires, tol): - """Tests that qnodes' keyword arguments work with gradients""" - - def circuit(x, y, input_state=np.array([0, 0])): - qml.BasisState(input_state, wires=[0, 1]) - qml.RX(x, wires=[0]) - qml.RY(y, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - circuit = qml.QNode(circuit, qubit_device_2_wires).to_torch() - - x = 0.543 - y = 0.45632 - - x_t = torch.autograd.Variable(torch.tensor(x), requires_grad=True) - y_t = torch.autograd.Variable(torch.tensor(y), requires_grad=True) - c = circuit(x_t, y_t, input_state=np.array([0, 0])) - c.backward() - assert np.allclose(x_t.grad.numpy(), [-np.sin(x)*np.cos(y)], atol=tol, rtol=0) - assert np.allclose(y_t.grad.numpy(), [-np.sin(y)*np.cos(x)], atol=tol, rtol=0) - - x_t = torch.autograd.Variable(torch.tensor(x), requires_grad=True) - y_t = torch.autograd.Variable(torch.tensor(y), requires_grad=True) - c = circuit(x_t, y_t, input_state=np.array([1, 0])) - c.backward() - assert np.allclose(x_t.grad.numpy(), [np.sin(x)*np.cos(y)], atol=tol, rtol=0) - assert np.allclose(y_t.grad.numpy(), [np.sin(y)*np.cos(x)], atol=tol, rtol=0) - - x_t = torch.autograd.Variable(torch.tensor(x), requires_grad=True) - y_t = torch.autograd.Variable(torch.tensor(y), requires_grad=True) - c = circuit(x_t, y_t) - c.backward() - assert np.allclose(x_t.grad.numpy(), [-np.sin(x)*np.cos(y)], atol=tol, rtol=0) - assert np.allclose(y_t.grad.numpy(), [-np.sin(y)*np.cos(x)], atol=tol, rtol=0) - - -class TestIntegration(): - """Integration tests to ensure the Torch QNode agrees with the NumPy QNode""" - - def test_qnode_evaluation_agrees(self, qubit_device_2_wires, tol): - """Tests that simple example is consistent.""" - - @qml.qnode(qubit_device_2_wires, interface='autograd') - def circuit(phi, theta): - qml.RX(phi[0], wires=0) - qml.RY(phi[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(theta[0], wires=0) - return qml.expval(qml.PauliZ(0)) - - @qml.qnode(qubit_device_2_wires, interface='torch') - def circuit_torch(phi, theta): - qml.RX(phi[0], wires=0) - qml.RY(phi[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(theta[0], wires=0) - return qml.expval(qml.PauliZ(0)) - - phi = [0.5, 0.1] - theta = [0.2] - - phi_t = torch.tensor(phi) - theta_t = torch.tensor(theta) - - autograd_eval = circuit(phi, theta) - torch_eval = circuit_torch(phi_t, theta_t) - assert np.allclose(autograd_eval, torch_eval.detach().numpy(), atol=tol, rtol=0) - - def test_qnode_gradient_agrees(self, qubit_device_2_wires, tol): - """Tests that simple gradient example is consistent.""" - - @qml.qnode(qubit_device_2_wires, interface='autograd') - def circuit(phi, theta): - qml.RX(phi[0], wires=0) - qml.RY(phi[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(theta[0], wires=0) - return qml.expval(qml.PauliZ(0)) - - @qml.qnode(qubit_device_2_wires, interface='torch') - def circuit_torch(phi, theta): - qml.RX(phi[0], wires=0) - qml.RY(phi[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(theta[0], wires=0) - return qml.expval(qml.PauliZ(0)) - - phi = [0.5, 0.1] - theta = [0.2] - - phi_t = torch.autograd.Variable(torch.tensor(phi), requires_grad=True) - theta_t = torch.autograd.Variable(torch.tensor(theta), requires_grad=True) - - dcircuit = qml.grad(circuit, [0, 1]) - autograd_grad = dcircuit(phi, theta) - - torch_eval = circuit_torch(phi_t, theta_t) - torch_eval.backward() - - assert np.allclose(autograd_grad[0], phi_t.grad.detach().numpy(), atol=tol, rtol=0) - assert np.allclose(autograd_grad[1], theta_t.grad.detach().numpy(), atol=tol, rtol=0) - - -gradient_test_data = [ - (0.5, -0.1), - (0.0, np.pi), - (-3.6, -3.6), - (1.0, 2.5), -] - - -class TestTorchGradients: - """Integration tests involving gradients of QNodes and hybrid computations using the torch interface""" - - @pytest.fixture - def qnodes(self): - """Two QNodes to be used for the gradient tests""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="torch") - def f(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - @qml.qnode(dev, interface="torch") - def g(y): - qml.RY(y, wires=0) - return qml.expval(qml.PauliX(0)) - - return f, g - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_addition_qnodes_gradient(self, qnodes, x, y): - """Test the gradient of addition of two QNode circuits""" - f, g = qnodes - - def add(a, b): - return a + b - - xt = torch.autograd.Variable(torch.tensor(x), requires_grad=True) - yt = torch.autograd.Variable(torch.tensor(y), requires_grad=True) - - # addition - a = f(xt) - b = g(yt) - a.retain_grad() - b.retain_grad() - - add(a, b).backward() - assert a.grad == 1.0 - assert b.grad == 1.0 - - # same tensor added to itself - - a = f(xt) - a.retain_grad() - - add(a, a).backward() - assert a.grad == 2.0 - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_subtraction_qnodes_gradient(self, qnodes, x, y): - """Test the gradient of subtraction of two QNode circuits""" - f, g = qnodes - - def subtract(a, b): - return a - b - - xt = torch.autograd.Variable(torch.tensor(x), requires_grad=True) - yt = torch.autograd.Variable(torch.tensor(y), requires_grad=True) - - # subtraction - a = f(xt) - b = g(yt) - a.retain_grad() - b.retain_grad() - - subtract(a, b).backward() - assert a.grad == 1.0 - assert b.grad == -1.0 - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_multiplication_qnodes_gradient(self, qnodes, x, y): - """Test the gradient of multiplication of two QNode circuits""" - f, g = qnodes - - def mult(a, b): - return a * b - - xt = torch.autograd.Variable(torch.tensor(x), requires_grad=True) - yt = torch.autograd.Variable(torch.tensor(y), requires_grad=True) - - # multiplication - a = f(xt) - b = g(yt) - a.retain_grad() - b.retain_grad() - - mult(a, b).backward() - assert a.grad == b - assert b.grad == a - - a = f(xt) - b = g(yt) - a.retain_grad() - b.retain_grad() - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_division_qnodes_gradient(self, qnodes, x, y): - """Test the gradient of division of two QNode circuits""" - f, g = qnodes - - def div(a, b): - return a / b - - xt = torch.autograd.Variable(torch.tensor(x), requires_grad=True) - yt = torch.autograd.Variable(torch.tensor(y), requires_grad=True) - - # division - # multiplication - a = f(xt) - b = g(yt) - a.retain_grad() - b.retain_grad() - - div(a, b).backward() - assert a.grad == 1 / b - assert torch.isclose(b.grad, -a / b ** 2) - - @pytest.mark.parametrize("x, y", gradient_test_data) - def test_composition_qnodes_gradient(self, qnodes, x, y): - """Test the gradient of composition of two QNode circuits""" - f, g = qnodes - - def compose(f, x): - return f(x) - - xt = torch.autograd.Variable(torch.tensor(x), requires_grad=True) - yt = torch.autograd.Variable(torch.tensor(y), requires_grad=True) - - # compose function with xt as input - compose(f, xt).backward() - grad1 = xt.grad.detach().numpy() - - f(xt).backward() - grad2 = xt.grad.detach().numpy() - assert grad1 == grad2 - - # compose function with a as input - a = f(xt) - a.retain_grad() - - compose(f, a).backward() - grad1 = a.grad.detach().numpy() - - a = f(xt) - a.retain_grad() - - f(a).backward() - grad2 = a.grad.detach().numpy() - assert grad1 == grad2 - - # compose function with b as input - b = g(yt) - b.retain_grad() - - compose(f, b).backward() - grad1 = b.grad.detach().numpy() - - b = g(yt) - b.retain_grad() - - f(b).backward() - grad2 = b.grad.detach().numpy() - assert grad1 == grad2 - - -class TestUnflatten: - """Tests for pennylane.interfaces.torch.unflatten_torch""" - - flat = torch.tensor([i for i in range(12)]) - - def test_unsupported_type_error(self): - """Test that an unsupported type exception is raised if there is - an unknown element in the model.""" - with pytest.raises(TypeError, match="Unsupported type in the model"): - unflatten_torch(self.flat, [object()]) - - def test_model_number(self): - """Test that the function simply splits flat between its first and remaining elements - when the model is a number""" - unflattened = unflatten_torch(self.flat, 0) - assert unflattened[0] == 0 - assert torch.all(unflattened[1] == self.flat[1:]) - - def test_model_tensor(self): - """Test that function correctly takes the first elements of flat and reshapes it into the - model tensor, while leaving the remaining elements as a flat tensor""" - model = torch.ones((3, 3)) - unflattened = unflatten_torch(self.flat, model) - - target = self.flat[:9].view((3, 3)) - remaining = self.flat[-3:] - - assert torch.all(unflattened[0] == target) - assert torch.all(unflattened[1] == remaining) - - def test_model_iterable(self): - """Test that the function correctly unflattens when the model is a list of numbers, - which should result in unflatten_torch returning a list of tensors""" - model = [1] * 12 - unflattened = unflatten_torch(self.flat, model) - - assert all([i.shape == () for i in unflattened[0]]) - assert unflattened[1].numel() == 0 - - def test_model_nested_tensor(self): - """Test that the function correctly unflattens when the model is a nested tensor, - which should result in unflatten_torch returning a list of tensors of the same shape""" - model = [torch.ones(3), torch.ones((2, 2)), torch.ones((3, 1)), torch.ones((1, 2))] - unflattened = unflatten_torch(self.flat, model) - - assert all( - [u.shape == model[i].shape for i, u in enumerate(unflattened[0])] - ) - assert unflattened[1].numel() == 0 - - -class TestParameterHandlingIntegration: - """Test that the parameter handling for differentiable/non-differentiable - parameters works correctly.""" - - def test_differentiable_parameter_first(self): - """Test that a differentiable parameter used as the first - argument is correctly evaluated by QNode.jacobian, and that - all other non-differentiable parameters are ignored""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="torch") - def circuit(weights, data1, data2): - # non-differentiable quantum function - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - # differentiable quantum function - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - # non-differentiable quantum function - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # differentiating the circuit wrt the weights - # input weights - weights = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - weights = torch.tensor(weights, requires_grad=True) - - # input data - data1 = torch.tensor([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - data2 = torch.tensor([1, 1], requires_grad=False) - - loss = circuit(weights, data1, data2) - loss.backward() - - # check that weights is only once differentiable - assert weights.grad.requires_grad == False - - res = weights.grad.detach().numpy() - - # we do not check for correctness, just that the output - # is the correct shape - assert res.shape == weights.shape - - # check that the first arg was marked as non-differentiable - assert circuit.get_trainable_args() == {0} - - # Check that the gradient was not computed for the - # non-differentiable elements of `data1` and `data2`. - # First, extract the variable indices that the jacobian method - # 'skipped' (those with grad_method="0"): - non_diff_var_indices = sorted([k for k, v in circuit.par_to_grad_method.items() if v == "0"]) - - # Check that these indices corresponds to the elements of data1 and data2 - # within the flattenened list [weights, data1, data2] - assert non_diff_var_indices == [18, 19, 20, 21, 22, 23] - - def test_differentiable_parameter_middle(self): - """Test that a differentiable parameter provided as the middle - argument is correctly evaluated by QNode.jacobian, and that - all other non-differentiable parameters are ignored""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="torch") - def circuit(data1, weights, data2): - # non-differentiable quantum function - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - # differentiable quantum function - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - # non-differentiable quantum function - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # input weights - weights = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - weights = torch.tensor(weights, requires_grad=True) - - # input data - data1 = torch.tensor([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - data2 = torch.tensor([1, 1], requires_grad=False) - - loss = circuit(data1, weights, data2) - loss.backward() - res = weights.grad.detach().numpy() - - # we do not check for correctness, just that the output - # is the correct shape - assert res.shape == weights.shape - - # check that the second arg was marked as non-differentiable - assert circuit.get_trainable_args() == {1} - - # Check that the gradient was not computed for the - # non-differentiable elements of `data1` and `data2`. - # First, extract the variable indices that the jacobian method - # 'skipped' (those with grad_method="0"): - non_diff_var_indices = sorted([k for k, v in circuit.par_to_grad_method.items() if v == "0"]) - - # Check that these indices corresponds to the elements of data1 and data2 - # within the flattenened list [data1, weights, data2] - assert non_diff_var_indices == [0, 1, 2, 3, 22, 23] - - def test_differentiable_parameter_last(self): - """Test that a differentiable parameter used as the last - argument is correctly evaluated by QNode.jacobian, and that - all other non-differentiable parameters are ignored""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="torch") - def circuit(data1, data2, weights): - # non-differentiable quantum function - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - # differentiable quantum function - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - # non-differentiable quantum function - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # input weights - weights = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - weights = torch.tensor(weights, requires_grad=True) - - # input data - data1 = torch.tensor([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - data2 = torch.tensor([1, 1], requires_grad=False) - - loss = circuit(data1, data2, weights) - loss.backward() - res = weights.grad.detach().numpy() - - # we do not check for correctness, just that the output - # is the correct shape - assert res.shape == weights.shape - - # check that the last arg was marked as non-differentiable - assert circuit.get_trainable_args() == {2} - - # Check that the gradient was not computed for the - # non-differentiable elements of `data1` and `data2`. - # First, extract the variable indices that the jacobian method - # 'skipped' (those with grad_method="0"): - non_diff_var_indices = sorted([k for k, v in circuit.par_to_grad_method.items() if v == "0"]) - - # Check that these indices corresponds to the elements of data1 and data2 - # within the flattenened list [data1, data2, weights] - assert non_diff_var_indices == [0, 1, 2, 3, 4, 5] - - - def test_multiple_differentiable_and_non_differentiable_parameters(self): - """Test that multiple differentiable and non-differentiable parameters - works as expected""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="torch") - def circuit(data1, weights1, data2, weights2): - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - qml.templates.StronglyEntanglingLayers(weights1, wires=[0, 1]) - qml.templates.AngleEmbedding(data2, wires=[0, 1]) - qml.templates.StronglyEntanglingLayers(weights2, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - # input weights - weights1 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - weights2 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=4) - - weights1 = torch.tensor(weights1, requires_grad=True) - weights2 = torch.tensor(weights2, requires_grad=True) - - # input data - data1 = torch.tensor([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - data2 = torch.tensor([1, 1], requires_grad=False) - - loss = circuit(data1, weights1, data2, weights2) - loss.backward() - res1 = weights1.grad.detach().numpy() - res2 = weights2.grad.detach().numpy() - - # we do not check for correctness, just that the output - # is the correct shape - assert res1.shape == weights1.shape - assert res2.shape == weights2.shape - - # check that the parameter shift was only performed for the - # differentiable elements of `weights`, not the data input - assert circuit.get_trainable_args() == {1, 3} - - def test_gradient_non_differentiable_exception(self): - """Test that an exception is raised if non-differentiable data is - differentiated""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="torch") - def circuit(data1): - qml.templates.AmplitudeEmbedding(data1, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - grad_fn = qml.grad(circuit, argnum=[0]) - data1 = torch.tensor([0, 1, 1, 0], requires_grad=False) / np.sqrt(2) - - loss = circuit(data1) - assert circuit.get_trainable_args() == set() - - assert not loss.requires_grad - - with pytest.raises(RuntimeError, match="does not have a grad_fn"): - loss.backward() - - def test_chained_qnodes(self): - """Test that the gradient of chained QNodes works without error""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="torch") - def circuit1(weights): - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - @qml.qnode(dev, interface="torch") - def circuit2(data, weights): - qml.templates.AngleEmbedding(data, wires=[0, 1]) - qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1]) - return qml.expval(qml.PauliX(0)) - - def cost(weights): - w1, w2 = weights - c1 = circuit1(w1) - c2 = circuit2(c1, w2) - return torch.sum(c2) ** 2 - - w1 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=3) - w2 = qml.init.strong_ent_layers_normal(n_wires=2, n_layers=4) - - w1 = torch.tensor(w1, requires_grad=True) - w2 = torch.tensor(w2, requires_grad=True) - - weights = [w1, w2] - - loss = cost(weights) - loss.backward() - - res = w1.grad.detach().numpy() - assert res.shape == w1.shape - - res = w2.grad.detach().numpy() - assert res.shape == w2.shape - - def test_gradient_value(self, tol): - """Test that the returned gradient value for a qubit QNode is correct, - when one of the arguments is non-differentiable.""" - dev = qml.device("default.qubit", wires=3) - - @qml.qnode(dev, interface="torch") - def circuit(a, b, c): - qml.RX(a, wires=0) - qml.RX(b, wires=1) - qml.RX(c, wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - return qml.expval(qml.PauliX(0) @ qml.PauliY(2)) - - theta = torch.tensor(0.5, requires_grad=True) - phi = torch.tensor(0.1, requires_grad=True) - - # varphi is non-differentiable - varphi = torch.tensor(0.23) - - loss = circuit(theta, phi, varphi) - loss.backward() - - res = [i.grad.detach().numpy() for i in [theta, phi]] - expected = torch.tensor([ - torch.cos(theta) * torch.sin(phi) * torch.sin(varphi), - torch.sin(theta) * torch.cos(phi) * torch.sin(varphi) - ]) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - # check that the parameter-shift rule was not applied to varphi - assert circuit.get_trainable_args() == {0, 1} - - def test_chained_gradient_value(self, mocker, tol): - """Test that the returned gradient value for two chained qubit QNodes - is correct.""" - spy = mocker.spy(qml.qnodes.JacobianQNode, "jacobian") - dev1 = qml.device("default.qubit", wires=3) - - @qml.qnode(dev1, interface="torch") - def circuit1(a, b, c): - qml.RX(a, wires=0) - qml.RX(b, wires=1) - qml.RX(c, wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(2)) - - dev2 = qml.device("default.qubit", wires=2) - - @qml.qnode(dev2, interface="torch") - def circuit2(data, weights): - qml.RX(data[0], wires=0) - qml.RX(data[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.RZ(weights[0], wires=0) - qml.RZ(weights[1], wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliX(0) @ qml.PauliY(1)) - - def cost(a, b, c, weights): - return circuit2(circuit1(a, b, c), weights) - - # Set the first parameter of circuit1 as non-differentiable. - a = torch.tensor(0.4, requires_grad=False) - - # the remaining free parameters are all differentiable - b = torch.tensor(0.5, requires_grad=True) - c = torch.tensor(0.1, requires_grad=True) - weights = torch.tensor([0.2, 0.3], requires_grad=True) - - loss = cost(a, b, c, weights) - loss.backward() - res = [i.grad.detach().numpy() for i in [b, c, weights]] - - # Output should have shape [dcost/db, dcost/dc, dcost/dw], - # where b,c are scalars, and w is a vector of length 2. - assert len(res) == 3 - assert res[0].shape == tuple() # scalar - assert res[1].shape == tuple() # scalar - assert res[2].shape == (2,) # vector - - cacbsc = torch.cos(a)*torch.cos(b)*torch.sin(c) - - expected = torch.tensor([ - # analytic expression for dcost/db - -torch.cos(a)*torch.sin(b)*torch.sin(c)*torch.cos(cacbsc)*torch.sin(weights[0])*torch.sin(torch.cos(a)), - # analytic expression for dcost/dc - torch.cos(a)*torch.cos(b)*torch.cos(c)*torch.cos(cacbsc)*torch.sin(weights[0])*torch.sin(torch.cos(a)), - # analytic expression for dcost/dw[0] - torch.sin(cacbsc)*torch.cos(weights[0])*torch.sin(torch.cos(a)), - # analytic expression for dcost/dw[1] - 0 - ]) - - # np.hstack 'flattens' the ragged gradient array allowing it - # to be compared with the expected result - assert np.allclose(np.hstack(res), expected, atol=tol, rtol=0) - - # Check that the gradient was computed - # for all parameters in circuit2 - assert circuit2.get_trainable_args() == {0, 1} - - # check that the gradient was not computed - # for the first parameter of circuit1 - assert circuit1.get_trainable_args() == {1, 2} - - def test_non_diff_not_a_variable(self): - """Test that an argument marked as non-differentiable - is not wrapped as a variable.""" - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev, interface="torch") - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - - assert isinstance(x, qml.variable.Variable) - assert isinstance(y, float) - assert isinstance(z, qml.variable.Variable) - - return qml.expval(qml.PauliZ(0)) - - x = torch.tensor(1., requires_grad=True) - y = torch.tensor(2., requires_grad=False) - z = torch.tensor(3., requires_grad=True) - - res = circuit(x, y, z) - - assert circuit.get_trainable_args() == {0, 2} - - assert circuit.arg_vars[0] != x - assert circuit.arg_vars[1] == y - assert circuit.arg_vars[2] != z - - a = 0.6 - b = 0.2 - test_data = [ - ([0, 1], np.cos(2*a) * np.cos(b), [-2 * np.cos(b) * np.sin(2*a), -np.cos(2*a) * np.sin(b)]), - ([1, 0], -np.cos(b) * np.sin(b), [0, -np.cos(b) ** 2 + np.sin(b) ** 2]), - ] - - @pytest.mark.parametrize("w, expected_res, expected_grad", test_data) - def test_non_diff_wires_argument(self, w, expected_res, expected_grad, tol): - """Test that passing wires as a non-differentiable positional - argument works correctly.""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="torch") - def circuit(wires, params): - qml.Hadamard(wires=wires[0]) - qml.CNOT(wires=[wires[0], wires[1]]) - qml.RX(params[0], wires=wires[0]) - qml.RY(params[1], wires=wires[1]) - qml.CNOT(wires=[wires[1], wires[0]]) - qml.RX(params[0], wires=wires[0]) - qml.RY(params[1], wires=wires[1]) - return qml.expval(qml.PauliZ(0)) - - params = torch.tensor([0.6, 0.2], requires_grad=True) - wires = torch.tensor(w) - - res = circuit(wires, params) - - assert circuit.get_trainable_args() == {1} - assert np.allclose(res.detach(), expected_res, atol=tol, rtol=0) - - res.backward() - res_grad = params.grad - - assert circuit.get_trainable_args() == {1} - assert np.allclose(res_grad.detach(), expected_grad, atol=tol, rtol=0) - - def test_call_changing_trainability(self): - """Test that trainability properly changes between QNode calls""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="torch") - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - return qml.expval(qml.PauliZ(0)) - - x = torch.tensor(1., requires_grad=True) - y = torch.tensor(2., requires_grad=False) - z = torch.tensor(3., requires_grad=True) - - res = circuit(x, y, z) - - assert circuit.get_trainable_args() == {0, 2} - - x.requires_grad = False - y.requires_grad = True - - res = circuit(x, y, z) - - assert circuit.get_trainable_args() == {1, 2} - - def test_immutability(self): - """Test that changing parameter differentiability raises an exception - on immutable QNodes.""" - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev, interface="torch", mutable=False) - def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y, wires=0) - qml.RZ(z, wires=0) - return qml.expval(qml.PauliZ(0)) - - x = torch.tensor(1., requires_grad=True) - y = torch.tensor(2., requires_grad=False) - z = torch.tensor(3., requires_grad=True) - - res = circuit(x, y, z) - assert circuit.get_trainable_args() == {0, 2} - - # change values and compute the gradient again - res = circuit(2*x, -y, z) - assert circuit.get_trainable_args() == {0, 2} - - # attempting to change differentiability raises an error - x.requires_grad = False - y.requires_grad = True - - with pytest.raises(qml.QuantumFunctionError, match="cannot be modified"): - circuit(x, y, z) - - -class TestConversion: - """Integration tests to make sure that to_torch() correctly converts - QNodes with/without pre-existing interfaces""" - - @pytest.fixture - def qnode(self, interface, tf_support): - """Returns a simple QNode corresponding to cos(x), - with interface as determined by the interface fixture""" - if interface == "tf" and not tf_support: - pytest.skip("Skipped, no tf support") - - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev, interface=interface) - def circuit(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - return circuit - - @pytest.mark.parametrize("interface", ["torch"]) - def test_torch_conversion(self, qnode, tol): - """Tests that the to_torch() function ignores QNodes that already - have the torch interface.""" - converted_qnode = to_torch(qnode) - assert converted_qnode is qnode - - x_val = 0.4 - x = torch.tensor(x_val, requires_grad=True) - res = converted_qnode(x) - res.backward() - - assert np.allclose(res.detach().numpy(), np.cos(x_val), atol=tol, rtol=0) - assert np.allclose(x.grad, -np.sin(x_val), atol=tol, rtol=0) - - @pytest.mark.parametrize("interface", [None, "autograd", "tf"]) - def test_other_conversion(self, qnode, tol): - """Tests that the to_torch() function correctly converts both tf and autograd qnodes and - QNodes with no interface.""" - converted_qnode = to_torch(qnode) - assert converted_qnode is not qnode - assert converted_qnode._qnode is getattr(qnode, "_qnode", qnode) - - x_val = 0.4 - x = torch.tensor(x_val, requires_grad=True) - res = converted_qnode(x) - res.backward() - - assert np.allclose(res.detach().numpy(), np.cos(x_val), atol=tol, rtol=0) - assert np.allclose(x.grad, -np.sin(x_val), atol=tol, rtol=0) diff --git a/tests/math/test_functions.py b/tests/math/test_functions.py index 8480089b139..d4ef13d55d8 100644 --- a/tests/math/test_functions.py +++ b/tests/math/test_functions.py @@ -1115,7 +1115,7 @@ def expected_grad(weights): -np.cos(a) * np.cos(c) * np.sin(b) ** 2 ]) - def test_weird_wires(self, in_tape_mode, tol): + def test_weird_wires(self, tol): """Test that the covariance matrix computes the correct result when weird wires are used""" dev = qml.device("default.qubit", wires=["a", -1, "q"]) @@ -1146,7 +1146,7 @@ def cov(weights): expected = self.expected_grad(weights) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_autograd(self, in_tape_mode, tol): + def test_autograd(self, tol): """Test that the covariance matrix computes the correct result, and is differentiable, using the Autograd interface""" dev = qml.device("default.qubit", wires=3) @@ -1176,7 +1176,7 @@ def cov(weights): expected = self.expected_grad(weights) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_torch(self, in_tape_mode, tol): + def test_torch(self, tol): """Test that the covariance matrix computes the correct result, and is differentiable, using the Torch interface""" dev = qml.device("default.qubit", wires=3) @@ -1205,7 +1205,7 @@ def circuit(weights): expected = self.expected_grad(weights) assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) - def test_tf(self, in_tape_mode, tol): + def test_tf(self, tol): """Test that the covariance matrix computes the correct result, and is differentiable, using the TF interface""" dev = qml.device("default.qubit", wires=3) @@ -1236,7 +1236,7 @@ def circuit(weights): expected = self.expected_grad(weights) assert np.allclose(grad, expected, atol=tol, rtol=0) - def test_jax(self, in_tape_mode, tol): + def test_jax(self, tol): """Test that the covariance matrix computes the correct result, and is differentiable, using the JAX interface""" dev = qml.device("default.qubit.jax", wires=3) diff --git a/tests/ops/test_channel_ops.py b/tests/ops/test_channel_ops.py index cd5bc5c9131..40fd2787c6c 100644 --- a/tests/ops/test_channel_ops.py +++ b/tests/ops/test_channel_ops.py @@ -20,7 +20,6 @@ import pennylane as qml from pennylane.ops import channel from pennylane.wires import Wires -pytestmark = pytest.mark.usefixtures("tape_mode") X = np.array([[0, 1], [1, 0]]) Y = np.array([[0, -1j], [1j, 0]]) diff --git a/tests/ops/test_qubit_ops.py b/tests/ops/test_qubit_ops.py index 49cd9f3c47f..e7f267d1cca 100644 --- a/tests/ops/test_qubit_ops.py +++ b/tests/ops/test_qubit_ops.py @@ -499,14 +499,10 @@ def test_CY_decomposition(self, tol): assert np.allclose(decomposed_matrix, op.matrix, atol=tol, rtol=0) @pytest.mark.parametrize("phi, theta, omega", [[0.5, 0.6, 0.7], [0.1, -0.4, 0.7], [-10, 5, -1]]) - @pytest.mark.parametrize("tapemode", [True, False]) - def test_CRot_decomposition(self, tol, phi, theta, omega, tapemode, monkeypatch): + def test_CRot_decomposition(self, tol, phi, theta, omega, monkeypatch): """Tests that the decomposition of the CRot gate is correct""" op = qml.CRot(phi, theta, omega, wires=[0, 1]) - - with monkeypatch.context() as m: - m.setattr(qml, "tape_mode_active", lambda: tapemode) - res = op.decomposition(phi, theta, omega, op.wires) + res = op.decomposition(phi, theta, omega, op.wires) mats = [] for i in reversed(res): diff --git a/tests/qnn/conftest.py b/tests/qnn/conftest.py index 89a20d2842c..96337c2272b 100644 --- a/tests/qnn/conftest.py +++ b/tests/qnn/conftest.py @@ -19,7 +19,7 @@ @pytest.fixture -def get_circuit(n_qubits, output_dim, interface, tape_mode): +def get_circuit(n_qubits, output_dim, interface): """Fixture for getting a sample quantum circuit with a controllable qubit number and output dimension. Returns both the circuit and the shape of the weights.""" diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index e3e3470689c..504ab79e5ea 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -22,8 +22,6 @@ tf = pytest.importorskip("tensorflow", minversion="2") -pytestmark = pytest.mark.usefixtures("tape_mode") - @pytest.mark.usefixtures("get_circuit") @pytest.fixture @@ -122,35 +120,8 @@ def circuit(inputs, w1, w2, *args): KerasLayer(circuit, weight_shapes, output_dim=1) @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) - def test_var_keyword(self, get_circuit, monkeypatch, output_dim): - """Test if a TypeError is raised when instantiated with a variable number of keyword - arguments""" - if qml.tape_mode_active(): - pytest.skip( - "This functionality is supported in tape mode, so will not raise an exception." - ) - - c, w = get_circuit - - class FuncPatch: - """Patch for variable number of keyword arguments""" - - sig = c.func.sig - var_pos = False - var_keyword = True - - with monkeypatch.context() as m: - m.setattr(c, "func", FuncPatch) - - with pytest.raises(TypeError, match="Cannot have a variable number of keyword"): - KerasLayer(c, w, output_dim) - - @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) - def test_var_keyword_tape_mode(self): - """Test that variable number of keyword arguments works in tape mode""" - if not qml.tape_mode_active(): - pytest.skip("This functionality is only supported in tape mode.") - + def test_var_keywordself): + """Test that variable number of keyword arguments works""" n_qubits = 2 output_dim = 2 @@ -217,33 +188,9 @@ def test_weight_shapes(self, get_circuit, output_dim, n_qubits): } @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) - def test_non_input_defaults(self, get_circuit, output_dim, n_qubits): - """Test if a TypeError is raised when default arguments that are not the input argument are - present in the QNode""" - if qml.tape_mode_active(): - pytest.skip( - "This functionality is supported in tape mode, so will not raise an exception." - ) - - c, w = get_circuit - - @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf") - def c_dummy(inputs, w1, w2, w3, w4, w5, w6, w7, w8=None): - """Dummy version of the circuit with a default argument""" - return c(inputs, w1, w2, w3, w4, w5, w6, w7) - - with pytest.raises( - TypeError, - match="Only the argument {} is permitted".format(qml.qnn.keras.KerasLayer._input_arg), - ): - KerasLayer(c_dummy, {**w, **{"w8": 1}}, output_dim) - - @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) - def test_non_input_defaults_tape_mode(self): + def test_non_input_defaults(self): """Test that everything works when default arguments that are not the input argument are - present in the QNode in tape mode""" - if not qml.tape_mode_active(): - pytest.skip("This functionality is only supported in tape mode.") + present in the QNode""" n_qubits = 2 output_dim = 2 @@ -455,8 +402,6 @@ def test_gradients(self, get_circuit, output_dim, n_qubits): @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) def test_backprop_gradients(self, mocker): """Test if KerasLayer is compatible with the backprop diff method.""" - if not qml.tape_mode_active(): - pytest.skip("This functionality is only supported in tape mode.") dev = qml.device("default.qubit.tf", wires=2) diff --git a/tests/qnn/test_qnn_torch.py b/tests/qnn/test_qnn_torch.py index a98493ceb0f..97c04277c56 100644 --- a/tests/qnn/test_qnn_torch.py +++ b/tests/qnn/test_qnn_torch.py @@ -24,8 +24,6 @@ torch = pytest.importorskip("torch") -pytestmark = pytest.mark.usefixtures("tape_mode") - def indices_up_to(n_max): """Returns an iterator over the number of qubits and output dimension, up to value n_max. @@ -128,33 +126,8 @@ def circuit(inputs, w1, w2, *args): TorchLayer(circuit, weight_shapes) @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) - def test_var_keyword(self, get_circuit, monkeypatch): - """Test if a TypeError is raised when instantiated with a variable number of keyword - arguments""" - if qml.tape_mode_active(): - pytest.skip("This functionality is supported in tape mode.") - - c, w = get_circuit - - class FuncPatch: - """Patch for variable number of keyword arguments""" - - sig = c.func.sig - var_pos = False - var_keyword = True - - with monkeypatch.context() as m: - m.setattr(c, "func", FuncPatch) - - with pytest.raises(TypeError, match="Cannot have a variable number of keyword"): - TorchLayer(c, w) - - @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) - def test_var_keyword_tape_mode(self, n_qubits, output_dim): - """Test that variable number of keyword arguments works in tape mode""" - if not qml.tape_mode_active(): - pytest.skip("This functionality is only supported in tape mode.") - + def test_var_keyword(self, n_qubits, output_dim): + """Test that variable number of keyword arguments works""" dev = qml.device("default.qubit", wires=n_qubits) w = { "w1": (3, n_qubits, 3), @@ -202,32 +175,9 @@ def test_nonspecified_init(self, get_circuit, n_qubits, monkeypatch): assert kwargs["b"] == 2 * math.pi @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) - def test_non_input_defaults(self, get_circuit, n_qubits): - """Test if a TypeError is raised when default arguments that are not the input argument are - present in the QNode""" - if qml.tape_mode_active(): - pytest.skip("This functionality is supported in tape mode.") - - c, w = get_circuit - - @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="torch") - def c_dummy(inputs, w1, w2, w3, w4, w5, w6, w7, w8=None): - """Dummy version of the circuit with a default argument""" - return c(inputs, w1, w2, w3, w4, w5, w6, w7) - - with pytest.raises( - TypeError, - match="Only the argument {} is permitted".format(qml.qnn.torch.TorchLayer._input_arg), - ): - TorchLayer(c_dummy, {**w, **{"w8": 1}}) - - @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) - def test_non_input_defaults_tape_mode(self, n_qubits, output_dim): + def test_non_input_defaults(self, n_qubits, output_dim): """Test that everything works when default arguments that are not the input argument are - present in the QNode in tape mode""" - if not qml.tape_mode_active(): - pytest.skip("This functionality is only supported in tape mode.") - + present in the QNode""" dev = qml.device("default.qubit", wires=n_qubits) w = { "w1": (3, n_qubits, 3), diff --git a/tests/qnodes/test_qnode_base.py b/tests/qnodes/test_qnode_base.py deleted file mode 100644 index d02c5e04cf0..00000000000 --- a/tests/qnodes/test_qnode_base.py +++ /dev/null @@ -1,1520 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane` :class:`QNode` class. -""" -import contextlib -import io -import textwrap - -import pytest -import numpy as np - -import pennylane as qml -from pennylane._device import Device -from pennylane.qnodes.base import BaseQNode, QuantumFunctionError, decompose_queue -from pennylane.variable import Variable -from pennylane.wires import Wires, WireError -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - -@pytest.fixture(scope="function") -def mock_qnode(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 = BaseQNode(circuit, mock_device) - node._construct([1.0], {}) - return node - - -@pytest.fixture(scope="function") -def operable_mock_device_2_wires(monkeypatch): - """A mock instance of the abstract Device class that can support Qubit qfuncs.""" - - dev = Device - with monkeypatch.context() as m: - m.setattr(dev, "__abstractmethods__", frozenset()) - m.setattr(dev, "capabilities", lambda cls: {"model": "qubit"}) - 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_device_2_wires_with_inverses(monkeypatch): - """A mock instance of the abstract Device class that can support Qubit qfuncs.""" - - dev = Device - with monkeypatch.context() as m: - m.setattr(dev, "__abstractmethods__", frozenset()) - m.setattr(dev, "capabilities", lambda cls: {"model": "qubit", "supports_inverse_operations": True}) - m.setattr(dev, "operations", ["BasisState", "RX", "RY", "RZ", "CNOT", "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 CV 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 TestQNodeOperationQueue: - """Tests that the QNode operation queue is properly filled and interacted with""" - - def test_operation_ordering(self, mock_qnode): - """Tests that the ordering of the operations is correct""" - - qnode = mock_qnode - 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_descendants_operations_only(self, mock_qnode): - """Tests that _op_descendants properly extracts the successors that are operations""" - - qnode = mock_qnode - operation_successors = qnode._op_descendants(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_descendants_observables_only(self, mock_qnode): - """Tests that _op_descendants properly extracts the successors that are observables""" - - qnode = mock_qnode - observable_successors = qnode._op_descendants(qnode.ops[0], only="O") - 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_descendants_both_operations_and_observables(self, mock_qnode): - """Tests that _op_descendants properly extracts all successors""" - - qnode = mock_qnode - successors = qnode._op_descendants(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_descendants_both_operations_and_observables_nodes(self, mock_qnode): - """Tests that _op_descendants properly extracts all successor nodes""" - - qnode = mock_qnode - successors = qnode._op_descendants(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_descendants_both_operations_and_observables_strict_ordering(self, mock_qnode): - """Tests that _op_descendants properly extracts all successors""" - - qnode = mock_qnode - successors = qnode._op_descendants(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_descendants_extracts_all_successors(self, mock_qnode): - """Tests that _op_descendants properly extracts all successors""" - - qnode = mock_qnode - successors = qnode._op_descendants(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(array([[0, 1], - [1, 0]]), wires=[1]))""" - ) - - node = BaseQNode(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._set_variables([0.1], {}) - 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) - - def test_print_applied_with_probs(self, mock_device): - """Test that printing applied gates works correctly when probs are returned""" - - H = np.array([[0, 1], [1, 0]]) - - def circuit(x): - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.SWAP(wires=[1, 0]) - qml.RZ(-0.2, wires=[1]) - return qml.probs(wires=[0]), qml.var(qml.Hermitian(H, wires=1)) - - expected_qnode_print = textwrap.dedent( - """\ - Operations - ========== - RX({x}, wires=[0]) - CNOT(wires=[0, 1]) - SWAP(wires=[1, 0]) - RZ(-0.2, wires=[1]) - - Observables - =========== - probs(wires=[0]) - var(Hermitian(array([[0, 1], - [1, 0]]), wires=[1]))""" - ) - - node = BaseQNode(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._set_variables([0.1], {}) - 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) - - def test_operation_appending(self, mock_device): - """Tests that operations are correctly appended.""" - CNOT = qml.CNOT(wires=[0, 1]) - - def circuit(x): - qml.QueuingContext.append(CNOT) - qml.RY(0.4, wires=[0]) - qml.RZ(-0.2, wires=[1]) - - return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1)) - - qnode = BaseQNode(circuit, mock_device) - qnode._construct([1.0], {}) - - assert qnode.ops[0].name == "CNOT" - assert qnode.ops[1].name == "RY" - assert qnode.ops[2].name == "RZ" - assert qnode.ops[3].name == "PauliX" - - def test_operation_removal(self, mock_device): - """Tests that operations are correctly removed.""" - - def circuit(x): - RX = qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 1]) - qml.RY(0.4, wires=[0]) - qml.RZ(-0.2, wires=[1]) - - qml.QueuingContext.remove(RX) - - return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1)) - - qnode = BaseQNode(circuit, mock_device) - qnode._construct([1.0], {}) - - assert qnode.ops[0].name == "CNOT" - assert qnode.ops[1].name == "RY" - assert qnode.ops[2].name == "RZ" - assert qnode.ops[3].name == "PauliX" - - def test_prune_tensors(self, mock_device): - """Test that the _prune_tensors auxiliary method prunes correct for - a single Identity in the Tensor.""" - px = qml.PauliX(1) - obs = qml.Identity(0) @ px - - def circuit(x): - return qml.expval(obs) - - qnode = BaseQNode(circuit, mock_device) - - assert qnode._prune_tensors(obs) == px - - def test_prune_tensors_no_pruning_took_place(self, mock_device): - """Test that the _prune_tensors auxiliary method returns - the original tensor if no observables were pruned.""" - px = qml.PauliX(1) - obs = px - - def circuit(x): - return qml.expval(obs) - - qnode = BaseQNode(circuit, mock_device) - - assert qnode._prune_tensors(obs) == px - - def test_prune_tensors_construct(self, mock_device): - """Test that the tensors are pruned in construct.""" - - def circuit(x): - return qml.expval(qml.PauliX(0) @ qml.Identity(1)) - - qnode = BaseQNode(circuit, mock_device) - qnode._construct([1.0], {}) - - assert qnode.ops[0].name == "PauliX" - assert len(qnode.ops[0].wires) == 1 - assert qnode.ops[0].wires[0] == 0 - - -class TestQNodeExceptions: - """Tests that QNode raises proper errors""" - - def test_operations_after_observables(self, operable_mock_device_2_wires): - """Error: 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 = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises(QuantumFunctionError, match="gates must precede measured"): - node(0.5) - - def test_return_of_non_observable(self, operable_mock_device_2_wires): - """Error: qfunc returns something besides observables.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(wires=0)), 0.3 - - node = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises(QuantumFunctionError, match="A quantum function must return either"): - node(0.5) - - def test_observable_with_no_measurement_type(self, operable_mock_device_2_wires): - """Error: observable lacks the measurement type.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(wires=0)), qml.PauliZ(wires=1) - - node = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises( - QuantumFunctionError, match="does not have the measurement type specified" - ): - node(0.5) - - def test_observable_not_returned(self, operable_mock_device_2_wires): - """Error: 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 = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises(QuantumFunctionError, match="All measured observables must be returned"): - node(0.5) - - def test_observable_order_violated(self, operable_mock_device_2_wires): - """Error: 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 = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises(QuantumFunctionError, match="All measured observables must be returned"): - node(0.5) - - def test_mixing_of_cv_and_qubit_operations(self, operable_mock_device_2_wires): - """Error: 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 = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises( - QuantumFunctionError, match="Continuous and discrete operations are not allowed" - ): - node(0.5) - - def test_cv_operations_on_qubit_device(self, operable_mock_device_2_wires): - """Error: cannot use CV operations on a qubit device.""" - - def circuit(x): - qml.Displacement(0.5, 0, wires=[0]) - return qml.expval(qml.X(0)) - - node = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises( - QuantumFunctionError, match="a qubit device; CV operations are not allowed" - ): - node(0.5) - - def test_qubit_operations_on_CV_device(self, operable_mock_device_2_wires, monkeypatch): - """Error: cannot use qubit operations on a CV device.""" - monkeypatch.setattr(operable_mock_device_2_wires, "capabilities", lambda: {"model": "cv"}) - - def circuit(x): - qml.RX(0.5, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises( - QuantumFunctionError, match="a CV device; qubit operations are not allowed" - ): - node(0.5) - - def test_multiple_measurements_on_same_wire(self, operable_mock_device_2_wires): - """Error: 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 = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises(QuantumFunctionError, match="can only be measured once"): - node(0.5) - - def test_invisible_operations(self, operable_mock_device_2_wires): - """Error: an operation does not affect the measurements.""" - - def circuit(x): - qml.RX(x, wires=[0]) - qml.RX(x, wires=[1]) # on its own component in the circuit graph - return qml.expval(qml.PauliZ(0)) - - kwargs = {"vis_check": True} - node = BaseQNode(circuit, operable_mock_device_2_wires, **kwargs) - with pytest.raises(QuantumFunctionError, match="cannot affect the circuit output"): - node(0.5) - - def test_operation_requiring_all_wires(self, operable_mock_device_2_wires): - """Error: an operation that must be applied to all wires is not - applied to all wires.""" - - class DummyOp(qml.operation.Operation): - """Dummy operation""" - - num_wires = qml.operation.WiresEnum.AllWires - num_params = 0 - par_domain = None - - def circuit(): - DummyOp(wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises(QuantumFunctionError, match="must act on all wires"): - node() - - def test_operation_on_nonexistant_wire(self, operable_mock_device_2_wires): - """Error: an operation is applied to a non-existant wire.""" - - def circuit(x): - qml.RX(x, wires=[0]) - qml.CNOT(wires=[0, 2]) - return qml.expval(qml.PauliZ(0)) - - node = BaseQNode(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): - """Error: an observable is measured on a non-existant wire.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(2)) - - node = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises(QuantumFunctionError, match="applied to invalid wire"): - node(0.5) - - def test_arg_as_wire_argument(self, operable_mock_device_2_wires): - """Error: trying to use a differentiable parameter as a wire argument.""" - - def circuit(x): - qml.RX(0.5, wires=[x]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(2)) - - node = BaseQNode(circuit, operable_mock_device_2_wires) - with pytest.raises(WireError, match="Wires must be hashable"): - node(1) - - def test_kwarg_as_wire_argument(self, operable_mock_device_2_wires): - """Error: trying to use a keyword-only parameter as a wire argument in an immutable circuit.""" - - def circuit(*, x=None): - qml.RX(0.5, wires=[x]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - node = BaseQNode(circuit, operable_mock_device_2_wires, mutable=False) - with pytest.raises(WireError, match="Wires must be hashable"): - node(x=1) - - @pytest.mark.xfail( - reason="Tests the auxiliary-equals-keyword-only syntax", raises=TypeError, strict=True - ) - def test_simple_valid_call(self, operable_mock_device_2_wires): - """BaseQNode gives an error here, "got multiple values for argument 'x'" - """ - - def circuit(x=0): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = BaseQNode(circuit, operable_mock_device_2_wires) - node(0.3) - assert node.ops[0].parameters[0] == 0.3 - - @pytest.mark.xfail( - reason="Tests the auxiliary-equals-keyword-only syntax", raises=AssertionError, strict=True - ) - def test_calling_no_kwargs(self, operable_mock_device_2_wires): - """Various quantum func calling syntax errors.""" - - def circuit(x, y=0.2, *args, m=0.3, n): - circuit.in_args = (x, y, m, n) - return qml.expval(qml.PauliZ(0)) - - node = BaseQNode(circuit, operable_mock_device_2_wires, mutable=True) - - with pytest.raises(QuantumFunctionError, match="parameter 'x' given twice"): - node(0.1, x=1.1) - with pytest.raises(QuantumFunctionError, match="Unknown quantum function parameter 'foo'"): - node(foo=1) - with pytest.raises( - QuantumFunctionError, match="'args' cannot be given using the keyword syntax" - ): - node(args=1) - with pytest.raises(QuantumFunctionError, match="positional parameter 'x' missing"): - node(n=0.4) - with pytest.raises(QuantumFunctionError, match="keyword-only parameter 'n' missing"): - node(0.1) - - # valid calls - node(x=0.1, n=0.4) - assert circuit.in_args[2:] == (0.3, 0.4) # first two are Variables - node(0.1, n=0.4) - assert circuit.in_args[2:] == (0.3, 0.4) - - def test_unused_positional_parameter(self, operable_mock_device_2_wires): - """Error: a positional parameter is not used in the circuit.""" - - def circuit(a, x): - qml.RX(a, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - kwargs = {"par_check": True} - node = BaseQNode(circuit, operable_mock_device_2_wires, **kwargs) - with pytest.raises(QuantumFunctionError, match="The positional parameters"): - node(1.0, 2.0) - - @pytest.mark.xfail( - reason="Tests the auxiliary-equals-keyword-only syntax", raises=AssertionError, strict=True - ) - def test_calling_with_kwargs(self, operable_mock_device_2_wires): - """Various quantum func calling syntax errors.""" - - def circuit(x, y=0.2, *, m=0.3, n, **kwargs): - circuit.in_args = (x, y, m, n) - return qml.expval(qml.PauliZ(0)) - - node = BaseQNode(circuit, operable_mock_device_2_wires, mutable=True) - - with pytest.raises(QuantumFunctionError, match="parameter 'x' given twice"): - node(0.1, x=1.1) - with pytest.raises( - QuantumFunctionError, match="'kwargs' cannot be given using the keyword syntax" - ): - node(kwargs=1) - with pytest.raises(QuantumFunctionError, match="takes 2 positional parameters, 3 given"): - node(0.1, 0.2, 100, n=0.4) - with pytest.raises(QuantumFunctionError, match="positional parameter 'x' missing"): - node(n=0.4) - with pytest.raises(QuantumFunctionError, match="keyword-only parameter 'n' missing"): - node(0.1) - - # valid calls - node(x=0.1, n=0.4) - assert circuit.in_args[2:] == (0.3, 0.4) # first two are Variables - node(0.1, n=0.4) - assert circuit.in_args[2:] == (0.3, 0.4) - - def test_calling_bad_errors(self, operable_mock_device_2_wires): - """Confusing quantum func calling errors and bugs (auxiliary-equals-parameters-with-default syntax).""" - - def circuit(x=0.1): - return qml.expval(qml.PauliZ(0)) - - node = BaseQNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(TypeError, match="got multiple values for argument 'x'"): - node(0.3) # default arg given positionally, wrong error message - - def test_calling_errors(self, operable_mock_device_2_wires): - """Good quantum func calling syntax errors (auxiliary-equals-parameters-with-default syntax).""" - - def circuit(x, y=0.2, *args, z=0.3): - circuit.in_args = (x, y, z) - return qml.expval(qml.PauliZ(0)) - - node = BaseQNode(circuit, operable_mock_device_2_wires, mutable=True) - - with pytest.raises( - QuantumFunctionError, match="'x' cannot be given using the keyword syntax" - ): - node(0.1, x=1.1) - with pytest.raises(QuantumFunctionError, match="Unknown quantum function parameter 'foo'"): - node(foo=1) - with pytest.raises( - QuantumFunctionError, match="'args' cannot be given using the keyword syntax" - ): - node(args=1) - with pytest.raises(TypeError, match="missing 1 required positional argument: 'x'"): - node(z=0.4) - - # valid calls - node(0.1) - assert circuit.in_args[1:] == (0.2, 0.3) # first is a Variable - node(0.1, y=1.2) - assert circuit.in_args[1:] == (1.2, 0.3) - node(0.1, z=1.3, y=1.2) - assert circuit.in_args[1:] == (1.2, 1.3) - - -class TestQNodeArgs: - """Tests the handling of calling arguments 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 np.cos(x) ** 2 - np.cos(y) * np.sin(x) ** 2 - - node = BaseQNode(circuit, qubit_device_1_wire) - res = node(x, y) - assert res == pytest.approx(analytic_expval(x, y), abs=tol) - - 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 - - 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 * np.cos(a) * np.cos(b) * np.sin(c), np.cos(a)] - - node = BaseQNode(circuit, qubit_device_2_wires) - res = node(a, b, c) - assert res == pytest.approx(analytic_expval(a, b, c), abs=tol) - - def test_multiple_keywordargs_used(self, qubit_device_2_wires, tol): - """Tests that qnodes can use multiple keyword-only arguments.""" - - def circuit(w, *, x=None, y=None): - qml.RX(x, wires=[0]) - qml.RX(y, wires=[1]) - qml.RZ(w, wires=[0]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - node = BaseQNode(circuit, qubit_device_2_wires) - c = node(1.0, x=np.pi, y=np.pi / 2) - assert c == pytest.approx([-1.0, 0.0], abs=tol) - - def test_arraylike_args_used(self, qubit_device_2_wires, tol): - """Tests that qnodes use array-like positional arguments.""" - - def circuit(x): - qml.RX(x[0], wires=[0]) - qml.RX(x[1], wires=[1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - node = BaseQNode(circuit, qubit_device_2_wires) - c = node([np.pi, np.pi]) - assert c == pytest.approx([-1.0, -1.0], abs=tol) - - def test_arraylike_keywordargs_used(self, qubit_device_2_wires, tol): - """Tests that qnodes use array-like keyword-only arguments.""" - - def circuit(w, *, x=None): - qml.RX(x[0], wires=[0]) - qml.RX(x[1], wires=[1]) - qml.RZ(w, wires=[0]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - node = BaseQNode(circuit, qubit_device_2_wires) - c = node(1.0, x=[np.pi, np.pi / 2]) - assert c == pytest.approx([-1.0, 0.0], abs=tol) - - def test_keywordargs_for_wires(self, qubit_device_2_wires, tol): - """Tests that wires can be passed as keyword-only arguments in mutable circuits.""" - - default_q = 0 - - def circuit(x, *, q=default_q): - qml.RX(x, wires=[q]) - return qml.expval(qml.PauliZ(q)) - - node = BaseQNode(circuit, qubit_device_2_wires) - c = node(np.pi, q=1) - assert node.ops[0].wires == Wires([1]) - assert c == pytest.approx(-1.0, abs=tol) - - c = node(np.pi) - assert node.ops[0].wires == Wires([default_q]) - assert c == pytest.approx(-1.0, abs=tol) - - 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 = BaseQNode(circuit, qubit_device_1_wire) - c = node(1.0, x=np.pi) - assert c == pytest.approx(-1.0, abs=tol) - - 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 = BaseQNode(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 = BaseQNode(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 c == pytest.approx([1.0, -1.0], abs=tol) - - def test_keywordargs_with_kwargs(self, qubit_device_1_wire, tol): - """Tests that nothing happens if unknown keyword arg passed with - qnodes accepting **kwargs.""" - - def circuit(w, x=None, **kwargs): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = BaseQNode(circuit, qubit_device_1_wire) - c = node(1.0, x=np.pi, y=10) - assert c == pytest.approx(-1.0, abs=tol) - - def test_complex_positional_argument_qubitunitary(self, tol): - """Tests that matrices containing complex positional arguments can be - passed to the QubitUnitary operation.""" - - dev = qml.device('default.qubit', wires=1) - - @qml.qnode(dev) - def circuit(phi, matrix): - qml.RZ(phi, wires=0) - qml.PauliY(0) - qml.QubitUnitary(matrix, wires=0) - return qml.expval(qml.PauliZ(0)) - - matrix = np.array([[1, 0], [0, 0.70710678 + 0.70710678*1.j]]) - arg = 0 - res = circuit(arg, matrix) - assert np.isclose(res, -1, atol=tol) - - -class TestQNodeCaching: - """Tests for the QNode construction caching""" - - def test_no_caching(self): - """Test that mutable circuit structure changes on subsequent evalutions.""" - - dev = qml.device("default.qubit", wires=2) - - def mutable_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)) - - node = BaseQNode(mutable_circuit, dev, mutable=True) - - # first evaluation - node(0, c=0) - assert len(node.circuit.operations) == 1 - temp = node.ops[0] - - # second evaluation - node(0, c=1) - assert len(node.circuit.operations) == 2 - node.ops[0] is not temp # all Operations in the circuit are generated anew - - def test_caching(self): - """Test that non-mutable circuit structure does not change on subsequent evalutions.""" - - dev = qml.device("default.qubit", wires=2) - - def non_mutable_circuit(x, *, c=None): - qml.RX(x, wires=0) - qml.RX(c, wires=0) - return qml.expval(qml.PauliZ(0)) - - node = BaseQNode(non_mutable_circuit, dev, mutable=False) - - # first evaluation - node(0, c=0) - assert len(node.circuit.operations) == 2 - temp = node.ops[0] - - # second evaluation - node(0, c=1) - assert len(node.circuit.operations) == 2 - node.ops[0] is temp # it's the same circuit with the same objects - - THETA = np.linspace(0.11, 1, 3) - PHI = np.linspace(0.32, 1, 3) - VARPHI = np.linspace(0.02, 1, 3) - - @pytest.mark.parametrize("theta,phi,varphi", list(zip(THETA, PHI, VARPHI))) - def test_mutable_qnode(self, theta, phi, varphi, tol): - """Test that a mutable QNode evaluated multiple times mutates well and produces - the desired result. - """ - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev) - def circuit(weights, n_layers=1): - for idx in range(n_layers): - qml.RX(weights[idx], wires=[0]) - return qml.expval(qml.PauliZ(0)) - - res = circuit([phi], n_layers=1) - exp = np.cos(phi) - assert np.allclose(res, exp, atol=tol, rtol=0) - - res = circuit([phi, theta], n_layers=2) - exp = np.cos(phi + theta) - assert np.allclose(res, exp, atol=tol, rtol=0) - - res = circuit([phi, theta, varphi], n_layers=3) - exp = np.cos(phi + theta + varphi) - assert np.allclose(res, exp, atol=tol, rtol=0) - - def test_mutable_qnode_for_loop_varying_executions(self, tol): - """Test that a mutable QNode containing a for loop correctly mutates - when called with different auxiliary arguments and different shaped positional - arguments. - """ - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev) - def node(x, n=1): - for k in range(2): - for j in range(min(n, k + 1)): - qml.RX(x[k][j], wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - res = node([[0.1], [0.2]], n=1) - exp = np.cos(sum([0.1] + [0.2])) - assert np.allclose(res, exp, atol=tol, rtol=0) - - res = node([[0.1], [0.2, 0.3]], n=2) - exp = np.cos(sum([0.1] + [0.2, 0.3])) - assert np.allclose(res, exp, atol=tol, rtol=0) - - -class TestQNodeEvaluate: - """Test for observable statistic evaluation""" - - @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_evaluate(self, x, y, tol): - """Tests correct evaluation""" - dev = qml.device("default.qubit", wires=2) - - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - node = BaseQNode(circuit, dev) - res = node.evaluate([x, y], {}) - expected = np.sin(y) * np.cos(x) - assert np.allclose(res, expected, atol=tol, rtol=0) - - @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_obs_evaluate(self, x, y, tol): - """Tests correct evaluation swapping out the observables""" - dev = qml.device("default.qubit", wires=2) - - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - node = BaseQNode(circuit, dev) - - # test standard evaluation - node = BaseQNode(circuit, dev) - res = node.evaluate([x, y], {}) - expected = np.sin(y) * np.cos(x) - assert np.allclose(res, expected, atol=tol, rtol=0) - - # hot-swap the observable - res = node.evaluate_obs([qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))], [x, y], {}) - expected = np.cos(y) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_single_mode_sample(self): - """Test that there is only one array of values returned - for single mode samples""" - shots = 10 - dev = qml.device("default.qubit", wires=2, shots=shots) - - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.sample(qml.PauliZ(0) @ qml.PauliX(1)) - - node = BaseQNode(circuit, dev) - res = node(0.432, 0.12) - assert res.shape == (10,) - - -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_decompose_queue_recursive(self, operable_mock_device_2_wires_with_inverses): - """Test that decompose queue works correctly - when an operation exists that can be decomposed""" - - queue = [qml.CRY(1, wires=[0, 1]), qml.U3(3, 4, 5, wires=0)] - - res = decompose_queue(queue, operable_mock_device_2_wires_with_inverses) - - assert len(res) == 9 - - assert res[0].name == "RY" - assert res[0].parameters == [0.5] - - assert res[1].name == "CNOT" - - assert res[2].name == "RY" - assert res[2].parameters == [-0.5] - - assert res[3].name == "CNOT" - - assert res[4].name == "RZ" - assert res[4].parameters == [5] - - assert res[5].name == "RY" - assert res[5].parameters == [3] - - assert res[6].name == "RZ" - assert res[6].parameters == [-5] - - assert res[7].name == "PhaseShift" - assert res[7].parameters == [5] - - assert res[8].name == "PhaseShift" - assert res[8].parameters == [4] - - def test_decompose_queue_inv(self, operable_mock_device_2_wires_with_inverses): - """Test that decompose queue works correctly - when an operation exists that can be decomposed""" - - queue = [ - qml.Rot(0, 1, 2, wires=0).inv(), - qml.U3(3, 4, 5, wires=0).inv(), - qml.RX(6, wires=0).inv(), - ] - - res = decompose_queue(queue, operable_mock_device_2_wires_with_inverses) - - - assert len(res) == 9 - - assert res[0].name == "RZ.inv" - assert res[0].parameters == [2] - - assert res[1].name == "RY.inv" - assert res[1].parameters == [1] - - assert res[2].name == "RZ.inv" - assert res[2].parameters == [0] - - assert res[3].name == "PhaseShift.inv" - assert res[3].parameters == [4] - - assert res[4].name == "PhaseShift.inv" - assert res[4].parameters == [5] - - assert res[5].name == "RZ.inv" - assert res[5].parameters == [-5] - - assert res[6].name == "RY.inv" - assert res[6].parameters == [3] - - assert res[7].name == "RZ.inv" - assert res[7].parameters == [5] - - assert res[8].name == "RX.inv" - assert res[8].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): - phi = 0.3 - ops = [qml.RZ(phi, 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) - - -class TestQNodeVariableMap: - """Test the conversion of arguments to Variable instances.""" - - def test_regular_arguments(self, mock_device): - """Test that regular arguments are properly converted to Variable instances.""" - - def circuit(a, b, c, d): - qml.RX(a, wires=[0]) - qml.RY(b, wires=[0]) - qml.RZ(c, wires=[0]) - qml.RZ(d, wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - arg_vars, kwarg_vars = node._make_variables([1.0, 2.0, 3.0, 4.0], {}) - - expected_arg_vars = [ - Variable(0, "a"), - Variable(1, "b"), - Variable(2, "c"), - Variable(3, "d"), - ] - - for var, expected in zip(qml.utils._flatten(arg_vars), expected_arg_vars): - assert var == expected - - assert not kwarg_vars - - def test_array_arguments(self, mock_device): - """Test that array arguments are properly converted to Variable instances.""" - - def circuit(weights): - qml.RX(weights[0, 0], wires=[0]) - qml.RY(weights[0, 1], wires=[0]) - qml.RZ(weights[1, 0], wires=[0]) - qml.RZ(weights[1, 1], wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - - weights = np.array([[1, 2], [3, 4]]) - arg_vars, kwarg_vars = node._make_variables([weights], {}) - - expected_arg_vars = [ - Variable(0, "weights[0,0]"), - Variable(1, "weights[0,1]"), - Variable(2, "weights[1,0]"), - Variable(3, "weights[1,1]"), - ] - - for var, expected in zip(qml.utils._flatten(arg_vars), expected_arg_vars): - assert var == expected - - assert not kwarg_vars - - def test_regular_keyword_arguments(self, mock_device): - """Test that regular keyword arguments are properly converted to Variable instances.""" - - def circuit(*, a=1, b=2, c=3, d=4): - qml.RX(a, wires=[0]) - qml.RY(b, wires=[0]) - qml.RZ(c, wires=[0]) - qml.RZ(d, wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - arg_vars, kwarg_vars = node._make_variables([], {"b": 3}) - - expected_kwarg_vars = { - "a": [Variable(0, "a", is_kwarg=True)], - "b": [Variable(0, "b", is_kwarg=True)], - "c": [Variable(0, "c", is_kwarg=True)], - "d": [Variable(0, "d", is_kwarg=True)], - } - - assert not arg_vars - - for expected_key in expected_kwarg_vars: - for var, expected in zip( - qml.utils._flatten(kwarg_vars[expected_key]), - qml.utils._flatten(expected_kwarg_vars[expected_key]), - ): - assert var == expected - - def test_array_keyword_arguments(self, mock_device): - """Test that array keyword arguments are properly converted to Variable instances.""" - - def circuit(*, a=np.array([[1, 0], [0, 1]]), b=np.array([1, 2, 3])): - qml.RX(a[0, 0], wires=[0]) - qml.RX(a[0, 1], wires=[0]) - qml.RX(a[1, 0], wires=[0]) - qml.RX(a[1, 1], wires=[0]) - qml.RY(b[0], wires=[0]) - qml.RY(b[1], wires=[0]) - qml.RY(b[2], wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - arg_vars, kwarg_vars = node._make_variables([], {"b": np.array([6, 7, 8, 9])}) - - expected_kwarg_vars = { - "a": [ - Variable(0, "a[0,0]", is_kwarg=True), - Variable(1, "a[0,1]", is_kwarg=True), - Variable(2, "a[1,0]", is_kwarg=True), - Variable(3, "a[1,1]", is_kwarg=True), - ], - "b": [ - Variable(0, "b[0]", is_kwarg=True), - Variable(1, "b[1]", is_kwarg=True), - Variable(2, "b[2]", is_kwarg=True), - Variable(3, "b[3]", is_kwarg=True), - ], - } - - assert not arg_vars - - for expected_key in expected_kwarg_vars: - for var, expected in zip( - qml.utils._flatten(kwarg_vars[expected_key]), - qml.utils._flatten(expected_kwarg_vars[expected_key]), - ): - assert var == expected - - def test_variadic_arguments(self, mock_device): - """Test that variadic arguments are properly converted to Variable instances.""" - - def circuit(a, *b): - qml.RX(a, wires=[0]) - qml.RX(b[0], wires=[0]) - qml.RX(b[1][1], wires=[0]) - qml.RX(b[2], wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - arg_vars, kwarg_vars = node._make_variables([0.1, 0.2, np.array([0, 1, 2, 3]), 0.5], {}) - - expected_arg_vars = [ - Variable(0, "a"), - Variable(1, "b[0]"), - Variable(2, "b[1][0]"), - Variable(3, "b[1][1]"), - Variable(4, "b[1][2]"), - Variable(5, "b[1][3]"), - Variable(6, "b[2]"), - ] - - assert not kwarg_vars - - for var, expected in zip(qml.utils._flatten(arg_vars), expected_arg_vars): - assert var == expected - - def test_non_trainable_args(self, mock_device): - """Test that non trainable args are not converted to Variables""" - - def circuit(a, b, c, d): - qml.RX(a, wires=[0]) - qml.RY(b, wires=[0]) - qml.RZ(c, wires=[0]) - qml.RZ(d, wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - node.set_trainable_args({0, 3}) - var_values = [1.0, 2.0, 3.0, 4.0] - arg_vars, kwarg_vars = node._make_variables(var_values, {}) - - expected_arg_vars = [ - Variable(0, "a"), - var_values[1], - var_values[2], - Variable(3, "d"), - ] - - for var, expected in zip(qml.utils._flatten(arg_vars), expected_arg_vars): - assert var == expected - - assert not kwarg_vars - - def test_numpy_scalars(self, mock_device): - """Test that non-differentiable NumPy scalars are correctly cast to Python numeric literals - during Variable creation.""" - - def circuit(a, b): - qml.RX(a, wires=[0]) - qml.RY(b, wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - node.set_trainable_args({0}) - var_values = [np.array(1.0), np.array(2.0)] - arg_vars, kwarg_vars = node._make_variables(var_values, {}) - - expected_arg_vars = [ - Variable(0, "a[]"), - var_values[1].item(), - ] - - for var, expected in zip(qml.utils._flatten(arg_vars), expected_arg_vars): - assert var == expected - - assert not kwarg_vars - - -class TestQNodeDraw: - """Test functionality related to draw.""" - - def test_unknown_charset_error(self, mock_qnode): - """Test that an error is raised for an unsupported charset.""" - with pytest.raises(ValueError, match="Charset does_not_exist is not supported"): - mock_qnode.draw(charset="does_not_exist") - - def test_draw_before_construction_error(self): - """Test that an error is raised when drawing a QNode that is not yet constructed is attempted.""" - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev) - def circuit(a): - qml.RX(a, wires=[0]) - - return qml.expval(qml.PauliZ(0)) - - with pytest.raises( - RuntimeError, - match="The QNode can only be drawn after its CircuitGraph has been constructed", - ): - circuit.draw() - - -class TestTrainableArgs: - """Test functionality related to trainable argument setting and validation""" - - def test_all_trainable(self, mock_device): - """Test that setting trainable_args to None treats all - arguments as differentiable""" - - def circuit(a, b, c, d): - qml.RX(a, wires=[0]) - qml.RY(b, wires=[0]) - qml.RZ(c, wires=[0]) - qml.RZ(d, wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - node.set_trainable_args(None) - var_values = [1.0, 2.0, 3.0, 4.0] - arg_vars, kwarg_vars = node._make_variables(var_values, {}) - - expected_arg_vars = [ - Variable(0, "a"), - Variable(1, "b"), - Variable(2, "c"), - Variable(3, "d"), - ] - - for var, expected in zip(qml.utils._flatten(arg_vars), expected_arg_vars): - assert var == expected - - def test_none_trainable(self, mock_device): - """Test that an empty set results in no trainable arguments""" - - def circuit(a, b, c, d): - qml.RX(a, wires=[0]) - qml.RY(b, wires=[0]) - qml.RZ(c, wires=[0]) - qml.RZ(d, wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - node.set_trainable_args(set()) - var_values = [1.0, 2.0, 3.0, 4.0] - arg_vars, kwarg_vars = node._make_variables(var_values, {}) - - expected_arg_vars = [1.0, 2.0, 3.0, 4.0] - - for var, expected in zip(qml.utils._flatten(arg_vars), expected_arg_vars): - assert var == expected - - def test_invalid_index_type(self, mock_device): - """Test floats and/or negative integers passed raise an exception""" - - def circuit(a, b, c, d): - qml.RX(a, wires=[0]) - qml.RY(b, wires=[0]) - qml.RZ(c, wires=[0]) - qml.RZ(d, wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - - with pytest.raises(ValueError, match="Argument indices must be positive integers"): - node.set_trainable_args({-1, 2}) - - with pytest.raises(ValueError, match="Argument indices must be positive integers"): - node.set_trainable_args({0.5}) - - def test_invalid_index_value(self, mock_device): - """Test that an exception is raised if a specified trainable argument doesn't exist""" - - def circuit(a, b, c, d): - qml.RX(a, wires=[0]) - qml.RY(b, wires=[0]) - qml.RZ(c, wires=[0]) - qml.RZ(d, wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - - with pytest.raises(ValueError, match=r"not available\. QNode has at most 4 arguments"): - node.set_trainable_args({0, 1, 5}) - - # QNodes with variable positional arguments turn this check off - - def circuit(a, b, c, d, *args): - qml.RX(a, wires=[0]) - qml.RY(b, wires=[0]) - qml.RZ(c, wires=[0]) - qml.RZ(d, wires=[0]) - - return qml.expval(qml.PauliX(0)) - - node = BaseQNode(circuit, mock_device) - - assert node.func.var_pos - assert node.func.n_pos == 4 - - # The following will no longer raise an exception, - # since we do not know in advance how many arguments - # the user will evaluate the QNode with. - node.set_trainable_args({0, 1, 6}) - assert node.get_trainable_args() == {0, 1, 6} - - -def test_old_qnode_in_tape_mode(): - """Test that the old QNode can still be evaluated when running in tape mode""" - - # tape mode should not be active so that we can use the old QNode - assert not qml.tape_mode_active() - - try: - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def f(x): - qml.RX(x, wires=0) - qml.RY(0.4, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - qml.enable_tape() - res = f(0.4) - exp = 0.9210609940028851 - - assert np.allclose(res, exp) - - # check that tape mode is turned on again after evaluating the old QNode - assert qml.tape_mode_active() - - finally: # always make sure we turn on tape mode to prevent disrupting the other tests - qml.enable_tape() diff --git a/tests/qnodes/test_qnode_cv.py b/tests/qnodes/test_qnode_cv.py deleted file mode 100644 index e8eeca3a005..00000000000 --- a/tests/qnodes/test_qnode_cv.py +++ /dev/null @@ -1,642 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the PennyLane :class:`~.CVQNode` class. -""" -import pytest -import numpy as np - -import pennylane as qml -from pennylane._device import Device -from pennylane.operation import CVObservable -from pennylane.qnodes.base import QuantumFunctionError -from pennylane.qnodes.cv import CVQNode -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - -class PolyN(qml.ops.PolyXP): - """Mimics NumberOperator using the arbitrary 2nd order observable interface. - Results should be identical.""" - def __init__(self, wires): - hbar = 2 - q = np.diag([-0.5, 0.5/hbar, 0.5/hbar]) - super().__init__(q, wires=wires) - self.name = 'PolyXP' - - -cv_ops = [getattr(qml.ops, name) for name in qml.ops._cv__ops__] -analytic_cv_ops = [cls for cls in cv_ops if cls.supports_parameter_shift] - - -@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, '_capabilities', {"model": "cv"}) - m.setattr(dev, 'operations', ["FockState", "Displacement", "CubicPhase", "Squeezing", "Rotation", "Kerr", "Beamsplitter"]) - m.setattr(dev, 'observables', ["X", "NumberOperator", "PolyXP"]) - 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) - - -def test_transform_observable_incorrect_heisenberg_size(): - """The number of dimensions of a CV observable Heisenberg representation does - not match the ev_order attribute.""" - - class P(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]) - - dev = qml.device("default.gaussian", wires=1) - def circuit(x): - qml.Displacement(x, 0.1, wires=0) - return qml.expval(P(0)) - - node = CVQNode(circuit, dev) - - with pytest.raises(QuantumFunctionError, match="Mismatch between the polynomial order"): - node.jacobian([0.5]) - - -class TestBestMethod: - """ - Test different flows of _best_method using a mock device. TODO more - """ - def test_gaussian_successors_fails(self, operable_mock_CV_device_2_wires): - """Tests that the parameter-shift differentiation method is not allowed - if a non-gaussian gate is between a differentiable gaussian gate and an observable.""" - - 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)) - - node = CVQNode(circuit, operable_mock_CV_device_2_wires) - - with pytest.raises(ValueError, match="analytic gradient method cannot be used with"): - node.jacobian([0.321], method="A") - - assert node.par_to_grad_method == {0: "F"} - - def test_correct_method_non_gaussian_successor_one_param(self, operable_mock_CV_device_2_wires): - """Tests that a non-Gaussian succeeding a parameter fallsback to finite-diff""" - par = [0.4, -2.3] - - 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 - return qml.expval(qml.X(0)), qml.expval(qml.X(1)) - - q = CVQNode(qf, operable_mock_CV_device_2_wires) - q._construct(par, {}) - assert q.par_to_grad_method == {0: "F", 1: "A"} - - def test_correct_method_non_gaussian_successor_unused_param(self, operable_mock_CV_device_2_wires): - """Tests that a non-Gaussian succeeding a parameter fallsback to finite-diff - alongside an unused parameter""" - par = [0.4, -2.3] - - 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)) - - q = CVQNode(qf, operable_mock_CV_device_2_wires) - q._construct(par, {}) - assert q.par_to_grad_method == {0: "F", 1: "0"} - - def test_param_not_differentiable(self, operable_mock_CV_device_2_wires): - """Tests that a parameter is not differentiable if used in an operation - where grad_method=None""" - par = [0.4] - - def qf(x): - qml.FockState(x, wires=[0]) - qml.Rotation(1.3, wires=[0]) - return qml.expval(qml.X(0)) - - q = CVQNode(qf, operable_mock_CV_device_2_wires) - q._construct(par, {}) - assert q.par_to_grad_method == {0: None} - - def test_param_no_observables(self, operable_mock_CV_device_2_wires): - """Tests that a parameter has 0 gradient if it is not followed by any observables""" - par = [0.4] - - def qf(x): - qml.Displacement(x, 0, wires=[0]) - qml.Squeezing(0.3, x, wires=[0]) - qml.Rotation(1.3, wires=[1]) - return qml.expval(qml.X(1)) - - q = CVQNode(qf, operable_mock_CV_device_2_wires) - q._construct(par, {}) - assert q.par_to_grad_method == {0: "0"} - - def test_correct_method_non_gaussian_successor_all_params(self, operable_mock_CV_device_2_wires): - """Tests that a non-Gaussian succeeding all parameters fallsback to finite-diff""" - par = [0.4, -2.3] - - def qf(x, y): - qml.Displacement(x, 0, wires=[0]) - qml.Displacement(1.2, y, wires=[1]) - 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)) - - q = CVQNode(qf, operable_mock_CV_device_2_wires) - q._construct(par, {}) - assert q.par_to_grad_method == {0: "F", 1: "F"} - - def test_correct_method_non_gaussian_preceeding_one_param(self, operable_mock_CV_device_2_wires): - """Tests that a non-Gaussian preceeding one parameter fallsback to finite-diff""" - par = [0.4, -2.3] - - 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)) - - q = CVQNode(qf, operable_mock_CV_device_2_wires) - q._construct(par, {}) - assert q.par_to_grad_method == {0: "A", 1: "F"} - - def test_correct_method_non_gaussian_observable(self, operable_mock_CV_device_2_wires): - """Tests that a non-Gaussian observable one parameter fallsback to finite-diff""" - par = [0.4, -2.3] - - def qf(x, y): - qml.Displacement(x, 0, wires=[0]) # followed by nongaussian observable - qml.Beamsplitter(0.2, 1.7, wires=[0, 1]) - qml.Displacement(y, 0, wires=[1]) # followed by order-2 observable - return qml.expval(qml.FockStateProjector(np.array([2]), 0)), qml.expval(qml.NumberOperator(1)) - - q = CVQNode(qf, operable_mock_CV_device_2_wires) - q._construct(par, {}) - assert q.par_to_grad_method == {0: "F", 1: "A"} - - -class TestExpectationJacobian: - """Jacobian integration tests for CV expectations.""" - - def test_keywordarg_second_order_cv(self, tol): - """Non-differentiable keyword arguments with a second order CV expectation value.""" - - dev = qml.device("default.gaussian", wires=3) - def circuit(x, *, k=0.0): - qml.Displacement(x, 0, wires=0) - qml.Rotation(k, wires=0) - return qml.expval(qml.PolyXP(np.diag([0, 1, 0]), wires=0)) # X^2 - - node = CVQNode(circuit, dev) - par = [0.62] - aux = {'k': 0.4} - - # circuit jacobians - grad_A = node.jacobian(par, aux, method="A") - grad_F = node.jacobian(par, aux, method="F") - expected = np.array([[8 * par[0] * np.cos(aux['k']) ** 2]]) - assert grad_A == pytest.approx(grad_F, abs=tol) - assert grad_A == pytest.approx(expected, abs=tol) - - def test_keywordarg_with_positional_arg_immutable_second_order_cv(self, tol): - """Non-differentiable keyword arguments appear in the same op with differentiable arguments, - qfunc is immutable so kwargs are passed as Variables.""" - - dev = qml.device("default.gaussian", wires=1) - def circuit(x, *, k=0.0): - qml.Displacement(0.5, 0, wires=0) - qml.Squeezing(x, k, wires=0) - return qml.expval(qml.X(0)) - - node = CVQNode(circuit, dev, mutable=False) - par = [0.39] - aux = {'k': -0.7} - - # circuit jacobians - grad_A = node.jacobian(par, aux, method="A", options={'force_order2': True}) - grad_F = node.jacobian(par, aux, method="F") - assert grad_A == pytest.approx(grad_F, abs=tol) - - @pytest.mark.parametrize('O', [qml.ops.X, qml.ops.NumberOperator, PolyN, qml.ops.Identity]) - @pytest.mark.parametrize('G', analytic_cv_ops) - def test_cv_gradients_gaussian_circuit(self, G, O, tol): - """Tests that the gradients of circuits of gaussian gates match between the finite difference and analytic methods.""" - gaussian_dev = qml.device("default.gaussian", wires=2) - - tol = 1e-5 - par = [0.4] - def circuit(x): - args = [0.3] * G.num_params - args[0] = x - qml.Displacement(0.5, 0, wires=0) - G(*args, wires=range(G.num_wires)) - qml.Beamsplitter(1.3, -2.3, wires=[0, 1]) - qml.Displacement(-0.5, 0.1, wires=0) - qml.Squeezing(0.5, -1.5, wires=0) - qml.Rotation(-1.1, wires=0) - return qml.expval(O(wires=0)) - - q = CVQNode(circuit, gaussian_dev) - val = q.evaluate(par, {}) - - grad_F = q.jacobian(par, method="F") - 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.par_to_grad_method == {0:"A"} - # the different methods agree - assert grad_A2 == pytest.approx(grad_F, abs=tol) - - def test_gradient_gate_with_two_parameters(self, tol): - """Gates with two parameters yield the correct parshift gradient.""" - - dev = qml.device("default.gaussian", wires=1) - def qf(r0, phi0, r1, phi1): - qml.Squeezing(r0, phi0, wires=[0]) - qml.Squeezing(r1, phi1, wires=[0]) - return qml.expval(qml.NumberOperator(0)) - - q = CVQNode(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 grad_A == pytest.approx(grad_F, abs=tol) - - def test_cv_gradients_multiple_gate_parameters(self, tol): - """Tests that gates with multiple free parameters yield correct gradients.""" - - gaussian_dev = qml.device("default.gaussian", wires=2) - def qf(r0, phi0, r1, phi1): - qml.Squeezing(r0, phi0, wires=[0]) - qml.Squeezing(r1, phi1, wires=[0]) - return qml.expval(qml.NumberOperator(0)) - - q = CVQNode(qf, gaussian_dev) - par = [0.4, -0.3, -0.7, 0.2] - - grad_F = q.jacobian(par, method="F") - grad_A = q.jacobian(par, method="A") - grad_A2 = q.jacobian(par, method="A", options={'force_order2': True}) - # analytic method works for every parameter - 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) - - # check against the known analytic formula - r0, phi0, r1, phi1 = par - dn = np.zeros([4]) - dn[0] = np.cosh(2 * r1) * np.sinh(2 * r0) + np.cos(phi0 - phi1) * np.cosh(2 * r0) * np.sinh(2 * r1) - dn[1] = -0.5 * np.sin(phi0 - phi1) * np.sinh(2 * r0) * np.sinh(2 * r1) - dn[2] = np.cos(phi0 - phi1) * np.cosh(2 * r1) * np.sinh(2 * r0) + np.cosh(2 * r0) * np.sinh(2 * r1) - dn[3] = 0.5 * np.sin(phi0 - phi1) * np.sinh(2 * r0) * np.sinh(2 * r1) - assert dn[np.newaxis, :] == pytest.approx(grad_F, abs=tol) - - def test_cv_gradients_repeated_gate_parameters(self, tol): - """Tests that repeated use of a free parameter in a multi-parameter gate yield correct gradients.""" - gaussian_dev = qml.device("default.gaussian", wires=2) - par = [0.2, 0.3] - - def qf(x, y): - qml.Displacement(x, 0, wires=[0]) - qml.Squeezing(y, -1.3*y, wires=[0]) - return qml.expval(qml.X(0)) - - q = CVQNode(qf, gaussian_dev) - grad_F = q.jacobian(par, method="F") - grad_A = q.jacobian(par, method="A") - grad_A2 = q.jacobian(par, method="A", options={'force_order2': True}) - - # analytic method works for every parameter - 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) - - def test_cv_gradients_parameters_inside_array(self, tol): - """Tests that free parameters inside an array passed to an Operation yield correct gradients.""" - gaussian_dev = qml.device("default.gaussian", wires=2) - par = [0.4, 1.3] - - def qf(x, y): - qml.Displacement(0.5, 0, wires=[0]) - qml.Squeezing(x, 0, wires=[0]) - M = np.zeros((5, 5), dtype=object) - M[1,1] = y - M[1,2] = 1.0 - M[2,1] = 1.0 - return qml.expval(qml.PolyXP(M, [0, 1])) - - q = CVQNode(qf, gaussian_dev) - - grad_best = q.jacobian(par) - grad_best2 = q.jacobian(par, options={"force_order2": True}) - grad_F = q.jacobian(par, method="F") - - # par[0] can use the "A" method, par[1] cannot - assert q.par_to_grad_method == {0: "A", 1: "F"} - # the different methods agree - assert grad_best == pytest.approx(grad_F, abs=tol) - assert grad_best2 == pytest.approx(grad_F, abs=tol) - - def test_cv_gradient_fanout(self, tol): - """Tests that CV qnodes can compute the correct gradient when the same parameter is used - in multiple gates.""" - gaussian_dev = qml.device("default.gaussian", wires=2) - par = [0.5, 1.3] - - def circuit(x, y): - qml.Displacement(x, 0, wires=[0]) - qml.Rotation(y, wires=[0]) - qml.Displacement(0, x, wires=[0]) - return qml.expval(qml.X(0)) - - q = CVQNode(circuit, gaussian_dev) - grad_F = q.jacobian(par, method="F") - grad_A = q.jacobian(par, method="A") - grad_A2 = q.jacobian(par, method="A", options={'force_order2': True}) - - # analytic method works for every parameter - 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) - - @pytest.mark.parametrize('name', qml.ops._cv__ops__) - def test_CVOperation_with_heisenberg_and_no_parshift(self, name, tol): - """An integration test for Gaussian CV gates that have a Heisenberg representation - but cannot be differentiated using the parameter-shift method themselves - (for example, they may accept no parameters, or have no gradient recipe). - - Tests that the parameter-shift method can still be used with other gates in the circuit. - """ - gaussian_dev = qml.device("default.gaussian", wires=2) - - cls = getattr(qml.ops, name) - if cls.supports_heisenberg and (not cls.supports_parameter_shift): - U = np.array([[0.51310276+0.81702166j, 0.13649626+0.22487759j], - [0.26300233+0.00556194j, -0.96414101-0.03508489j]]) - - if cls.num_wires <= 0: - w = list(range(2)) - else: - w = list(range(cls.num_wires)) - - def circuit(x): - qml.Displacement(x, 0, wires=0) - - if cls.par_domain == "A": - cls(U, wires=w) - else: - cls(wires=w) - return qml.expval(qml.X(0)) - - qnode = CVQNode(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", options={'force_order2': True}) - - # par[0] can use the "A" method - assert qnode.par_to_grad_method == {0: "A"} - - # the different methods agree - assert grad_A == pytest.approx(grad_F, abs=tol) - assert grad_A2 == pytest.approx(grad_F, abs=tol) - - def test_non_gaussian_gate_successor(self, gaussian_device, tol): - """Parshift differentiation method is allowed and matches finite diff - if a non-Gaussian gate follows the parametrized gate but is not followed by an observable.""" - - def circuit(x): - qml.Squeezing(x, 0, wires=[0]) - qml.Beamsplitter(1.1, 0, wires=[0, 1]) - qml.Kerr(0.54, wires=[1]) # nongaussian - return qml.expval(qml.NumberOperator(0)) - - node = CVQNode(circuit, gaussian_device) - par = [0.321] - - grad_A = node.jacobian(par, wrt=[0], method="A") - grad_F = node.jacobian(par, method="F") - assert grad_A == pytest.approx(grad_F, abs=tol) - assert node.par_to_grad_method == {0: "A"} - - def test_non_gaussian_obs_predecessor(self, gaussian_device, tol): - """Parshift differentiation method is allowed and matches finite diff - if a non-Gaussian gate precedes an observable but is not preceded by the parametrized gate.""" - - def circuit(x): - qml.Squeezing(x, 0, wires=[0]) - qml.Kerr(0.54, wires=[1]) # nongaussian - qml.Beamsplitter(1.1, 0, wires=[0, 1]) - return qml.expval(qml.NumberOperator(0)) - - node = CVQNode(circuit, gaussian_device) - par = [0.321] - - grad_A = node.jacobian(par, wrt=[0], method="A") - grad_F = node.jacobian(par, method="F") - assert grad_A == pytest.approx(grad_F, abs=tol) - assert node.par_to_grad_method == {0: "A"} - - 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 = CVQNode(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) - - @pytest.mark.xfail(reason="FIXME: 'A' method fails on QuadOperator (it has no gradient recipe)", raises=AttributeError, strict=True) - def test_quadoperator(self, tol): - """Test the differentiation of CV observables that depend on positional qfunc parameters.""" - - def circuit(a): - qml.Displacement(1.0, 0, wires=0) - return qml.expval(qml.QuadOperator(a, 0)) - - gaussian_dev = qml.device("default.gaussian", wires=1) - qnode = CVQNode(circuit, gaussian_dev) - - par = [0.6] - grad_F = qnode.jacobian(par, method='F') - grad_A = qnode.jacobian(par, method='A') - grad_A2 = qnode.jacobian(par, method='A', options={'force_order2': True}) - - # par 0 can use the 'A' method - assert qnode.par_to_grad_method == {0: 'A'} - # the different methods agree - assert grad_A == pytest.approx(grad_F, abs=tol) - assert grad_A2 == pytest.approx(grad_F, abs=tol) - - -class TestVarianceJacobian: - """Variance analytic jacobian integration tests.""" - - def test_first_order_cv(self, tol): - """Test variance of a first order CV expectation value""" - dev = qml.device("default.gaussian", wires=1) - - def circuit(r, phi): - qml.Squeezing(r, 0, wires=0) - qml.Rotation(phi, wires=0) - return qml.var(qml.X(0)) - - circuit = CVQNode(circuit, dev) - - 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 var == pytest.approx(expected, abs=tol) - - # 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 gradA == pytest.approx(expected, abs=tol) - assert gradF == pytest.approx(expected, abs=tol) - - def test_second_order_cv(self, tol): - """Test variance of a second order CV expectation value""" - dev = qml.device("default.gaussian", wires=1) - - def circuit(n, a): - qml.ThermalState(n, wires=0) - qml.Displacement(a, 0, wires=0) - return qml.var(qml.NumberOperator(0)) - - circuit = CVQNode(circuit, dev) - - n = 0.12 - a = 0.765 - var = circuit(n, a) - expected = n ** 2 + n + np.abs(a) ** 2 * (1 + 2 * n) - assert var == pytest.approx(expected, abs=tol) - - # circuit jacobians - gradF = circuit.jacobian([n, a], method="F") - expected = np.array([[2 * a ** 2 + 2 * n + 1, 2 * a * (2 * n + 1)]]) - assert gradF == pytest.approx(expected, abs=tol) - - - 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 = CVQNode(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_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) - - def circuit(a): - qml.Displacement(a, 0, wires=0) - return qml.var(qml.NumberOperator(0)) - - circuit = CVQNode(circuit, dev) - - with pytest.raises(ValueError, match=r"cannot be used with the argument\(s\) \{'a'\}"): - circuit.jacobian([1.0], method="A") - - def test_error_unsupported_grad_recipe(self, monkeypatch): - """Test exception raised if attempting to use the second order rule for - computing the gradient analytically of an expectation value that - contains an operation with an more than two terms in the gradient recipe""" - - class DummyOp(qml.operation.CVOperation): - num_wires = 1 - num_params = 1 - par_domain = "R" - grad_method = "A" - grad_recipe = ([[1, 1, 1], [1, 1, 1], [1, 1, 1]],) - - dev = qml.device("default.gaussian", wires=1) - - dev._operation_map["DummyOp"] = None - - def circuit(a): - DummyOp(a, wires=[0]) - return qml.expval(qml.NumberOperator(0)) - - with monkeypatch.context() as m: - circuit = CVQNode(circuit, dev, force_order2=True) - - m.setattr(circuit, "_best_method", lambda arg: "A") - with pytest.raises(NotImplementedError, match=r"analytic gradient for order-2 operators is unsupported"): - grad_A = circuit.jacobian(0, method="A", options={'force_order2': True}) diff --git a/tests/qnodes/test_qnode_decorator.py b/tests/qnodes/test_qnode_decorator.py deleted file mode 100644 index 2d07bcee58f..00000000000 --- a/tests/qnodes/test_qnode_decorator.py +++ /dev/null @@ -1,303 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane.qnode` decorator. -""" -# pylint: disable=protected-access,cell-var-from-loop -import numpy as np -import pytest - -import pennylane as qml -from pennylane.qnodes import qnode, CVQNode, JacobianQNode, BaseQNode, QubitQNode, ReversibleQNode -from pennylane.qnodes.jacobian import DEFAULT_STEP_SIZE_ANALYTIC, DEFAULT_STEP_SIZE -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - -def test_create_qubit_qnode(): - """Test the decorator correctly creates Qubit QNodes""" - dev = qml.device("default.qubit", wires=1) - - @qnode(dev) - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert isinstance(circuit, QubitQNode) - assert hasattr(circuit, "jacobian") - - -def test_create_CV_qnode(): - """Test the decorator correctly creates Qubit QNodes""" - dev = qml.device("default.gaussian", wires=1) - - @qnode(dev) - def circuit(a): - qml.Displacement(a, 0, wires=0) - return qml.expval(qml.X(wires=0)) - - assert isinstance(circuit, CVQNode) - assert hasattr(circuit, "jacobian") - - -def test_fallback_Jacobian_qnode(monkeypatch): - """Test the decorator falls back to Jacobian QNode if it - can't determine the device model""" - dev = qml.device("default.gaussian", wires=1) - - # use monkeypatch to avoid setting class attributes - with monkeypatch.context() as m: - m.setattr(dev, 'capabilities', lambda: {"model": None}) - - @qnode(dev) - def circuit(a): - qml.Displacement(a, 0, wires=0) - return qml.expval(qml.X(wires=0)) - - assert not isinstance(circuit, CVQNode) - assert not isinstance(circuit, QubitQNode) - assert isinstance(circuit, JacobianQNode) - assert hasattr(circuit, "jacobian") - - -def test_torch_interface(skip_if_no_torch_support): - """Test torch interface conversion""" - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, interface="torch") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert circuit.interface == "torch" - - -step_sizes = [(True, DEFAULT_STEP_SIZE_ANALYTIC), (False, DEFAULT_STEP_SIZE)] - - -@pytest.mark.parametrize("analytic, step_size", step_sizes) -def test_finite_diff_qubit_qnode(analytic, step_size): - """Test that a finite-difference differentiable qubit QNode - is correctly created when diff_method='finite-diff' and analytic=True""" - dev = qml.device("default.qubit", wires=1, analytic=analytic) - - @qnode(dev, diff_method="finite-diff") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert not isinstance(circuit, CVQNode) - assert not isinstance(circuit, QubitQNode) - assert isinstance(circuit, JacobianQNode) - assert hasattr(circuit, "jacobian") - assert circuit.h == step_size - assert circuit.order == 1 - - -@pytest.mark.parametrize("order", [1, 2]) -def test_setting_order(order): - """Test that the order is correctly set and reset in a finite-difference QNode.""" - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, diff_method="finite-diff", order=order) - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert circuit.order == order - - circuit.order = 1 - assert circuit.order == 1 - - -def test_finite_diff_qubit_qnode_passing_step_size_through_decorator(): - """Test that a finite-difference differentiable qubit QNode is correctly - created when diff_method='finite-diff' and the step size is set through the - decorator.""" - step_size = 0.5 - new_step_size = 0.12345 - - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, diff_method="finite-diff", h=step_size) - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert not isinstance(circuit, CVQNode) - assert not isinstance(circuit, QubitQNode) - assert isinstance(circuit, JacobianQNode) - assert hasattr(circuit, "jacobian") - assert circuit.h == step_size - - circuit.h = new_step_size - assert circuit.h == new_step_size - - -def test_reversible_diff_method(): - """Test that a ReversibleQNode can be created via the qnode decorator""" - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, diff_method="reversible") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert isinstance(circuit, ReversibleQNode) - - -def test_reversible_diff_method_exception(monkeypatch): - """Test that an exception is raised if the reversible diff_method - is specified for a device which does not have reversible capability.""" - dev = qml.device("default.qubit", wires=1) - - # overwrite capabilities - capabilities = dev.capabilities().copy() - capabilities["supports_reversible_diff"] = False - monkeypatch.setattr(dev, 'capabilities', lambda: capabilities) - - with pytest.raises(ValueError, match="Reversible differentiation method not supported"): - - @qnode(dev, diff_method="reversible") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - -def test_tf_interface(skip_if_no_tf_support): - """Test tf interface conversion""" - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, interface="tf") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert circuit.interface == "tf" - - -def test_autograd_interface(): - """Test autograd interface conversion""" - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, interface="autograd") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert circuit.interface == "autograd" - - -def test_no_interface(): - """Test no interface conversion""" - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, interface=None) - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert circuit.interface is None - - -def test_not_differentiable(): - """Test QNode marked as non-differentiable""" - dev = qml.device("default.qubit", wires=1) - - @qnode(dev, interface=None, diff_method=None) - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - assert isinstance(circuit, BaseQNode) - assert not isinstance(circuit, JacobianQNode) - - assert not hasattr(circuit, "interface") - assert not hasattr(circuit, "jacobian") - - -def test_invalid_diff_method(): - """Test exception raised if an invalid diff - method is provided""" - dev = qml.device("default.qubit", wires=1) - - with pytest.raises(ValueError, match=r"Differentiation method \w+ not recognized"): - - @qnode(dev, interface=None, diff_method="test") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - -def test_invalid_interface(): - """Test exception raised if an invalid interface - is provided""" - dev = qml.device("default.qubit", wires=1) - - with pytest.raises(ValueError, match=r"Interface \w+ not recognized"): - - @qnode(dev, interface="test") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - -def test_classical_diff_method_unsupported(): - """Test exception raised if an the classical diff method is specified for a - device that does not support it""" - dev = qml.device("default.qubit", wires=1) - - with pytest.raises( - ValueError, - match=r"device does not support native computations with " "autodifferentiation frameworks", - ): - - @qnode(dev, diff_method="backprop") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - -def test_device_diff_method_unsupported(): - """Test exception raised if an the device diff method is specified for a - device that does not support it""" - dev = qml.device("default.qubit", wires=1) - - with pytest.raises( - ValueError, match=r"device does not provide a native method " "for computing the jacobian" - ): - - @qnode(dev, diff_method="device") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - -def test_parameter_shift_diff_method_unsupported(): - """Test exception raised if an the device diff method is specified for a - device that does not support it""" - - class DummyDevice(qml.devices.DefaultQubit): - @classmethod - def capabilities(cls): - return {"model": None} - - dev = DummyDevice(wires=2) - - with pytest.raises( - ValueError, match=r"The parameter shift rule is not available for devices with model" - ): - - @qnode(dev, diff_method="parameter-shift") - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) diff --git a/tests/qnodes/test_qnode_jacobian.py b/tests/qnodes/test_qnode_jacobian.py deleted file mode 100644 index 40dcef676e0..00000000000 --- a/tests/qnodes/test_qnode_jacobian.py +++ /dev/null @@ -1,241 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane` :class:`JacobianQNode` class. -""" -from unittest import mock - -import pytest -import numpy as np - -import pennylane as qml -from pennylane._device import Device -from pennylane.operation import CVObservable -from pennylane.qnodes.base import QuantumFunctionError -from pennylane.qnodes.jacobian import JacobianQNode -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - -@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, '_capabilities', {"model": "qubit"}) - 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) - - -class TestAJacobianQNodeDetails: - """Test configuration details of the autograd interface""" - - def test_interface_str(self, qubit_device_2_wires): - """Test that the interface string is correctly identified - as None""" - def circuit(x, y, z): - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1)) - - circuit = JacobianQNode(circuit, qubit_device_2_wires) - assert circuit.interface == None - - -class TestJacobianQNodeExceptions: - """Tests that JacobianQNode.jacobian raises proper errors.""" - - def test_gradient_of_sample(self, operable_mock_device_2_wires): - """Differentiation of a sampled output.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.sample(qml.PauliZ(0)), qml.sample(qml.PauliX(1)) - - node = JacobianQNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(QuantumFunctionError, - match="Circuits that include sampling can not be differentiated."): - node.jacobian(1.0) - - def test_nondifferentiable_operator(self, operable_mock_device_2_wires): - """Differentiating wrt. a parameter - that appears as an argument to a nondifferentiable operator.""" - - def circuit(x): - qml.BasisState(np.array([x, 0]), wires=[0, 1]) # not differentiable - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = JacobianQNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(ValueError, match=r"Cannot differentiate with respect to argument\(s\) \{'x'\}"): - node.jacobian(0.5) - - def test_operator_not_supporting_pd_analytic(self, operable_mock_device_2_wires): - """Differentiating wrt. a parameter that appears - as an argument to an operation that does not support parameter-shift derivatives.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.Hermitian(np.diag([x, 0]), 0)) - - node = JacobianQNode(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): - """The gradient method set is bogus.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - # in mutable mode, the grad method would be - # recomputed and overwritten from the - # bogus value 'J'. Caching stops this from happening. - node = JacobianQNode(circuit, operable_mock_device_2_wires, mutable=False) - - node.evaluate([0.0], {}) - node.par_to_grad_method[0] = "J" - - with pytest.raises(ValueError, match="Unknown gradient method"): - node.jacobian(0.5) - - def test_indices_not_unique(self, operable_mock_device_2_wires): - """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 = JacobianQNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(ValueError, match="Parameter indices must be unique."): - node.jacobian(0.5, wrt=[0, 0]) - - def test_indices_nonexistant(self, operable_mock_device_2_wires): - """ 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 = JacobianQNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(ValueError, match="Tried to compute the gradient with respect to"): - node.jacobian(0.5, wrt=[0, 6]) - - with pytest.raises(ValueError, match="Tried to compute the gradient with respect to"): - node.jacobian(0.5, wrt=[1, -1]) - - def test_unknown_gradient_method(self, operable_mock_device_2_wires): - """ The gradient method is unknown.""" - - def circuit(x): - qml.Rot(0.3, x, -0.2, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = JacobianQNode(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): - """Finite difference are attempted with wrong order.""" - - def circuit(x): - qml.Rot(0.3, x, -0.2, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - node = JacobianQNode(circuit, operable_mock_device_2_wires) - - with pytest.raises(ValueError, match="Order must be 1 or 2"): - node.jacobian(0.5, method="F", options={'order': 3}) - - -class TestBestMethod: - """Test different flows of _best_method""" - - def test_all_finite_difference(self, operable_mock_device_2_wires): - """Finite difference is the best method in almost all cases""" - - def circuit(x, y, z): - qml.Rot(x, y, z, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - q = JacobianQNode(circuit, operable_mock_device_2_wires) - q._construct([1.0, 1.0, 1.0], {}) - assert q.par_to_grad_method == {0: "F", 1: "F", 2: "F"} - - def test_no_following_observable(self, operable_mock_device_2_wires): - """Test that the gradient is 0 if no observables succeed""" - - def circuit(x): - qml.RX(x, wires=[1]) - return qml.expval(qml.PauliZ(0)) - - q = JacobianQNode(circuit, operable_mock_device_2_wires) - q._construct([1.0], {}) - assert q.par_to_grad_method == {0: "0"} - - def test_param_unused(self, operable_mock_device_2_wires): - """Test that the gradient is 0 of an unused parameter""" - - def circuit(x, y): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - q = JacobianQNode(circuit, operable_mock_device_2_wires) - q._construct([1.0, 1.0], {}) - assert q.par_to_grad_method == {0: "F", 1: "0"} - - def test_not_differentiable(self, operable_mock_device_2_wires): - """Test that an operation with grad_method=None is marked as - non-differentiable""" - - def circuit(x): - qml.BasisState(x, wires=[1]) - return qml.expval(qml.PauliZ(0)) - - q = JacobianQNode(circuit, operable_mock_device_2_wires) - q._construct([np.array([1.0])], {}) - assert q.par_to_grad_method == {0: None} - - -@pytest.mark.parametrize("order", [1, 2]) -@pytest.mark.parametrize("h", [0.01, 0.1]) -def test_finite_difference_options(qubit_device_1_wire, order, h, monkeypatch): - """Test that the _pd_finite_diff method for calculating the finite difference gradient - correctly receives the options set when the JacobianQNode was instantiated.""" - - def circuit(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - q = JacobianQNode(circuit, qubit_device_1_wire, order=order, h=h) - - _pd_finite_diff = mock.MagicMock(return_value=0) - - with monkeypatch.context() as m: - m.setattr("pennylane.qnodes.jacobian.JacobianQNode._pd_finite_diff", _pd_finite_diff) - q.jacobian(0.2) - call_kwargs = _pd_finite_diff.call_args[1] - - assert call_kwargs["order"] == order - assert call_kwargs["h"] == h diff --git a/tests/qnodes/test_qnode_metric_tensor.py b/tests/qnodes/test_qnode_metric_tensor.py deleted file mode 100644 index 59c75d87d4e..00000000000 --- a/tests/qnodes/test_qnode_metric_tensor.py +++ /dev/null @@ -1,515 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane` :class:`QubitQNode` metric tensor methods. -""" -import pytest -import numpy as np -from scipy.linalg import block_diag - -import pennylane as qml -from pennylane.qnodes.qubit import QubitQNode -from pennylane.qnodes.base import QuantumFunctionError -from gate_data import Y, Z -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - -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 = QubitQNode(circuit, dev) - - with pytest.raises(QuantumFunctionError, match="has no defined generator"): - circuit.metric_tensor([1], only_construct=True) - - 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 = QubitQNode(circuit, dev) - - with monkeypatch.context() as m: - m.setattr("pennylane.RX.generator", [qml.RX, 1]) - - with pytest.raises(QuantumFunctionError, match="no corresponding observable"): - circuit.metric_tensor([1], only_construct=True) - - 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)), qml.expval(qml.PauliX(1)) - - circuit = QubitQNode(circuit, dev) - circuit.metric_tensor([1, 1, 1], only_construct=True) - 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].data[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)), qml.expval(qml.PauliX(1)), qml.expval(qml.PauliX(2)) - - circuit = QubitQNode(circuit, dev) - - params = np.ones([8]) - circuit.metric_tensor([params], only_construct=True) - 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)), qml.expval(qml.PauliX(1)) - - circuit = QubitQNode(circuit, dev) - - 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) - - 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)), qml.expval(qml.PauliX(1)) - - circuit = QubitQNode(circuit, dev) - - 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 - - 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)), qml.expval(qml.PauliX(1)), qml.expval(qml.PauliX(2)) - - final = QubitQNode(final, dev) - - 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(params) - - # ============================================ - # 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. - - 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)) - - layer2_diag = QubitQNode(layer2_diag, dev) - 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]) - - 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)) - - layer3_diag = QubitQNode(layer3_diag, 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)) - - layer3_off_diag_first_order = QubitQNode(layer3_off_diag_first_order, 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])) - - layer3_off_diag_second_order = QubitQNode(layer3_off_diag_second_order, dev) - - # 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(params, 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. - - 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)) - - layer2_diag = QubitQNode(layer2_diag, dev) - 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]) - - 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)) - - layer3_diag = QubitQNode(layer3_diag, dev) - - # 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) diff --git a/tests/qnodes/test_qnode_passthru.py b/tests/qnodes/test_qnode_passthru.py deleted file mode 100644 index f8676a87603..00000000000 --- a/tests/qnodes/test_qnode_passthru.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane` :class:`.PassthruQNode` class. -""" -import pytest -import numpy as np - -try: - import tensorflow as tf - - if tf.__version__[0] == "1": - tf = None # default.tensor.tf requires TF 2 - else: - from tensorflow import Variable -except ImportError: - tf = None - -import pennylane as qml -from pennylane.qnodes import PassthruQNode, BaseQNode, JacobianQNode -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - - -@pytest.fixture(scope="function") -def mock_qnode(mock_device): - """Simple PassthruQNode with default properties.""" - - def circuit(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - node = PassthruQNode(circuit, mock_device) - return node - - -class TestPassthruBasics: - """Tests basic PassthruQNode properties.""" - - def test_always_mutable(self, mock_qnode): - """PassthruQNodes are always mutable.""" - assert mock_qnode.mutable - - def test_repr(self, mock_qnode): - """String representation.""" - assert repr(mock_qnode) == "" - - def test_immutable_error(self, mock_device): - """Test that an error is raised if the mutable=False option is passed - upon instantiation""" - def circuit(x): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - with pytest.raises(ValueError, match=r"PassthruQNode does not support immutable mode"): - node = PassthruQNode(circuit, mock_device, mutable=False) - -@pytest.mark.skipif(tf is None, reason="TensorFlow 2.0 not found.") -@pytest.fixture(scope="function") -def tensornet_tf_device(): - return qml.device('default.tensor.tf', wires=2) - - -@pytest.mark.skipif(tf is None, reason="TensorFlow 2.0 not found.") -class TestPassthruTF: - """Test that TF objects can be successfully passed through to a TF simulator device, and back to user.""" - - # real data type used by the default.tensor.tf plugin (TensorFlow is strict about types) - DTYPE = tf.float64 if tf else None - - def test_arraylike_args(self, tensornet_tf_device, tol): - """Tests that PassthruQNode can use array-like TF objects as positional arguments.""" - - def circuit(x): - qml.RX(x[0], wires=[0]) - qml.RX(2*x[1], wires=[1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - node = PassthruQNode(circuit, tensornet_tf_device) - x = tf.Variable([1.1, 1.4], dtype=self.DTYPE) - res = node(x) - assert isinstance(res, tf.Tensor) - assert res.shape == (2,) - assert res.dtype == self.DTYPE - - def test_arraylike_keywordargs(self, tensornet_tf_device, tol): - """Tests that qnodes use array-like TF objects as keyword-only arguments.""" - - def circuit(*, x=None): - qml.RX(x[0], wires=[0]) - qml.RX(2*x[1], wires=[1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - node = PassthruQNode(circuit, tensornet_tf_device) - x = tf.Variable([1.1, 1.4], dtype=self.DTYPE) - res = node(x=x) - assert isinstance(res, tf.Tensor) - assert res.shape == (2,) - assert res.dtype == self.DTYPE - - def test_tensor_operations(self, tensornet_tf_device, tol): - """Tests the evaluation of a PassthruQNode involving algebraic operations between tensor parameters, - and TF functions acting on them.""" - - def circuit(phi, theta): - x = phi * theta - qml.RX(x[0], wires=0) - qml.RY(tf.cos(x[1]), wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.Hadamard(1)) - - node = PassthruQNode(circuit, tensornet_tf_device) - - phi = tf.Variable(np.array([0.7, -1.2]), dtype=self.DTYPE) - theta = tf.Variable(1.7, dtype=self.DTYPE) - res = node(phi, theta) - assert isinstance(res, tf.Tensor) - assert res.shape == (2,) - assert res.dtype == self.DTYPE - - def test_evaluate(self, tensornet_tf_device, tol): - """Tests the evaluation of a PassthruQNode.""" - - def circuit(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.RX(x[2], wires=1) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - x = np.array([-1.4, 0.2, 2.1]) - x_tf = tf.Variable(x, dtype=self.DTYPE) - - node = PassthruQNode(circuit, tensornet_tf_device) - res = node(x_tf) - - # compare to a reference node - #ref_device = qml.device('default.qubit', wires=2) - #ref_node = BaseQNode(circuit, ref_device) - #ref_res = ref_node(x) - # analytic result - ref_res = np.cos(x[0]) * np.array([1.0, np.cos(x[1]) * np.cos(x[2])]) - - assert isinstance(res, tf.Tensor) - assert res.shape == (2,) - assert res.dtype == self.DTYPE - assert res.numpy() == pytest.approx(ref_res, abs=tol) - - def test_circuit_with_decomposition(self, tol): - """Tests a PassthruQNode in which the circuit contains an operation - that needs to be decomposed.""" - theta = 0.543 - phi = -0.234 - lam = 0.654 - p = [theta, phi, lam] - - dev = qml.device("default.tensor.tf", wires=1) - - def circuit(weights): - qml.QubitStateVector(1j*np.array([1, -1])/np.sqrt(2), wires=[0]) - qml.U3(weights[0], weights[1], weights[2], wires=[0]) # <--- decomposition is required - return qml.expval(qml.PauliX(0)) - - node = PassthruQNode(circuit, dev, interface="tf") - - params = tf.Variable(p, dtype=tf.float64) - - with tf.GradientTape() as tape: - tape.watch(params) - res = node(params) - - expected = np.sin(lam)*np.sin(phi) - np.cos(theta)*np.cos(lam)*np.cos(phi) - assert np.allclose(res, expected, atol=tol, rtol=0) - - @pytest.mark.parametrize("vectorize_jacobian", [True, False]) - def test_jacobian(self, tensornet_tf_device, vectorize_jacobian, tol): - """Tests the computing of the Jacobian of a PassthruQNode using TensorFlow.""" - - def circuit(phi, theta): - qml.RX(phi[0], wires=0) - qml.RY(phi[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.RX(theta, wires=1) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - ph = np.array([0.7, -1.2]) - th = 1.7 - phi = tf.Variable(ph, dtype=self.DTYPE) - theta = tf.Variable(th, dtype=self.DTYPE) - - node = PassthruQNode(circuit, tensornet_tf_device) - # In TF 2, tf.GradientTape.jacobian comes with a vectorization option. - with tf.GradientTape(persistent=not vectorize_jacobian) as tape: - tape.watch([phi, theta]) - res = node(phi, theta) - phi_grad, theta_grad = tape.jacobian(res, [phi, theta], - unconnected_gradients=tf.UnconnectedGradients.ZERO, - experimental_use_pfor=vectorize_jacobian) - - # compare to a reference Jacobian computed using finite differences - #ref_device = qml.device('default.qubit', wires=2) - #ref_node = JacobianQNode(circuit, ref_device) - #ref_jac = ref_node.jacobian([ph, th]) - # analytic result - ref_jac = np.array([ - [-np.sin(ph[0]), 0., 0.], - [-np.sin(ph[0]) * np.cos(ph[1]) * np.cos(th), - np.cos(ph[0]) * -np.sin(ph[1]) * np.cos(th), - np.cos(ph[0]) * np.cos(ph[1]) * -np.sin(th)] - ]) - - assert isinstance(phi_grad, tf.Tensor) - assert phi_grad.shape == (2, 2) - assert phi_grad.dtype == self.DTYPE - assert phi_grad.numpy() == pytest.approx(ref_jac[:, 0:2], abs=tol) - - assert isinstance(theta_grad, tf.Tensor) - assert theta_grad.shape == (2,) - assert theta_grad.dtype == self.DTYPE - assert theta_grad.numpy() == pytest.approx(ref_jac[:, 2], abs=tol) diff --git a/tests/qnodes/test_qnode_qubit.py b/tests/qnodes/test_qnode_qubit.py deleted file mode 100644 index 637ccdf6330..00000000000 --- a/tests/qnodes/test_qnode_qubit.py +++ /dev/null @@ -1,710 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the PennyLane :class:`~.QubitQNode` class. -""" -import pytest -import numpy as np - -import pennylane as qml -from pennylane._device import Device -from pennylane.operation import CVObservable -from pennylane.qnodes.base import QuantumFunctionError -from pennylane.qnodes.qubit import QubitQNode -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - -thetas = np.linspace(-2*np.pi, 2*np.pi, 8) - - -@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, '_capabilities', {"model": "qubit"}) - 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) - - -class TestBestMethod: - """Test different flows of _best_method""" - - def test_no_following_observable(self, operable_mock_device_2_wires): - """Test that the gradient is 0 if no observables succeed""" - - def circuit(x): - qml.RX(x, wires=[1]) - return qml.expval(qml.PauliZ(0)) - - q = QubitQNode(circuit, operable_mock_device_2_wires) - q._construct([1.0], {}) - assert q.par_to_grad_method == {0: "0"} - - def test_param_unused(self, operable_mock_device_2_wires): - """Test that the gradient is 0 of an unused parameter""" - - def circuit(x, y): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - q = QubitQNode(circuit, operable_mock_device_2_wires) - q._construct([1.0, 1.0], {}) - assert q.par_to_grad_method == {0: "A", 1: "0"} - - def test_not_differentiable(self, operable_mock_device_2_wires): - """Test that an operation with grad_method=None is marked as - non-differentiable""" - - def circuit(x): - qml.BasisState(x, wires=[1]) - return qml.expval(qml.PauliZ(0)) - - q = QubitQNode(circuit, operable_mock_device_2_wires) - q._construct([np.array([1.0])], {}) - assert q.par_to_grad_method == {0: None} - - -class TestExpectationJacobian: - """Jacobian integration tests for qubit expectations.""" - - @pytest.mark.parametrize("mult", [1, -2, 1.623, -0.051, 0]) # intergers, floats, zero - def test_parameter_multipliers(self, mult, tol): - """Test that various types and values of scalar multipliers for differentiable - qfunc parameters yield the correct gradients.""" - - def circuit(x): - qml.RY(mult * x, wires=[0]) - return qml.expval(qml.PauliX(0)) - - dev = qml.device("default.qubit", wires=1) - q = QubitQNode(circuit, dev) - - par = [0.1] - - # gradients - exact = mult * np.cos(mult * np.array([par])) - grad_F = q.jacobian(par, method="F") - grad_A = q.jacobian(par, method="A") - - # different methods must agree - assert grad_F == pytest.approx(exact, abs=tol) - assert grad_A == pytest.approx(exact, abs=tol) - - @pytest.mark.parametrize("reused_p", thetas ** 3 / 19) - @pytest.mark.parametrize("other_p", thetas ** 2 / 1) - def test_fanout_multiple_params(self, reused_p, other_p, tol): - """Tests that the correct gradient is computed for qnodes which - use the same parameter in multiple gates.""" - - from gate_data import Rotx as Rx, Roty as Ry, Rotz as Rz - - def expZ(state): - return np.abs(state[0]) ** 2 - np.abs(state[1]) ** 2 - - extra_param = 0.31 - def circuit(reused_param, other_param): - qml.RX(extra_param, wires=[0]) - qml.RY(reused_param, wires=[0]) - qml.RZ(other_param, wires=[0]) - qml.RX(reused_param, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=1) - f = QubitQNode(circuit, dev) - zero_state = np.array([1., 0.]) - - # analytic gradient - grad_A = f.jacobian([reused_p, other_p]) - - # manual gradient - grad_true0 = (expZ(Rx(reused_p) @ Rz(other_p) @ Ry(reused_p + np.pi / 2) @ Rx(extra_param) @ zero_state) \ - -expZ(Rx(reused_p) @ Rz(other_p) @ Ry(reused_p - np.pi / 2) @ Rx(extra_param) @ zero_state)) / 2 - grad_true1 = (expZ(Rx(reused_p + np.pi / 2) @ Rz(other_p) @ Ry(reused_p) @ Rx(extra_param) @ zero_state) \ - -expZ(Rx(reused_p - np.pi / 2) @ Rz(other_p) @ Ry(reused_p) @ Rx(extra_param) @ zero_state)) / 2 - grad_true = grad_true0 + grad_true1 # product rule - - assert grad_A[0, 0] == pytest.approx(grad_true, abs=tol) - - @pytest.mark.parametrize("shape", [(8,), (8, 1), (4, 2), (2, 2, 2), (2, 1, 2, 1, 2)]) - def test_multidim_array_parameter(self, shape, tol): - """Tests that arguments which are multidimensional arrays are - properly evaluated and differentiated in QubitQNodes.""" - - n = np.prod(shape) - base_array = np.linspace(-1.0, 1.0, n) - multidim_array = np.reshape(base_array, shape) - - def circuit(w): - for k in range(n): - qml.RX(w[np.unravel_index(k, shape)], wires=k) # base_array[k] - return tuple(qml.expval(qml.PauliZ(idx)) for idx in range(n)) - - dev = qml.device("default.qubit", wires=n) - circuit = QubitQNode(circuit, dev) - - # circuit evaluations - circuit_output = circuit(multidim_array) - expected_output = np.cos(base_array) - assert circuit_output == pytest.approx(expected_output, abs=tol) - - # circuit jacobians - circuit_jacobian = circuit.jacobian([multidim_array]) - expected_jacobian = -np.diag(np.sin(base_array)) - assert circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - def test_gradient_gate_with_multiple_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 = QubitQNode(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.par_to_grad_method == {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 grad_A == pytest.approx(grad_F, abs=tol) - - def test_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 = QubitQNode(qf, dev) - grad_A = q.jacobian(par, method="A") - grad_F = q.jacobian(par, method="F") - - # the different methods agree - assert grad_A == pytest.approx(grad_F, abs=tol) - - def test_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 = QubitQNode(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.par_to_grad_method == {0: "A", 1: "F"} - # the different methods agree - assert grad == pytest.approx(grad_F, abs=tol) - - def test_keywordarg_not_differentiated(self, tol): - """Tests that qnodes do not differentiate w.r.t. keyword arguments.""" - par = np.array([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 = QubitQNode(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 = QubitQNode(circuit2, dev) - - res1 = circuit1.jacobian([par]) - res2 = circuit2.jacobian([par]) - assert res1 == pytest.approx(res2, abs=tol) - - 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 = QubitQNode(circuit1, dev) - - vals = np.array([np.pi, np.pi / 2, np.pi / 3]) - circuit_output = circuit1(*vals) - expected_output = np.cos(vals) - assert circuit_output == pytest.approx(expected_output, abs=tol) - - # circuit jacobians - circuit_jacobian = circuit1.jacobian(vals) - expected_jacobian = -np.diag(np.sin(vals)) - assert circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - 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 = QubitQNode(circuit2, dev) - - a = 0.7418 - b = -5.0 - circuit_output = circuit2(a, b) - expected_output = np.cos(a) - assert circuit_output == pytest.approx(expected_output, abs=tol) - - # circuit jacobians - circuit_jacobian = circuit2.jacobian([a, b]) - expected_jacobian = np.array([[-np.sin(a), 0]]) - assert circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - 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 = QubitQNode(circuit3, dev) - - a = 0.7418 - b = -5.0 - circuit_output = circuit3(a, b) - expected_output = np.cos(b) - assert circuit_output == pytest.approx(expected_output, abs=tol) - - # circuit jacobians - circuit_jacobian = circuit3.jacobian([a, b]) - expected_jacobian = np.array([[0, -np.sin(b)]]) - assert circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - 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 = QubitQNode(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 circuit_output == pytest.approx(expected_output, abs=tol) - - # 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 circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - 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 = QubitQNode(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 circuit_output == pytest.approx(expected_output, abs=tol) - - # 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 circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - def test_array_parameters_evaluate(self, tol): - """Tests that array parameters gives same result as positional arguments.""" - a, b, c = 0.5, 0.54, 0.3 - dev = qml.device("default.qubit", wires=2) - - 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)) - - 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) - - circuit1 = QubitQNode(circuit1, dev) - circuit2 = QubitQNode(circuit2, dev) - circuit3 = QubitQNode(circuit3, dev) - - 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 positional_res == pytest.approx(array_res, abs=tol) - assert positional_grad == pytest.approx(array_grad, abs=tol) - - list_res = circuit2(a, [b, c]) - list_grad = circuit2.jacobian([a, [b, c]]) - - assert positional_res == pytest.approx(list_res, abs=tol) - assert positional_grad == pytest.approx(list_grad, abs=tol) - - 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 positional_res == pytest.approx(array_res, abs=tol) - assert positional_grad == pytest.approx(array_grad, abs=tol) - - @pytest.mark.parametrize('theta', thetas) - @pytest.mark.parametrize('G', [qml.ops.RX, qml.ops.RY, qml.ops.RZ]) - def test_pauli_rotation_gradient(self, G, theta, tol): - """Tests that the automatic gradients of Pauli rotations are correct.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=1) - circuit = QubitQNode(circuit, dev) - - autograd_val = circuit.jacobian([theta]) - manualgrad_val = (circuit(theta + np.pi / 2) - circuit(theta - np.pi / 2)) / 2 - assert autograd_val == pytest.approx(manualgrad_val, abs=tol) - - @pytest.mark.parametrize('theta', thetas) - def test_Rot_gradient(self, theta, tol): - """Tests that the automatic gradient of a arbitrary Euler-angle-parameterized gate is correct.""" - - def circuit(x,y,z): - qml.Rot(x,y,z, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=1) - circuit = QubitQNode(circuit, dev) - eye = np.eye(3) - - angle_inputs = np.array([theta, theta ** 3, np.sqrt(2) * theta]) - autograd_val = circuit.jacobian(angle_inputs) - manualgrad_val = np.zeros((1,3)) - - for idx in range(3): - onehot_idx = eye[idx] - param1 = angle_inputs + np.pi / 2 * onehot_idx - param2 = angle_inputs - np.pi / 2 * onehot_idx - manualgrad_val[0, idx] = (circuit(*param1) - circuit(*param2)) / 2 - - assert autograd_val == pytest.approx(manualgrad_val, abs=tol) - - def test_controlled_RX_gradient(self, tol): - """Test gradient of controlled RX gate""" - dev = qml.device("default.qubit", wires=2) - - def circuit(x): - qml.PauliX(wires=0) - qml.CRX(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit = QubitQNode(circuit, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def circuit1(x): - qml.RX(x, wires=0) - qml.CRX(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit1 = QubitQNode(circuit1, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def test_controlled_RY_gradient(self, tol): - """Test gradient of controlled RY gate""" - dev = qml.device("default.qubit", wires=2) - - def circuit(x): - qml.PauliX(wires=0) - qml.CRY(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit = QubitQNode(circuit, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def circuit1(x): - qml.RX(x, wires=0) - qml.CRY(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit1 = QubitQNode(circuit1, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def test_controlled_RZ_gradient(self, tol): - """Test gradient of controlled RZ gate""" - dev = qml.device("default.qubit", wires=2) - - def circuit(x): - qml.PauliX(wires=0) - qml.CRZ(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit = QubitQNode(circuit, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def circuit1(x): - qml.RX(x, wires=0) - qml.CRZ(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit1 = QubitQNode(circuit1, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - -class TestVarianceJacobian: - """Variance analytic jacobian integration tests.""" - - def test_involutory_variance(self, tol): - """Tests qubit observable that are involutory""" - def circuit(a): - qml.RX(a, wires=0) - return qml.var(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=1) - circuit = QubitQNode(circuit, dev) - - a = 0.54 - var = circuit(a) - expected = 1 - np.cos(a) ** 2 - assert var == pytest.approx(expected, abs=tol) - - # circuit jacobians - gradA = circuit.jacobian([a], method="A") - gradF = circuit.jacobian([a], method="F") - expected = 2 * np.sin(a) * np.cos(a) - assert gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def test_non_involutory_variance(self, tol): - """Tests a qubit Hermitian observable that is not involutory""" - A = np.array([[4, -1 + 6j], [-1 - 6j, 2]]) - - def circuit(a): - qml.RX(a, wires=0) - return qml.var(qml.Hermitian(A, 0)) - - dev = qml.device("default.qubit", wires=1) - circuit = QubitQNode(circuit, dev) - - a = 0.54 - var = circuit(a) - expected = (39 / 2) - 6 * np.sin(2 * a) + (35 / 2) * np.cos(2 * a) - assert var == pytest.approx(expected, abs=tol) - - # 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 gradA == pytest.approx(expected, abs=tol) - assert gradF == pytest.approx(expected, abs=tol) - - def test_fanout(self, tol): - """Tests qubit observable with repeated parameters""" - def circuit(a): - qml.RX(a, wires=0) - qml.RY(a, wires=0) - return qml.var(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=2) - circuit = QubitQNode(circuit, dev) - - a = 0.54 - var = circuit(a) - expected = 0.5 * np.sin(a) ** 2 * (np.cos(2 * a) + 3) - assert var == pytest.approx(expected, abs=tol) - - # circuit jacobians - gradA = circuit.jacobian([a], method="A") - gradF = circuit.jacobian([a], method="F") - expected = 4 * np.sin(a) * np.cos(a) ** 3 - assert gradA == pytest.approx(expected, abs=tol) - assert gradF == pytest.approx(expected, abs=tol) - - def test_expval_and_variance(self, tol): - """Test that the qnode works for a combination of expectation - values and variances""" - dev = qml.device("default.qubit", wires=3) - - 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)) - - circuit = QubitQNode(circuit, dev) - - 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 var == pytest.approx(expected, abs=tol) - - # # 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) diff --git a/tests/qnodes/test_qnode_reversible.py b/tests/qnodes/test_qnode_reversible.py deleted file mode 100644 index c0dcb1dc7cb..00000000000 --- a/tests/qnodes/test_qnode_reversible.py +++ /dev/null @@ -1,698 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the PennyLane :class:`~.ReversibleQNode` class. -""" -import pytest -import numpy as np - -import pennylane as qml -from pennylane.qnodes.rev import ReversibleQNode -pytestmark = pytest.mark.usefixtures("non_tape_mode_only") - -thetas = np.linspace(-2 * np.pi, 2 * np.pi, 8) - - -class TestExpectationJacobian: - """Jacobian integration tests for qubit expectations.""" - - @pytest.mark.parametrize("mult", [1, -2, 1.623, -0.051, 0]) # intergers, floats, zero - def test_parameter_multipliers(self, mult, tol): - """Test that various types and values of scalar multipliers for differentiable - qfunc parameters yield the correct gradients.""" - - def circuit(x): - qml.RY(mult * x, wires=[0]) - return qml.expval(qml.PauliX(0)) - - dev = qml.device("default.qubit", wires=1) - q = ReversibleQNode(circuit, dev) - - par = [0.1] - - # gradients - exact = mult * np.cos(mult * np.array([par])) - grad_F = q.jacobian(par, method="F") - grad_A = q.jacobian(par, method="A") - - # different methods must agree - assert grad_F == pytest.approx(exact, abs=tol) - assert grad_A == pytest.approx(exact, abs=tol) - - @pytest.mark.parametrize("reused_p", thetas ** 3 / 19) - @pytest.mark.parametrize("other_p", thetas ** 2 / 1) - def test_fanout_multiple_params(self, reused_p, other_p, tol): - """Tests that the correct gradient is computed for qnodes which - use the same parameter in multiple gates.""" - - from gate_data import Rotx as Rx, Roty as Ry, Rotz as Rz - - def expZ(state): - return np.abs(state[0]) ** 2 - np.abs(state[1]) ** 2 - - extra_param = 0.31 - - def circuit(reused_param, other_param): - qml.RX(extra_param, wires=[0]) - qml.RY(reused_param, wires=[0]) - qml.RZ(other_param, wires=[0]) - qml.RX(reused_param, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=1) - f = ReversibleQNode(circuit, dev) - zero_state = np.array([1.0, 0.0]) - - # analytic gradient - grad_A = f.jacobian([reused_p, other_p]) - - # manual gradient - grad_true0 = ( - expZ( - Rx(reused_p) @ Rz(other_p) @ Ry(reused_p + np.pi / 2) @ Rx(extra_param) @ zero_state - ) - - expZ( - Rx(reused_p) @ Rz(other_p) @ Ry(reused_p - np.pi / 2) @ Rx(extra_param) @ zero_state - ) - ) / 2 - grad_true1 = ( - expZ( - Rx(reused_p + np.pi / 2) @ Rz(other_p) @ Ry(reused_p) @ Rx(extra_param) @ zero_state - ) - - expZ( - Rx(reused_p - np.pi / 2) @ Rz(other_p) @ Ry(reused_p) @ Rx(extra_param) @ zero_state - ) - ) / 2 - grad_true = grad_true0 + grad_true1 # product rule - - assert grad_A[0, 0] == pytest.approx(grad_true, abs=tol) - - @pytest.mark.parametrize("shape", [(8,), (8, 1), (4, 2), (2, 2, 2), (2, 1, 2, 1, 2)]) - def test_multidim_array_parameter(self, shape, tol): - """Tests that arguments which are multidimensional arrays are - properly evaluated and differentiated in ReversibleQNodes.""" - - n = np.prod(shape) - base_array = np.linspace(-1.0, 1.0, n) - multidim_array = np.reshape(base_array, shape) - - def circuit(w): - for k in range(n): - qml.RX(w[np.unravel_index(k, shape)], wires=k) # base_array[k] - return tuple(qml.expval(qml.PauliZ(idx)) for idx in range(n)) - - dev = qml.device("default.qubit", wires=n) - circuit = ReversibleQNode(circuit, dev) - - # circuit evaluations - circuit_output = circuit(multidim_array) - expected_output = np.cos(base_array) - assert circuit_output == pytest.approx(expected_output, abs=tol) - - # circuit jacobians - circuit_jacobian = circuit.jacobian([multidim_array]) - expected_jacobian = -np.diag(np.sin(base_array)) - assert circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - def test_gradient_gate_with_multiple_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 = ReversibleQNode(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.par_to_grad_method == {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 grad_A == pytest.approx(grad_F, abs=tol) - - def test_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 = ReversibleQNode(qf, dev) - grad_A = q.jacobian(par, method="A") - grad_F = q.jacobian(par, method="F") - - # the different methods agree - assert grad_A == pytest.approx(grad_F, abs=tol) - - def test_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 = ReversibleQNode(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.par_to_grad_method == {0: "A", 1: "F"} - # the different methods agree - assert grad == pytest.approx(grad_F, abs=tol) - - def test_keywordarg_not_differentiated(self, tol): - """Tests that qnodes do not differentiate w.r.t. keyword arguments.""" - par = np.array([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 = ReversibleQNode(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 = ReversibleQNode(circuit2, dev) - - res1 = circuit1.jacobian([par]) - res2 = circuit2.jacobian([par]) - assert res1 == pytest.approx(res2, abs=tol) - - 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 = ReversibleQNode(circuit1, dev) - - vals = np.array([np.pi, np.pi / 2, np.pi / 3]) - circuit_output = circuit1(*vals) - expected_output = np.cos(vals) - assert circuit_output == pytest.approx(expected_output, abs=tol) - - # circuit jacobians - circuit_jacobian = circuit1.jacobian(vals) - expected_jacobian = -np.diag(np.sin(vals)) - assert circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - 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 = ReversibleQNode(circuit2, dev) - - a = 0.7418 - b = -5.0 - circuit_output = circuit2(a, b) - expected_output = np.cos(a) - assert circuit_output == pytest.approx(expected_output, abs=tol) - - # circuit jacobians - circuit_jacobian = circuit2.jacobian([a, b]) - expected_jacobian = np.array([[-np.sin(a), 0]]) - assert circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - 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 = ReversibleQNode(circuit3, dev) - - a = 0.7418 - b = -5.0 - circuit_output = circuit3(a, b) - expected_output = np.cos(b) - assert circuit_output == pytest.approx(expected_output, abs=tol) - - # circuit jacobians - circuit_jacobian = circuit3.jacobian([a, b]) - expected_jacobian = np.array([[0, -np.sin(b)]]) - assert circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - 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 = ReversibleQNode(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 circuit_output == pytest.approx(expected_output, abs=tol) - - # 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 circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - 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 = ReversibleQNode(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 circuit_output == pytest.approx(expected_output, abs=tol) - - # 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 circuit_jacobian == pytest.approx(expected_jacobian, abs=tol) - - def test_array_parameters_evaluate(self, tol): - """Tests that array parameters gives same result as positional arguments.""" - a, b, c = 0.5, 0.54, 0.3 - dev = qml.device("default.qubit", wires=2) - - 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)) - - 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) - - circuit1 = ReversibleQNode(circuit1, dev) - circuit2 = ReversibleQNode(circuit2, dev) - circuit3 = ReversibleQNode(circuit3, dev) - - 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 positional_res == pytest.approx(array_res, abs=tol) - assert positional_grad == pytest.approx(array_grad, abs=tol) - - list_res = circuit2(a, [b, c]) - list_grad = circuit2.jacobian([a, [b, c]]) - - assert positional_res == pytest.approx(list_res, abs=tol) - assert positional_grad == pytest.approx(list_grad, abs=tol) - - 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 positional_res == pytest.approx(array_res, abs=tol) - assert positional_grad == pytest.approx(array_grad, abs=tol) - - @pytest.mark.parametrize("theta", thetas) - @pytest.mark.parametrize("G", [qml.ops.RX, qml.ops.RY, qml.ops.RZ]) - def test_pauli_rotation_gradient(self, G, theta, tol): - """Tests that the automatic gradients of Pauli rotations are correct.""" - - def circuit(x): - qml.RX(x, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=1) - circuit = ReversibleQNode(circuit, dev) - - autograd_val = circuit.jacobian([theta]) - manualgrad_val = (circuit(theta + np.pi / 2) - circuit(theta - np.pi / 2)) / 2 - assert autograd_val == pytest.approx(manualgrad_val, abs=tol) - - @pytest.mark.parametrize("theta", thetas) - def test_Rot_gradient(self, theta, tol): - """Tests that the automatic gradient of a arbitrary Euler-angle-parameterized gate is correct.""" - - def circuit(x, y, z): - qml.Rot(x, y, z, wires=[0]) - return qml.expval(qml.PauliZ(0)) - - dev = qml.device("default.qubit", wires=1) - circuit = ReversibleQNode(circuit, dev) - eye = np.eye(3) - - angle_inputs = np.array([theta, theta ** 3, np.sqrt(2) * theta]) - autograd_val = circuit.jacobian(angle_inputs) - manualgrad_val = np.zeros((1, 3)) - - for idx in range(3): - onehot_idx = eye[idx] - param1 = angle_inputs + np.pi / 2 * onehot_idx - param2 = angle_inputs - np.pi / 2 * onehot_idx - manualgrad_val[0, idx] = (circuit(*param1) - circuit(*param2)) / 2 - - assert autograd_val == pytest.approx(manualgrad_val, abs=tol) - - @pytest.mark.parametrize("op, name", [(qml.CRX, "CRX"), (qml.CRY, "CRY"), (qml.CRZ, "CRZ")]) - def test_controlled_rotation_gates_exception(self, op, name): - """Tests that an exception is raised when a controlled - rotation gate is used with the ReversibleQNode.""" - # remove this test when this support is added - dev = qml.device("default.qubit", wires=2) - - def circuit(x): - qml.PauliX(wires=0) - op(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit = ReversibleQNode(circuit, dev) - with pytest.raises(ValueError, match="The {} gate is not currently supported".format(name)): - circuit.jacobian([0.542]) - - def test_phaseshift_exception(self): - """Tests that an exception is raised when a PhaseShift gate - is used with the ReversibleQNode.""" - # remove this test when this support is added - dev = qml.device("default.qubit", wires=1) - - def circuit(x): - qml.PauliX(wires=0) - qml.PhaseShift(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - circuit = ReversibleQNode(circuit, dev) - - with pytest.raises(ValueError, match="The PhaseShift gate is not currently supported"): - circuit.jacobian([0.542]) - - @pytest.mark.xfail( - reason="The ReversibleQNode does not support gradients of the PhaseShift gate." - ) - def test_phaseshift_gradient(self, tol): - """Test gradient of PhaseShift gate""" - dev = qml.device("default.qubit", wires=1) - - def circuit(x): - qml.Hadamard(wires=0) - qml.PhaseShift(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - circuit = ReversibleQNode(circuit, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def circuit1(x): - qml.Hadamard(x, wires=0) - qml.PhaseShift(x, wires=0) - return qml.expval(qml.PauliY(0)) - - circuit1 = ReversibleQNode(circuit1, dev) - - b = 0.123 # gradient is -sin(b) - - # 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - @pytest.mark.xfail( - reason="The ReversibleQNode does not support gradients of controlled rotations" - ) - def test_controlled_RX_gradient(self, tol): - """Test gradient of controlled RX gate""" - dev = qml.device("default.qubit", wires=2) - - def circuit(x): - qml.PauliX(wires=0) - qml.CRX(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit = ReversibleQNode(circuit, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def circuit1(x): - qml.RX(x, wires=0) - qml.CRX(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit1 = ReversibleQNode(circuit1, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - @pytest.mark.xfail( - reason="The ReversibleQNode does not support gradients of controlled rotations" - ) - def test_controlled_RY_gradient(self, tol): - """Test gradient of controlled RY gate""" - dev = qml.device("default.qubit", wires=2) - - def circuit(x): - qml.PauliX(wires=0) - qml.CRY(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit = ReversibleQNode(circuit, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def circuit1(x): - qml.RX(x, wires=0) - qml.CRY(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit1 = ReversibleQNode(circuit1, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - @pytest.mark.xfail( - reason="The ReversibleQNode does not support gradients of controlled rotations" - ) - def test_controlled_RZ_gradient(self, tol): - """Test gradient of controlled RZ gate""" - dev = qml.device("default.qubit", wires=2) - - def circuit(x): - qml.PauliX(wires=0) - qml.CRZ(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit = ReversibleQNode(circuit, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - def circuit1(x): - qml.RX(x, wires=0) - qml.CRZ(x, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - circuit1 = ReversibleQNode(circuit1, dev) - - 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 gradF == pytest.approx(expected, abs=tol) - assert gradA == pytest.approx(expected, abs=tol) - - -class TestIntegration: - """Integration tests for ReversibleQNode.""" - - def test_incapable_device_exception(self, monkeypatch): - """Test that an exception is raised if the reversible diff_method - is specified for a device which does not have reversible capability.""" - dev = qml.device("default.qubit", wires=1) - - # overwrite capabilities - capabilities = dev.capabilities().copy() - capabilities["supports_reversible_diff"] = False - monkeypatch.setattr(dev, 'capabilities', lambda: capabilities) - - def circuit(a): - qml.RX(a, wires=0) - return qml.expval(qml.PauliZ(wires=0)) - - with pytest.raises(ValueError, match="Reversible differentiation method not supported"): - ReversibleQNode(circuit, dev) - - -class TestHelperFunctions: - """Tests for additional helper functions.""" - - one_qubit_vec1 = np.array([1, 1]) - one_qubit_vec2 = np.array([1, 1j]) - two_qubit_vec = np.array([1, 1, 1, -1]).reshape([2, 2]) - single_qubit_obs1 = qml.PauliZ(0) - single_qubit_obs2 = qml.PauliY(0) - two_qubit_obs = qml.Hermitian(np.eye(4), wires=[0, 1]) - - @pytest.mark.parametrize( - "wires, vec1, obs, vec2, expected", - [ - (1, one_qubit_vec1, single_qubit_obs1, one_qubit_vec1, 0), - (1, one_qubit_vec2, single_qubit_obs1, one_qubit_vec2, 0), - (1, one_qubit_vec1, single_qubit_obs1, one_qubit_vec2, 1 - 1j), - (1, one_qubit_vec2, single_qubit_obs1, one_qubit_vec1, 1 + 1j), - (1, one_qubit_vec1, single_qubit_obs2, one_qubit_vec1, 0), - (1, one_qubit_vec2, single_qubit_obs2, one_qubit_vec2, 2), - (1, one_qubit_vec1, single_qubit_obs2, one_qubit_vec2, 1 + 1j), - (1, one_qubit_vec2, single_qubit_obs2, one_qubit_vec1, 1 - 1j), - (2, two_qubit_vec, single_qubit_obs1, two_qubit_vec, 0), - (2, two_qubit_vec, single_qubit_obs2, two_qubit_vec, 0), - (2, two_qubit_vec, two_qubit_obs, two_qubit_vec, 4), - ], - ) - def test_matrix_elem(self, wires, vec1, obs, vec2, expected): - """Tests for the helper function _matrix_elem""" - dev = qml.device("default.qubit", wires=wires) - mock_circuit = lambda: None - qnode = ReversibleQNode(mock_circuit, dev) - res = qnode._matrix_elem(vec1, obs, vec2) - assert res == expected diff --git a/tests/tape/tapes/test_cv_param_shift.py b/tests/tape/test_cv_param_shift.py similarity index 100% rename from tests/tape/tapes/test_cv_param_shift.py rename to tests/tape/test_cv_param_shift.py diff --git a/tests/tape/tapes/test_jacobian_tape.py b/tests/tape/test_jacobian_tape.py similarity index 100% rename from tests/tape/tapes/test_jacobian_tape.py rename to tests/tape/test_jacobian_tape.py diff --git a/tests/tape/tapes/test_qnode.py b/tests/tape/test_qnode.py similarity index 100% rename from tests/tape/tapes/test_qnode.py rename to tests/tape/test_qnode.py diff --git a/tests/tape/tapes/test_qubit_param_shift.py b/tests/tape/test_qubit_param_shift.py similarity index 100% rename from tests/tape/tapes/test_qubit_param_shift.py rename to tests/tape/test_qubit_param_shift.py diff --git a/tests/tape/tapes/test_reversible.py b/tests/tape/test_reversible.py similarity index 100% rename from tests/tape/tapes/test_reversible.py rename to tests/tape/test_reversible.py diff --git a/tests/tape/tapes/test_tape.py b/tests/tape/test_tape.py similarity index 100% rename from tests/tape/tapes/test_tape.py rename to tests/tape/test_tape.py diff --git a/tests/tape/test_tape_measure.py b/tests/tape/test_tape_measure.py deleted file mode 100644 index cec40bd7010..00000000000 --- a/tests/tape/test_tape_measure.py +++ /dev/null @@ -1,789 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for the measure module""" -import contextlib -import pytest -import numpy as np - -import pennylane as qml -from pennylane import QuantumFunctionError -from pennylane.devices import DefaultQubit - -# Beta imports -from pennylane.tape import qnode -from pennylane.tape.queuing import AnnotatedQueue -from pennylane.tape.operation import mock_operations -from pennylane.tape.measure import ( - expval, - var, - sample, - probs, - state, - density_matrix, - Expectation, - Sample, - State, - Variance, - Probability, - MeasurementProcess, -) - - -@pytest.fixture(autouse=True) -def patch_operator(): - with contextlib.ExitStack() as stack: - for mock in mock_operations(): - stack.enter_context(mock) - yield - - -@pytest.mark.parametrize( - "stat_func,return_type", [(expval, Expectation), (var, Variance), (sample, Sample)] -) -class TestBetaStatistics: - """Tests for annotating the return types of the statistics functions""" - - @pytest.mark.parametrize( - "op", - [qml.PauliX, qml.PauliY, qml.PauliZ, qml.Hadamard, qml.Identity], - ) - def test_annotating_obs_return_type(self, stat_func, return_type, op): - """Test that the return_type related info is updated for a - measurement""" - with AnnotatedQueue() as q: - A = op(0) - stat_func(A) - - assert q.queue[:-1] == [A] - meas_proc = q.queue[-1] - assert isinstance(meas_proc, MeasurementProcess) - assert meas_proc.return_type == return_type - - assert q._get_info(A) == {"owner": meas_proc} - assert q._get_info(meas_proc) == {"owns": (A)} - - def test_annotating_tensor_hermitian(self, stat_func, return_type): - """Test that the return_type related info is updated for a measurement - when called for an Hermitian observable""" - - mx = np.array([[1, 0], [0, 1]]) - - with AnnotatedQueue() as q: - Herm = qml.Hermitian(mx, wires=[1]) - stat_func(Herm) - - assert q.queue[:-1] == [Herm] - meas_proc = q.queue[-1] - assert isinstance(meas_proc, MeasurementProcess) - assert meas_proc.return_type == return_type - - assert q._get_info(Herm) == {"owner": meas_proc} - assert q._get_info(meas_proc) == {"owns": (Herm)} - - @pytest.mark.parametrize( - "op1,op2", - [ - (qml.PauliY, qml.PauliX), - (qml.Hadamard, qml.Hadamard), - (qml.PauliY, qml.Identity), - (qml.Identity, qml.Identity), - ], - ) - def test_annotating_tensor_return_type(self, op1, op2, stat_func, return_type): - """Test that the return_type related info is updated for a measurement - when called for an Tensor observable""" - with AnnotatedQueue() as q: - A = op1(0) - B = op2(1) - tensor_op = A @ B - stat_func(tensor_op) - - assert q.queue[:-1] == [A, B, tensor_op] - meas_proc = q.queue[-1] - assert isinstance(meas_proc, MeasurementProcess) - assert meas_proc.return_type == return_type - - assert q._get_info(A) == {"owner": tensor_op} - assert q._get_info(B) == {"owner": tensor_op} - assert q._get_info(tensor_op) == {"owns": (A, B), "owner": meas_proc} - - @pytest.mark.parametrize( - "op1,op2", - [ - (qml.PauliY, qml.PauliX), - (qml.Hadamard, qml.Hadamard), - (qml.PauliY, qml.Identity), - (qml.Identity, qml.Identity), - ], - ) - def test_queueing_tensor_observable(self, op1, op2, stat_func, return_type): - """Test that if the constituent components of a tensor operation are not - found in the queue for annotation, that they are queued first and then annotated.""" - A = op1(0) - B = op2(1) - - with AnnotatedQueue() as q: - tensor_op = A @ B - stat_func(tensor_op) - - assert q.queue[:-1] == [A, B, tensor_op] - meas_proc = q.queue[-1] - assert isinstance(meas_proc, MeasurementProcess) - assert meas_proc.return_type == return_type - - assert q._get_info(A) == {"owner": tensor_op} - assert q._get_info(B) == {"owner": tensor_op} - assert q._get_info(tensor_op) == {"owns": (A, B), "owner": meas_proc} - - -@pytest.mark.parametrize("stat_func", [expval, var, sample]) -class TestBetaStatisticsError: - """Tests for errors arising for the beta statistics functions""" - - def test_not_an_observable(self, stat_func): - """Test that a QuantumFunctionError is raised if the provided - argument is not an observable""" - dev = qml.device("default.qubit", wires=2) - - @qnode(dev) - def circuit(): - qml.RX(0.52, wires=0) - return stat_func(qml.CNOT(wires=[0, 1])) - - with pytest.raises(QuantumFunctionError, match="CNOT is not an observable"): - res = circuit() - - -class TestBetaProbs: - """Tests for annotating the return types of the probs function""" - - @pytest.mark.parametrize("wires", [[0], [0, 1], [1, 0, 2]]) - def test_annotating_probs(self, wires): - with AnnotatedQueue() as q: - probs(wires) - - assert len(q.queue) == 1 - - meas_proc = q.queue[0] - assert isinstance(meas_proc, MeasurementProcess) - assert meas_proc.return_type == Probability - - -class TestProperties: - """Test for the properties""" - - def test_wires_match_observable(self): - """Test that the wires of the measurement process - match an internal observable""" - obs = qml.Hermitian(np.diag([1, 2, 3]), wires=["a", "b", "c"]) - m = MeasurementProcess(Expectation, obs=obs) - - assert np.all(m.wires == obs.wires) - - def test_eigvals_match_observable(self): - """Test that the eigenvalues of the measurement process - match an internal observable""" - obs = qml.Hermitian(np.diag([1, 2, 3]), wires=[0, 1, 2]) - m = MeasurementProcess(Expectation, obs=obs) - - assert np.all(m.eigvals == np.array([1, 2, 3])) - - # changing the observable data should be reflected - obs.data = [np.diag([5, 6, 7])] - assert np.all(m.eigvals == np.array([5, 6, 7])) - - def test_error_obs_and_eigvals(self): - """Test that providing both eigenvalues and an observable - results in an error""" - obs = qml.Hermitian(np.diag([1, 2, 3]), wires=[0, 1, 2]) - - with pytest.raises(ValueError, match="Cannot set the eigenvalues"): - MeasurementProcess(Expectation, obs=obs, eigvals=[0, 1]) - - def test_error_obs_and_wires(self): - """Test that providing both wires and an observable - results in an error""" - obs = qml.Hermitian(np.diag([1, 2, 3]), wires=[0, 1, 2]) - - with pytest.raises(ValueError, match="Cannot set the wires"): - MeasurementProcess(Expectation, obs=obs, wires=qml.wires.Wires([0, 1])) - - def test_observable_with_no_eigvals(self): - """An observable with no eigenvalues defined should cause - the eigvals property on the associated measurement process - to be None""" - obs = qml.NumberOperator(wires=0) - m = MeasurementProcess(Expectation, obs=obs) - assert m.eigvals is None - - def test_repr(self): - """Test the string representation of a MeasurementProcess.""" - m = MeasurementProcess(Expectation, obs=qml.PauliZ(wires="a") @ qml.PauliZ(wires="b")) - expected = "expval(PauliZ(wires=['a']) @ PauliZ(wires=['b']))" - assert str(m) == expected - - m = MeasurementProcess(Probability, obs=qml.PauliZ(wires="a")) - expected = "probs(PauliZ(wires=['a']))" - assert str(m) == expected - - -class TestExpansion: - """Test for measurement expansion""" - - def test_expand_pauli(self): - """Test the expansion of a Pauli observable""" - obs = qml.PauliX(0) @ qml.PauliY(1) - m = MeasurementProcess(Expectation, obs=obs) - tape = m.expand() - - assert len(tape.operations) == 4 - - assert tape.operations[0].name == "Hadamard" - assert tape.operations[0].wires.tolist() == [0] - - assert tape.operations[1].name == "PauliZ" - assert tape.operations[1].wires.tolist() == [1] - assert tape.operations[2].name == "S" - assert tape.operations[2].wires.tolist() == [1] - assert tape.operations[3].name == "Hadamard" - assert tape.operations[3].wires.tolist() == [1] - - assert len(tape.measurements) == 1 - assert tape.measurements[0].return_type is Expectation - assert tape.measurements[0].wires.tolist() == [0, 1] - assert np.all(tape.measurements[0].eigvals == np.array([1, -1, -1, 1])) - - def test_expand_hermitian(self, tol): - """Test the expansion of an hermitian observable""" - H = np.array([[1, 2], [2, 4]]) - obs = qml.Hermitian(H, wires=["a"]) - - m = MeasurementProcess(Expectation, obs=obs) - tape = m.expand() - - assert len(tape.operations) == 1 - - assert tape.operations[0].name == "QubitUnitary" - assert tape.operations[0].wires.tolist() == ["a"] - assert np.allclose( - tape.operations[0].parameters[0], - np.array([[-2, 1], [1, 2]]) / np.sqrt(5), - atol=tol, - rtol=0, - ) - - assert len(tape.measurements) == 1 - assert tape.measurements[0].return_type is Expectation - assert tape.measurements[0].wires.tolist() == ["a"] - assert np.all(tape.measurements[0].eigvals == np.array([0, 5])) - - def test_expand_no_observable(self): - """Check that an exception is raised if the measurement to - be expanded has no observable""" - m = MeasurementProcess(Probability, wires=qml.wires.Wires([0, 1])) - - with pytest.raises(NotImplementedError, match="Cannot expand"): - m.expand() - - -class TestState: - """Tests for the state function""" - - @pytest.mark.parametrize("wires", range(2, 5)) - def test_state_shape_and_dtype(self, wires): - """Test that the state is of correct size and dtype for a trivial circuit""" - - dev = qml.device("default.qubit", wires=wires) - - @qnode(dev) - def func(): - return state() - - state_val = func() - assert state_val.shape == (2 ** wires,) - assert state_val.dtype == np.complex128 - - def test_return_type_is_state(self): - """Test that the return type of the observable is State""" - - dev = qml.device("default.qubit", wires=1) - - @qnode(dev) - def func(): - qml.Hadamard(0) - return state() - - func() - obs = func.qtape.observables - assert len(obs) == 1 - assert obs[0].return_type is State - - @pytest.mark.parametrize("wires", range(2, 5)) - def test_state_correct_ghz(self, wires): - """Test that the correct state is returned when the circuit prepares a GHZ state""" - - dev = qml.device("default.qubit", wires=wires) - - @qnode(dev) - def func(): - qml.Hadamard(wires=0) - for i in range(wires - 1): - qml.CNOT(wires=[i, i + 1]) - return state() - - state_val = func() - assert np.allclose(np.sum(np.abs(state_val) ** 2), 1) - assert np.allclose(state_val[0], 1 / np.sqrt(2)) - assert np.allclose(state_val[-1], 1 / np.sqrt(2)) - - def test_return_with_other_types(self): - """Test that an exception is raised when a state is returned along with another return - type""" - - dev = qml.device("default.qubit", wires=2) - - @qnode(dev) - def func(): - qml.Hadamard(wires=0) - return state(), expval(qml.PauliZ(1)) - - with pytest.raises( - QuantumFunctionError, - match="The state or density matrix" - " cannot be returned in combination" - " with other return types", - ): - func() - - @pytest.mark.parametrize("wires", range(2, 5)) - def test_state_equal_to_dev_state(self, wires): - """Test that the returned state is equal to the one stored in dev.state for a template - circuit""" - - dev = qml.device("default.qubit", wires=wires) - - weights = qml.init.strong_ent_layers_uniform(3, wires) - - @qnode(dev) - def func(): - qml.templates.StronglyEntanglingLayers(weights, wires=range(wires)) - return state() - - state_val = func() - assert np.allclose(state_val, func.device.state) - - @pytest.mark.usefixtures("skip_if_no_tf_support") - def test_interface_tf(self, skip_if_no_tf_support): - """Test that the state correctly outputs in the tensorflow interface""" - import tensorflow as tf - - dev = qml.device("default.qubit", wires=4) - - @qnode(dev, interface="tf") - def func(): - for i in range(4): - qml.Hadamard(i) - return state() - - state_expected = 0.25 * tf.ones(16) - state_val = func() - - assert isinstance(state_val, tf.Tensor) - assert state_val.dtype == tf.complex128 - assert np.allclose(state_expected, state_val.numpy()) - assert state_val.shape == (16,) - - def test_interface_torch(self): - """Test that the state correctly outputs in the torch interface""" - torch = pytest.importorskip("torch", minversion="1.6") - - dev = qml.device("default.qubit", wires=4) - - @qnode(dev, interface="torch") - def func(): - for i in range(4): - qml.Hadamard(i) - return state() - - state_expected = 0.25 * torch.ones(16, dtype=torch.complex128) - state_val = func() - - assert isinstance(state_val, torch.Tensor) - assert state_val.dtype == torch.complex128 - assert torch.allclose(state_expected, state_val) - assert state_val.shape == (16,) - - def test_jacobian_not_supported(self): - """Test if an error is raised if the jacobian method is called via qml.grad""" - dev = qml.device("default.qubit", wires=4) - - @qnode(dev, diff_method="parameter-shift") - def func(x): - for i in range(4): - qml.RX(x, wires=i) - return state() - - d_func = qml.jacobian(func) - - with pytest.raises(ValueError, match="The jacobian method does not support"): - d_func(0.1) - - def test_no_state_capability(self, monkeypatch): - """Test if an error is raised for devices that are not capable of returning the state. - This is tested by changing the capability of default.qubit""" - dev = qml.device("default.qubit", wires=1) - capabilities = dev.capabilities().copy() - capabilities["returns_state"] = False - - @qnode(dev) - def func(): - return state() - - with monkeypatch.context() as m: - m.setattr(DefaultQubit, "capabilities", lambda *args, **kwargs: capabilities) - with pytest.raises(QuantumFunctionError, match="The current device is not capable"): - func() - - def test_state_not_supported(self, monkeypatch): - """Test if an error is raised for devices inheriting from the base Device class, - which do not currently support returning the state""" - dev = qml.device("default.gaussian", wires=1) - - @qnode(dev) - def func(): - return state() - - with pytest.raises(QuantumFunctionError, match="Returning the state is not supported"): - func() - - @pytest.mark.usefixtures("skip_if_no_tf_support") - @pytest.mark.parametrize( - "device", ["default.qubit", "default.qubit.tf", "default.qubit.autograd"] - ) - def test_devices(self, device, skip_if_no_tf_support): - """Test that the returned state is equal to the expected returned state for all of - PennyLane's built in statevector devices""" - - dev = qml.device(device, wires=4) - - @qnode(dev, diff_method="parameter-shift") - def func(): - for i in range(4): - qml.Hadamard(i) - return state() - - state_val = func() - state_expected = 0.25 * np.ones(16) - - assert np.allclose(state_val, state_expected) - assert np.allclose(state_val, dev.state) - - @pytest.mark.usefixtures("skip_if_no_tf_support") - def test_gradient_with_passthru_tf(self, skip_if_no_tf_support): - """Test that the gradient of the state is accessible when using default.qubit.tf with the - backprop diff_method.""" - import tensorflow as tf - - dev = qml.device("default.qubit.tf", wires=1) - - @qnode(dev, interface="tf", diff_method="backprop") - def func(x): - qml.RY(x, wires=0) - return state() - - x = tf.Variable(0.1, dtype=tf.complex128) - - with tf.GradientTape() as tape: - result = func(x) - - grad = tape.jacobian(result, x) - expected = tf.stack([-0.5 * tf.sin(x / 2), 0.5 * tf.cos(x / 2)]) - assert np.allclose(grad, expected) - - def test_gradient_with_passthru_autograd(self): - """Test that the gradient of the state is accessible when using default.qubit.autograd - with the backprop diff_method.""" - from pennylane import numpy as anp - - dev = qml.device("default.qubit.autograd", wires=1) - - @qnode(dev, interface="autograd", diff_method="backprop") - def func(x): - qml.RY(x, wires=0) - return state() - - x = anp.array(0.1, requires_grad=True) - - def loss_fn(x): - res = func(x) - return anp.real(res) # This errors without the real. Likely an issue with complex - # numbers in autograd - - d_loss_fn = qml.jacobian(loss_fn) - - grad = d_loss_fn(x) - expected = np.array([-0.5 * np.sin(x / 2), 0.5 * np.cos(x / 2)]) - assert np.allclose(grad, expected) - - @pytest.mark.parametrize("wires", [[0, 2, 3, 1], ["a", -1, "b", 1000]]) - def test_custom_wire_labels(self, wires): - """Test if an error is raised when custom wire labels are used""" - dev = qml.device("default.qubit", wires=wires) - - @qnode(dev, diff_method="parameter-shift") - def func(): - qml.Hadamard(wires=wires[0]) - for i in range(3): - qml.CNOT(wires=[wires[i], wires[i + 1]]) - return state() - - with pytest.raises(QuantumFunctionError, match="custom wire labels"): - func() - - -class TestDensityMatrix: - """Tests for the density matrix function""" - - @pytest.mark.parametrize("wires", range(2, 5)) - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) - def test_density_matrix_shape_and_dtype(self, dev_name, wires): - """Test that the density matrix is of correct size and dtype for a - trivial circuit""" - - dev = qml.device(dev_name, wires=wires) - - @qnode(dev) - def circuit(): - return density_matrix([0]) - - state_val = circuit() - - assert state_val.shape == (2, 2) - assert state_val.dtype == np.complex128 - - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) - def test_return_type_is_state(self, dev_name): - """Test that the return type of the observable is State""" - - dev = qml.device(dev_name, wires=2) - - @qnode(dev) - def func(): - qml.Hadamard(0) - return density_matrix(0) - - func() - obs = func.qtape.observables - assert len(obs) == 1 - assert obs[0].return_type is State - - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) - def test_correct_density_matrix_product_state_first(self, dev_name): - """Test that the correct density matrix is returned when - tracing out a product state""" - - dev = qml.device(dev_name, wires=2) - - @qnode(dev) - def func(): - qml.Hadamard(wires=1) - qml.PauliY(wires=0) - return density_matrix(0) - - density_first = func() - - assert np.allclose( - np.array([[0.0 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, 1.0 + 0.0j]]), density_first - ) - - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) - def test_correct_density_matrix_product_state_second(self, dev_name): - """Test that the correct density matrix is returned when - tracing out a product state""" - - dev = qml.device(dev_name, wires=2) - - @qnode(dev) - def func(): - qml.Hadamard(wires=1) - qml.PauliY(wires=0) - return density_matrix(1) - - density_second = func() - assert np.allclose( - np.array([[0.5 + 0.0j, 0.5 + 0.0j], [0.5 + 0.0j, 0.5 + 0.0j]]), density_second - ) - - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) - def test_correct_density_matrix_three_wires_first(self, dev_name): - """Test that the correct density matrix for an example with three wires""" - - dev = qml.device(dev_name, wires=3) - - @qnode(dev) - def func(): - qml.Hadamard(wires=1) - qml.PauliY(wires=0) - return density_matrix([0, 1]) - - density_full = func() - assert np.allclose( - np.array( - [ - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j, 0.5 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j, 0.5 + 0.0j], - ] - ), - density_full, - ) - - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) - def test_correct_density_matrix_three_wires_second(self, dev_name): - """Test that the correct density matrix for an example with three wires""" - - dev = qml.device(dev_name, wires=3) - - @qnode(dev) - def func(): - qml.Hadamard(0) - qml.Hadamard(1) - qml.CNOT(wires=[1, 2]) - return qml.density_matrix(wires=[1, 2]) - - density = func() - - assert np.allclose( - np.array( - [ - [ - [0.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j], - ] - ] - ), - density, - ) - - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) - def test_correct_density_matrix_mixed_state(self, dev_name): - """Test that the correct density matrix for an example with a mixed state""" - - dev = qml.device(dev_name, wires=2) - - @qnode(dev) - def func(): - qml.Hadamard(0) - qml.CNOT(wires=[0, 1]) - return qml.density_matrix(wires=[1]) - - density = func() - - assert np.allclose(np.array([[0.5 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, 0.5 + 0.0j]]), density) - - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) - def test_correct_density_matrix_all_wires(self, dev_name): - """Test that the correct density matrix is returned when all wires are given""" - - dev = qml.device(dev_name, wires=2) - - @qnode(dev) - def func(): - qml.Hadamard(0) - qml.CNOT(wires=[0, 1]) - return qml.density_matrix(wires=[0, 1]) - - density = func() - - assert np.allclose( - np.array( - [ - [0.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j], - ] - ), - density, - ) - - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) - def test_return_with_other_types(self, dev_name): - """Test that an exception is raised when a state is returned along with another return - type""" - - dev = qml.device(dev_name, wires=2) - - @qnode(dev) - def func(): - qml.Hadamard(wires=0) - return density_matrix(0), expval(qml.PauliZ(1)) - - with pytest.raises( - QuantumFunctionError, - match="The state or density matrix" - " cannot be returned in combination" - " with other return types", - ): - func() - - def test_no_state_capability(self, monkeypatch): - """Test if an error is raised for devices that are not capable of returning - the density matrix. This is tested by changing the capability of default.qubit""" - dev = qml.device("default.qubit", wires=2) - capabilities = dev.capabilities().copy() - capabilities["returns_state"] = False - - @qnode(dev) - def func(): - return density_matrix(0) - - with monkeypatch.context() as m: - m.setattr(DefaultQubit, "capabilities", lambda *args, **kwargs: capabilities) - with pytest.raises( - QuantumFunctionError, - match="The current device is not capable" " of returning the state", - ): - func() - - def test_density_matrix_not_supported(self): - """Test if an error is raised for devices inheriting from the base Device class, - which do not currently support returning the state""" - dev = qml.device("default.gaussian", wires=2) - - @qnode(dev) - def func(): - return density_matrix(0) - - with pytest.raises(QuantumFunctionError, match="Returning the state is not supported"): - func() - - @pytest.mark.parametrize("wires", [[0, 2, 3, 1], ["a", -1, "b", 1000]]) - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) - def test_custom_wire_labels(self, wires, dev_name): - """Test if an error is raised when custom wire labels are used""" - dev = qml.device(dev_name, wires=wires) - - @qnode(dev, diff_method="parameter-shift") - def func(): - qml.Hadamard(wires=wires[0]) - for i in range(3): - qml.CNOT(wires=[wires[i], wires[i + 1]]) - return density_matrix(0) - - with pytest.raises(QuantumFunctionError, match="custom wire labels"): - func() diff --git a/tests/tape/test_tape_mode.py b/tests/tape/test_tape_mode.py deleted file mode 100644 index 3c4c29a3114..00000000000 --- a/tests/tape/test_tape_mode.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests enabling and disabling tape mode""" -import pytest - -import pennylane as qml - - -def test_enable_tape_mode_decorator(): - """Test that the enable_tape function properly - enables tape mode when creating QNodes using the decorator.""" - dev = qml.device("default.qubit", wires=1) - - qml.disable_tape() - - @qml.qnode(dev) - def circuit(x, y): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - circuit(0.5, 0.1) - - assert not isinstance(circuit, qml.tape.QNode) - assert isinstance(circuit, qml.qnodes.BaseQNode) - assert not hasattr(circuit, "qtape") - - qml.enable_tape() - - assert "tape" in qml.expval.__module__ - - @qml.qnode(dev) - def circuit(x, y): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - circuit(0.5, 0.1) - - assert isinstance(circuit, qml.tape.QNode) - assert not isinstance(circuit, qml.qnodes.BaseQNode) - assert hasattr(circuit, "qtape") - - -def test_enable_tape_mode_class(): - """Test that the enable_tape function properly - enables tape mode when creating QNodes using the class.""" - dev = qml.device("default.qubit", wires=1) - - qml.disable_tape() - - def circuit(x, y): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - qnode = qml.QNode(circuit, dev) - qnode(0.5, 0.1) - - assert not isinstance(qnode, qml.tape.QNode) - assert isinstance(qnode, qml.qnodes.BaseQNode) - assert not hasattr(qnode, "qtape") - - qml.enable_tape() - - def circuit(x, y): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - qnode = qml.QNode(circuit, dev) - qnode(0.5, 0.1) - - assert isinstance(qnode, qml.tape.QNode) - assert not isinstance(qnode, qml.qnodes.BaseQNode) - assert hasattr(qnode, "qtape") - - - -def test_disable_tape(): - """Test that the disable_tape function reverts QNode creation - to standard behaviour""" - dev = qml.device("default.qubit", wires=1) - - def circuit(x, y): - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - # doesn't matter how many times we call it - qml.enable_tape() - qml.enable_tape() - - qnode = qml.QNode(circuit, dev) - qnode(0.5, 0.1) - - assert isinstance(qnode, qml.tape.QNode) - assert not isinstance(qnode, qml.qnodes.BaseQNode) - assert hasattr(qnode, "qtape") - - qml.disable_tape() - - assert "tape" not in qml.expval.__module__ - - qnode = qml.QNode(circuit, dev) - qnode(0.5, 0.1) - - assert not isinstance(qnode, qml.tape.QNode) - assert isinstance(qnode, qml.qnodes.BaseQNode) - assert not hasattr(qnode, "qtape") - - qml.enable_tape() - - -def test_disable_tape_exception(): - """Test that disabling tape mode raises a warning - if not currently in tape mode""" - qml.disable_tape() - with pytest.warns(UserWarning, match="Tape mode is not currently enabled"): - qml.disable_tape() - qml.enable_tape() - - -def test_tape_mode_detection(): - """Test that the function `tape_mode_active` returns True - only if tape mode is activated.""" - qml.disable_tape() - assert not qml.tape_mode_active() - qml.enable_tape() - assert qml.tape_mode_active() diff --git a/tests/tape/test_tape_queuing.py b/tests/tape/test_tape_queuing.py deleted file mode 100644 index 86f93078951..00000000000 --- a/tests/tape/test_tape_queuing.py +++ /dev/null @@ -1,357 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane` :class:`QueuingContext` class. -""" -import contextlib - -import pytest -import pennylane as qml -import numpy as np - -from pennylane.tape.queuing import AnnotatedQueue, AnnotatedQueue, Queue, QueuingContext -from pennylane.tape.operation import mock_operations - - -@pytest.fixture(autouse=True) -def patch_operator(): - with contextlib.ExitStack() as stack: - for mock in mock_operations(): - stack.enter_context(mock) - yield - - -@pytest.fixture(scope="function") -def mock_queuing_context(monkeypatch): - """A mock instance of the abstract QueuingContext class.""" - with monkeypatch.context() as m: - m.setattr(QueuingContext, "__abstractmethods__", frozenset()) - m.setattr( - QueuingContext, - "_append", - lambda self, operator: self.queue.append(operator), - ) - m.setattr( - QueuingContext, - "_remove", - lambda self, operator: self.queue.remove(operator), - ) - context = QueuingContext() - context.queue = [] - - yield context - - -@pytest.fixture(scope="function") -def three_mock_queuing_contexts(monkeypatch): - """A list of three mock instances of the abstract QueuingContext class.""" - with monkeypatch.context() as m: - m.setattr(QueuingContext, "__abstractmethods__", frozenset()) - m.setattr( - QueuingContext, - "_append", - lambda self, operator: self.queue.append(operator), - ) - m.setattr( - QueuingContext, - "_remove", - lambda self, operator: self.queue.remove(operator), - ) - - contexts = [QueuingContext() for _ in range(3)] - for context in contexts: - context.queue = [] - - yield contexts - - -class TestQueuingContext: - """Test the logic associated with the QueuingContext class.""" - - def test_context_activation(self, mock_queuing_context): - """Test that the QueuingContext is properly activated and deactivated.""" - - # Assert that the list of active contexts is empty - assert not QueuingContext._active_contexts - - with mock_queuing_context: - assert len(QueuingContext._active_contexts) == 1 - assert mock_queuing_context in QueuingContext._active_contexts - - assert not QueuingContext._active_contexts - - def test_multiple_context_activation(self, three_mock_queuing_contexts): - """Test that multiple QueuingContexts are properly activated and deactivated.""" - - # Assert that the list of active contexts is empty - assert not QueuingContext._active_contexts - - with three_mock_queuing_contexts[0]: - with three_mock_queuing_contexts[1]: - with three_mock_queuing_contexts[2]: - assert len(QueuingContext._active_contexts) == 3 - assert three_mock_queuing_contexts[0] in QueuingContext._active_contexts - assert three_mock_queuing_contexts[1] in QueuingContext._active_contexts - assert three_mock_queuing_contexts[2] in QueuingContext._active_contexts - - assert not QueuingContext._active_contexts - - def test_append_no_context(self): - """Test that append does not fail when no context is present.""" - - QueuingContext.append(qml.PauliZ(0)) - - def test_remove_no_context(self): - """Test that remove does not fail when no context is present.""" - - QueuingContext.remove(qml.PauliZ(0)) - - def test_no_active_context(self, mock_queuing_context): - """Test that if there are no active contexts, active_context() returns None""" - assert mock_queuing_context.active_context() is None - - -class TestQueue: - """Test the Queue class.""" - - def test_arbitrary_obj(self): - """Tests that arbitrary objects can be appended to and removed from the queue.""" - - objs = [5, "hi", 1.2, np.einsum, lambda x: x + 1] - with Queue() as q: - for obj in objs: - q.append(obj) - assert q.queue == objs - - with q: - for _ in range(len(objs)): - obj = objs.pop() - q.remove(obj) - assert q.queue == objs - - def test_remove_not_in_queue(self): - """Test that remove fails when the object to be removed is not in the queue.""" - - with Queue() as q1: - op1 = qml.PauliZ(0) - op2 = qml.PauliZ(1) - q1.append(op1) - q1.append(op2) - - with Queue() as q2: - q2.append(op1) - - with pytest.raises(ValueError, match="not in list"): - q2.remove(op2) - - def test_append_qubit_gates(self): - """Test that gates are successfully appended to the queue.""" - with Queue() as q: - ops = [ - qml.RX(0.5, wires=0), - qml.RY(-10.1, wires=1), - qml.CNOT(wires=[0, 1]), - qml.PhaseShift(-1.1, wires=18), - qml.T(wires=99), - ] - assert q.queue == ops - - def test_append_qubit_observables(self): - """Test that ops that are also observables are successfully - appended to the queue.""" - with Queue() as q: - # wire repetition is deliberate, Queue contains no checks/logic - # for circuits - ops = [ - qml.Hadamard(wires=0), - qml.PauliX(wires=1), - qml.PauliY(wires=1), - qml.Hermitian(np.ones([2, 2]), wires=7), - ] - assert q.queue == ops - - def test_append_tensor_ops(self): - """Test that ops which are used as inputs to `Tensor` - are successfully added to the queue, as well as the `Tensor` object.""" - - with Queue() as q: - A = qml.PauliZ(0) - B = qml.PauliY(1) - tensor_op = qml.operation.Tensor(A, B) - assert q.queue == [A, B, tensor_op] - assert tensor_op.obs == [A, B] - - def test_append_tensor_ops_overloaded(self): - """Test that Tensor ops created using `@` - are successfully added to the queue, as well as the `Tensor` object.""" - - with Queue() as q: - A = qml.PauliZ(0) - B = qml.PauliY(1) - tensor_op = A @ B - assert q.queue == [A, B, tensor_op] - assert tensor_op.obs == [A, B] - - -class TestAnnotatedQueue: - """Tests for the annotated queue class""" - - def test_remove_not_in_queue(self): - """Test that remove fails when the object to be removed is not in the queue.""" - - with AnnotatedQueue() as q1: - op1 = qml.PauliZ(0) - op2 = qml.PauliZ(1) - q1.append(op1) - q1.append(op2) - - with AnnotatedQueue() as q2: - q2.append(op1) - with pytest.raises(KeyError): - q2.remove(op2) - - def test_append_qubit_gates(self): - """Test that gates are successfully appended to the queue.""" - with AnnotatedQueue() as q: - ops = [ - qml.RX(0.5, wires=0), - qml.RY(-10.1, wires=1), - qml.CNOT(wires=[0, 1]), - qml.PhaseShift(-1.1, wires=18), - qml.T(wires=99), - ] - assert q.queue == ops - - def test_append_qubit_observables(self): - """Test that ops that are also observables are successfully - appended to the queue.""" - with AnnotatedQueue() as q: - # wire repetition is deliberate, Queue contains no checks/logic - # for circuits - ops = [ - qml.Hadamard(wires=0), - qml.PauliX(wires=1), - qml.PauliY(wires=1), - qml.Hermitian(np.ones([2, 2]), wires=7), - ] - assert q.queue == ops - - def test_append_tensor_ops(self): - """Test that ops which are used as inputs to `Tensor` - are successfully added to the queue, as well as the `Tensor` object.""" - - with AnnotatedQueue() as q: - A = qml.PauliZ(0) - B = qml.PauliY(1) - tensor_op = qml.operation.Tensor(A, B) - assert q.queue == [A, B, tensor_op] - assert tensor_op.obs == [A, B] - - def test_append_tensor_ops_overloaded(self): - """Test that Tensor ops created using `@` - are successfully added to the queue, as well as the `Tensor` object.""" - - with AnnotatedQueue() as q: - A = qml.PauliZ(0) - B = qml.PauliY(1) - tensor_op = A @ B - assert q.queue == [A, B, tensor_op] - assert tensor_op.obs == [A, B] - - def test_get_info(self): - """Test that get_info correctly returns an annotation""" - A = qml.RZ(0.5, wires=1) - - with AnnotatedQueue() as q: - q.append(A, inv=True) - - assert q._get_info(A) == {"inv": True} - - def test_get_info_error(self): - """Test that an exception is raised if get_info is called - for a non-existent object""" - - with AnnotatedQueue() as q: - A = qml.PauliZ(0) - - B = qml.PauliY(1) - - with pytest.raises(ValueError, match="not in the queue"): - q._get_info(B) - - def test_get_info_none(self): - """Test that get_info returns None if there is no active queuing context""" - A = qml.RZ(0.5, wires=1) - - with AnnotatedQueue() as q: - q.append(A, inv=True) - - assert QueuingContext.get_info(A) is None - - def test_update_info(self): - """Test that update_info correctly updates an annotation""" - A = qml.RZ(0.5, wires=1) - - with AnnotatedQueue() as q: - q.append(A, inv=True) - assert QueuingContext.get_info(A) == {"inv": True} - - assert q._get_info(A) == {"inv": True} - - q._update_info(A, inv=False, owner=None) - assert q._get_info(A) == {"inv": False, "owner": None} - - def test_update_error(self): - """Test that an exception is raised if get_info is called - for a non-existent object""" - - with AnnotatedQueue() as q: - A = qml.PauliZ(0) - - B = qml.PauliY(1) - - with pytest.raises(ValueError, match="not in the queue"): - q._update_info(B, inv=True) - - def test_append_annotating_object(self): - """Test appending an object that writes annotations when queuing itself""" - - class AnnotatingTensor(qml.operation.Tensor): - """Dummy tensor class that queues itself on initialization - to an annotating queue.""" - - def __init__(self, *args): - super().__init__(*args) - self.queue() - - def queue(self): - QueuingContext.append(self, owns=tuple(self.obs)) - - for o in self.obs: - try: - QueuingContext.update_info(o, owner=self) - except AttributeError: - pass - - return self - - with AnnotatedQueue() as q: - A = qml.PauliZ(0) - B = qml.PauliY(1) - tensor_op = AnnotatingTensor(A, B) - - assert q.queue == [A, B, tensor_op] - assert q._get_info(A) == {"owner": tensor_op} - assert q._get_info(B) == {"owner": tensor_op} - assert q._get_info(tensor_op) == {"owns": (A, B)} diff --git a/tests/templates/test_broadcast.py b/tests/templates/test_broadcast.py index 87a7e2ee2a8..3254e2a9b19 100644 --- a/tests/templates/test_broadcast.py +++ b/tests/templates/test_broadcast.py @@ -27,9 +27,6 @@ from pennylane.wires import Wires -pytestmark = pytest.mark.usefixtures("tape_mode") - - @template def ConstantTemplate(wires): T(wires=wires) diff --git a/tests/templates/test_embeddings.py b/tests/templates/test_embeddings.py index 1fe880ef212..e9dab158888 100644 --- a/tests/templates/test_embeddings.py +++ b/tests/templates/test_embeddings.py @@ -31,9 +31,6 @@ from pennylane.wires import Wires -pytestmark = pytest.mark.usefixtures("tape_mode") - - class TestAmplitudeEmbedding: """ Tests the AmplitudeEmbedding method.""" @@ -674,9 +671,6 @@ def circuit(x=None): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") - n_wires = 1 weights = np.zeros(shape=(1, 1)) dev = qml.device('default.qubit', wires=n_wires) @@ -754,9 +748,6 @@ def circuit(x=None): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") - n_subsystems = 2 dev = qml.device('default.gaussian', wires=n_subsystems) @@ -833,9 +824,6 @@ def circuit(x=None): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") - n_subsystems = 2 dev = qml.device('default.gaussian', wires=n_subsystems) diff --git a/tests/templates/test_integration.py b/tests/templates/test_integration.py index d450279d247..e3b3fe65f96 100644 --- a/tests/templates/test_integration.py +++ b/tests/templates/test_integration.py @@ -63,8 +63,6 @@ # Fixtures for integration tests ######################################### -pytestmark = pytest.mark.usefixtures("tape_mode") - # Each entry to QUBIT_DIFFABLE_NONDIFFABLE or CV_DIFFABLE_NONDIFFABLE # adds a template with specified inputs to the integration tests # ``TestIntegrationQnode``, ``TestIntegrationOtherOps``, ``TestIntegrationGradient`` diff --git a/tests/templates/test_layers.py b/tests/templates/test_layers.py index 129375a3e1c..2e35c5e9bcb 100644 --- a/tests/templates/test_layers.py +++ b/tests/templates/test_layers.py @@ -38,9 +38,6 @@ TOLERANCE = 1e-8 -pytestmark = pytest.mark.usefixtures("tape_mode") - - class TestCVNeuralNet: """Tests for the CVNeuralNet from the pennylane.template module.""" @@ -260,8 +257,6 @@ def test_uses_correct_number_of_imprimitives(self, n_layers, n_subsystems): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") dev = qml.device("default.qubit", wires=2) @@ -347,8 +342,6 @@ def circuit(weights): def test_no_seed(self, tol): """Test that two calls to a qnode with RandomLayers() for 'seed=None' option create the same circuit for immutable qnodes.""" - if qml.tape_mode_active(): - pytest.skip("Immutable QNodes no longer exist in tape mode") dev = qml.device("default.qubit", wires=2) weights = [[0.1] * 100] @@ -496,8 +489,6 @@ def circuit(phi): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") dev = qml.device("default.qubit", wires=4) @@ -595,8 +586,6 @@ def circuit(initial_layer, weights): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") dev = qml.device("default.qubit", wires=4) initial_layer = np.random.randn(2) @@ -706,8 +695,6 @@ def circuit(weights): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") n_wires = 1 dev = qml.device('default.qubit', wires=n_wires) @@ -827,9 +814,6 @@ def test_u2_exceptions(self, weights, wires, msg_match): dev = qml.device("default.qubit", wires=N) - if not qml.tape_mode_active() and "must be 2-dimensional" in msg_match: - pytest.skip("Validation only performed in tape mode") - @qml.qnode(dev) def circuit(): ParticleConservingU2( diff --git a/tests/templates/test_state_preparations.py b/tests/templates/test_state_preparations.py index 1e22fea9b78..e27d19860f1 100644 --- a/tests/templates/test_state_preparations.py +++ b/tests/templates/test_state_preparations.py @@ -32,9 +32,6 @@ from pennylane.wires import Wires -pytestmark = pytest.mark.usefixtures("tape_mode") - - class TestHelperFunctions: """Tests the functionality of helper functions.""" @@ -106,7 +103,6 @@ def test_correct_pl_gates(self, basis_state, wires, target_wires): ([1, 0, 1], [0, 1, 2], [1, 0, 1]), ]) # fmt: on - @pytest.mark.usefixtures("tape_mode") def test_state_preparation(self, tol, qubit_device_3_wires, basis_state, wires, target_state): """Tests that the template BasisStatePreparation integrates correctly with PennyLane.""" @@ -151,9 +147,6 @@ def test_error_basis_state_format(self, basis_state, wires): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") - dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) @@ -221,7 +214,6 @@ class TestMottonenStatePreparation: ([1/2, 0, 0, 0, 1j/2, 0, 1j/math.sqrt(2), 0], [0, 1, 2], [1/2, 0, 0, 0, 1j/2, 0, 1j/math.sqrt(2), 0]), ([1/2, 0, 1j/2, 1j/math.sqrt(2)], [0, 1], [1/2, 0, 0, 0, 1j/2, 0, 1j/math.sqrt(2), 0]), ]) - @pytest.mark.usefixtures("tape_mode") # fmt: on def test_state_preparation_fidelity(self, tol, qubit_device_3_wires, state_vector, wires, target_state): """Tests that the template MottonenStatePreparation integrates correctly with PennyLane @@ -286,7 +278,6 @@ def circuit(): ([1/2, 0, 0, 0, 1j/2, 0, 1j/math.sqrt(2), 0], [0, 1, 2], [1/2, 0, 0, 0, 1j/2, 0, 1j/math.sqrt(2), 0]), ([1/2, 0, 1j/2, 1j/math.sqrt(2)], [0, 1], [1/2, 0, 0, 0, 1j/2, 0, 1j/math.sqrt(2), 0]), ]) - @pytest.mark.usefixtures("tape_mode") # fmt: on def test_state_preparation_probability_distribution(self, tol, qubit_device_3_wires, state_vector, wires, target_state): """Tests that the template MottonenStatePreparation integrates correctly with PennyLane @@ -349,8 +340,6 @@ def test_get_alpha_y(self, current_qubit, expected, tol): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") dev = qml.device("default.qubit", wires=2) @@ -431,7 +420,6 @@ def test_correct_gates_two_wires(self): assert rec.queue[5].data[1] == "XY" assert rec.queue[5].wires == Wires([0, 1]) - @pytest.mark.usefixtures("tape_mode") def test_GHZ_generation(self, qubit_device_3_wires, tol): """Test that the template prepares a GHZ state.""" GHZ_state = np.array([1/math.sqrt(2), 0, 0, 0, 0, 0, 0, 1/math.sqrt(2)]) @@ -449,7 +437,6 @@ def circuit(weights): assert np.allclose(circuit.device.state, GHZ_state, atol=tol, rtol=0) - @pytest.mark.usefixtures("tape_mode") def test_even_superposition_generation(self, qubit_device_3_wires, tol): """Test that the template prepares a even superposition state.""" even_superposition_state = np.ones(8)/math.sqrt(8) @@ -472,9 +459,6 @@ def circuit(weights): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") - dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py index 79593ae545f..e3d7e2129e7 100644 --- a/tests/templates/test_subroutines.py +++ b/tests/templates/test_subroutines.py @@ -39,9 +39,6 @@ ) -pytestmark = pytest.mark.usefixtures("tape_mode") - - # fmt: off PAULI_WORD_TEST_DATA = [ (1, ["X", "Y", "Z"]), @@ -349,9 +346,6 @@ def circuit(theta, phi, varphi): def test_interferometer_wrong_dim(self): """Integration test for the CVNeuralNetLayers method.""" - if not qml.tape_mode_active(): - pytest.skip("Validation only performed in tape mode") - dev = qml.device("default.gaussian", wires=4) @qml.qnode(dev) @@ -564,9 +558,6 @@ def test_correct_gates_two_wires(self): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") - dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) diff --git a/tests/templates/test_templ_utils.py b/tests/templates/test_templ_utils.py index 927a81ce1ba..b5c5282c384 100644 --- a/tests/templates/test_templ_utils.py +++ b/tests/templates/test_templ_utils.py @@ -17,13 +17,11 @@ # pylint: disable=protected-access,cell-var-from-loop import pytest import numpy as np -from pennylane.variable import Variable from pennylane.templates.utils import (check_wires, check_shape, check_shapes, get_shape, check_number_of_layers, - check_no_variable, check_is_in_options, check_type) @@ -94,13 +92,6 @@ ([[[1], [2], [3]], [[1], [2]]], 4), ] -NO_VARIABLES_PASS = [[[], np.array([1., 4.])], - [1, 'a']] - -NO_VARIABLES_FAIL = [[[Variable(0.1)], Variable([0.1])], - np.array([Variable(0.3), Variable(4.)]), - Variable(-1.)] - OPTIONS_PASS = [("a", ["a", "b"])] OPTIONS_FAIL = [("c", ["a", "b"])] @@ -108,12 +99,12 @@ TYPE_PASS = [(["a"], list, type(None)), (1, int, type(None)), ("a", int, str), - (Variable(1.), list, Variable) + (list, int) ] TYPE_FAIL = [("a", list, type(None)), - (Variable(1.), int, list), - (1., Variable, type(None)) + (int, list), + (1., type(None)) ] @@ -123,17 +114,6 @@ class TestInputChecks: """Test private functions that check the input of templates.""" - @pytest.mark.parametrize("arg", NO_VARIABLES_PASS) - def test_check_no_variable(self, arg): - """Tests that variable check succeeds for valid arguments.""" - check_no_variable(arg, msg="XXX") - - @pytest.mark.parametrize("arg", NO_VARIABLES_FAIL) - def test_check_no_variable_exception(self, arg): - """Tests that variable check throws error for invalid arguments.""" - with pytest.raises(ValueError, match="XXX"): - check_no_variable(arg, msg="XXX") - @pytest.mark.parametrize("wires, target", WIRES_PASS) def test_check_wires(self, wires, target): """Tests that wires check returns correct wires list and its length.""" diff --git a/tests/test_measure.py b/tests/test_measure.py index 4b2945cd2d2..d3f644e9e9b 100644 --- a/tests/test_measure.py +++ b/tests/test_measure.py @@ -16,8 +16,24 @@ import numpy as np import pennylane as qml -from pennylane.qnodes import QuantumFunctionError -from pennylane.operation import Sample, Variance, Expectation +from pennylane import QuantumFunctionError +from pennylane.devices import DefaultQubit + +from pennylane.queuing import AnnotatedQueue +from pennylane.measure import ( + expval, + var, + sample, + probs, + state, + density_matrix, + Expectation, + Sample, + State, + Variance, + Probability, + MeasurementProcess, +) def test_no_measure(tol): @@ -239,3 +255,744 @@ def circuit(): return res circuit() + + +@pytest.mark.parametrize( + "stat_func,return_type", [(expval, Expectation), (var, Variance), (sample, Sample)] +) +class TestStatisticsQueuing: + """Tests for annotating the return types of the statistics functions""" + + @pytest.mark.parametrize( + "op", + [qml.PauliX, qml.PauliY, qml.PauliZ, qml.Hadamard, qml.Identity], + ) + def test_annotating_obs_return_type(self, stat_func, return_type, op): + """Test that the return_type related info is updated for a + measurement""" + with AnnotatedQueue() as q: + A = op(0) + stat_func(A) + + assert q.queue[:-1] == [A] + meas_proc = q.queue[-1] + assert isinstance(meas_proc, MeasurementProcess) + assert meas_proc.return_type == return_type + + assert q._get_info(A) == {"owner": meas_proc} + assert q._get_info(meas_proc) == {"owns": (A)} + + def test_annotating_tensor_hermitian(self, stat_func, return_type): + """Test that the return_type related info is updated for a measurement + when called for an Hermitian observable""" + + mx = np.array([[1, 0], [0, 1]]) + + with AnnotatedQueue() as q: + Herm = qml.Hermitian(mx, wires=[1]) + stat_func(Herm) + + assert q.queue[:-1] == [Herm] + meas_proc = q.queue[-1] + assert isinstance(meas_proc, MeasurementProcess) + assert meas_proc.return_type == return_type + + assert q._get_info(Herm) == {"owner": meas_proc} + assert q._get_info(meas_proc) == {"owns": (Herm)} + + @pytest.mark.parametrize( + "op1,op2", + [ + (qml.PauliY, qml.PauliX), + (qml.Hadamard, qml.Hadamard), + (qml.PauliY, qml.Identity), + (qml.Identity, qml.Identity), + ], + ) + def test_annotating_tensor_return_type(self, op1, op2, stat_func, return_type): + """Test that the return_type related info is updated for a measurement + when called for an Tensor observable""" + with AnnotatedQueue() as q: + A = op1(0) + B = op2(1) + tensor_op = A @ B + stat_func(tensor_op) + + assert q.queue[:-1] == [A, B, tensor_op] + meas_proc = q.queue[-1] + assert isinstance(meas_proc, MeasurementProcess) + assert meas_proc.return_type == return_type + + assert q._get_info(A) == {"owner": tensor_op} + assert q._get_info(B) == {"owner": tensor_op} + assert q._get_info(tensor_op) == {"owns": (A, B), "owner": meas_proc} + + @pytest.mark.parametrize( + "op1,op2", + [ + (qml.PauliY, qml.PauliX), + (qml.Hadamard, qml.Hadamard), + (qml.PauliY, qml.Identity), + (qml.Identity, qml.Identity), + ], + ) + def test_queueing_tensor_observable(self, op1, op2, stat_func, return_type): + """Test that if the constituent components of a tensor operation are not + found in the queue for annotation, that they are queued first and then annotated.""" + A = op1(0) + B = op2(1) + + with AnnotatedQueue() as q: + tensor_op = A @ B + stat_func(tensor_op) + + assert q.queue[:-1] == [A, B, tensor_op] + meas_proc = q.queue[-1] + assert isinstance(meas_proc, MeasurementProcess) + assert meas_proc.return_type == return_type + + assert q._get_info(A) == {"owner": tensor_op} + assert q._get_info(B) == {"owner": tensor_op} + assert q._get_info(tensor_op) == {"owns": (A, B), "owner": meas_proc} + + +@pytest.mark.parametrize("stat_func", [expval, var, sample]) +class TestBetaStatisticsError: + """Tests for errors arising for the beta statistics functions""" + + def test_not_an_observable(self, stat_func): + """Test that a QuantumFunctionError is raised if the provided + argument is not an observable""" + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(): + qml.RX(0.52, wires=0) + return stat_func(qml.CNOT(wires=[0, 1])) + + with pytest.raises(QuantumFunctionError, match="CNOT is not an observable"): + res = circuit() + + +class TestBetaProbs: + """Tests for annotating the return types of the probs function""" + + @pytest.mark.parametrize("wires", [[0], [0, 1], [1, 0, 2]]) + def test_annotating_probs(self, wires): + with AnnotatedQueue() as q: + probs(wires) + + assert len(q.queue) == 1 + + meas_proc = q.queue[0] + assert isinstance(meas_proc, MeasurementProcess) + assert meas_proc.return_type == Probability + + +class TestProperties: + """Test for the properties""" + + def test_wires_match_observable(self): + """Test that the wires of the measurement process + match an internal observable""" + obs = qml.Hermitian(np.diag([1, 2, 3]), wires=["a", "b", "c"]) + m = MeasurementProcess(Expectation, obs=obs) + + assert np.all(m.wires == obs.wires) + + def test_eigvals_match_observable(self): + """Test that the eigenvalues of the measurement process + match an internal observable""" + obs = qml.Hermitian(np.diag([1, 2, 3]), wires=[0, 1, 2]) + m = MeasurementProcess(Expectation, obs=obs) + + assert np.all(m.eigvals == np.array([1, 2, 3])) + + # changing the observable data should be reflected + obs.data = [np.diag([5, 6, 7])] + assert np.all(m.eigvals == np.array([5, 6, 7])) + + def test_error_obs_and_eigvals(self): + """Test that providing both eigenvalues and an observable + results in an error""" + obs = qml.Hermitian(np.diag([1, 2, 3]), wires=[0, 1, 2]) + + with pytest.raises(ValueError, match="Cannot set the eigenvalues"): + MeasurementProcess(Expectation, obs=obs, eigvals=[0, 1]) + + def test_error_obs_and_wires(self): + """Test that providing both wires and an observable + results in an error""" + obs = qml.Hermitian(np.diag([1, 2, 3]), wires=[0, 1, 2]) + + with pytest.raises(ValueError, match="Cannot set the wires"): + MeasurementProcess(Expectation, obs=obs, wires=qml.wires.Wires([0, 1])) + + def test_observable_with_no_eigvals(self): + """An observable with no eigenvalues defined should cause + the eigvals property on the associated measurement process + to be None""" + obs = qml.NumberOperator(wires=0) + m = MeasurementProcess(Expectation, obs=obs) + assert m.eigvals is None + + def test_repr(self): + """Test the string representation of a MeasurementProcess.""" + m = MeasurementProcess(Expectation, obs=qml.PauliZ(wires="a") @ qml.PauliZ(wires="b")) + expected = "expval(PauliZ(wires=['a']) @ PauliZ(wires=['b']))" + assert str(m) == expected + + m = MeasurementProcess(Probability, obs=qml.PauliZ(wires="a")) + expected = "probs(PauliZ(wires=['a']))" + assert str(m) == expected + + +class TestExpansion: + """Test for measurement expansion""" + + def test_expand_pauli(self): + """Test the expansion of a Pauli observable""" + obs = qml.PauliX(0) @ qml.PauliY(1) + m = MeasurementProcess(Expectation, obs=obs) + tape = m.expand() + + assert len(tape.operations) == 4 + + assert tape.operations[0].name == "Hadamard" + assert tape.operations[0].wires.tolist() == [0] + + assert tape.operations[1].name == "PauliZ" + assert tape.operations[1].wires.tolist() == [1] + assert tape.operations[2].name == "S" + assert tape.operations[2].wires.tolist() == [1] + assert tape.operations[3].name == "Hadamard" + assert tape.operations[3].wires.tolist() == [1] + + assert len(tape.measurements) == 1 + assert tape.measurements[0].return_type is Expectation + assert tape.measurements[0].wires.tolist() == [0, 1] + assert np.all(tape.measurements[0].eigvals == np.array([1, -1, -1, 1])) + + def test_expand_hermitian(self, tol): + """Test the expansion of an hermitian observable""" + H = np.array([[1, 2], [2, 4]]) + obs = qml.Hermitian(H, wires=["a"]) + + m = MeasurementProcess(Expectation, obs=obs) + tape = m.expand() + + assert len(tape.operations) == 1 + + assert tape.operations[0].name == "QubitUnitary" + assert tape.operations[0].wires.tolist() == ["a"] + assert np.allclose( + tape.operations[0].parameters[0], + np.array([[-2, 1], [1, 2]]) / np.sqrt(5), + atol=tol, + rtol=0, + ) + + assert len(tape.measurements) == 1 + assert tape.measurements[0].return_type is Expectation + assert tape.measurements[0].wires.tolist() == ["a"] + assert np.all(tape.measurements[0].eigvals == np.array([0, 5])) + + def test_expand_no_observable(self): + """Check that an exception is raised if the measurement to + be expanded has no observable""" + m = MeasurementProcess(Probability, wires=qml.wires.Wires([0, 1])) + + with pytest.raises(NotImplementedError, match="Cannot expand"): + m.expand() + + +class TestState: + """Tests for the state function""" + + @pytest.mark.parametrize("wires", range(2, 5)) + def test_state_shape_and_dtype(self, wires): + """Test that the state is of correct size and dtype for a trivial circuit""" + + dev = qml.device("default.qubit", wires=wires) + + @qml.qnode(dev) + def func(): + return state() + + state_val = func() + assert state_val.shape == (2 ** wires,) + assert state_val.dtype == np.complex128 + + def test_return_type_is_state(self): + """Test that the return type of the observable is State""" + + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def func(): + qml.Hadamard(0) + return state() + + func() + obs = func.qtape.observables + assert len(obs) == 1 + assert obs[0].return_type is State + + @pytest.mark.parametrize("wires", range(2, 5)) + def test_state_correct_ghz(self, wires): + """Test that the correct state is returned when the circuit prepares a GHZ state""" + + dev = qml.device("default.qubit", wires=wires) + + @qml.qnode(dev) + def func(): + qml.Hadamard(wires=0) + for i in range(wires - 1): + qml.CNOT(wires=[i, i + 1]) + return state() + + state_val = func() + assert np.allclose(np.sum(np.abs(state_val) ** 2), 1) + assert np.allclose(state_val[0], 1 / np.sqrt(2)) + assert np.allclose(state_val[-1], 1 / np.sqrt(2)) + + def test_return_with_other_types(self): + """Test that an exception is raised when a state is returned along with another return + type""" + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def func(): + qml.Hadamard(wires=0) + return state(), expval(qml.PauliZ(1)) + + with pytest.raises( + QuantumFunctionError, + match="The state or density matrix" + " cannot be returned in combination" + " with other return types", + ): + func() + + @pytest.mark.parametrize("wires", range(2, 5)) + def test_state_equal_to_dev_state(self, wires): + """Test that the returned state is equal to the one stored in dev.state for a template + circuit""" + + dev = qml.device("default.qubit", wires=wires) + + weights = qml.init.strong_ent_layers_uniform(3, wires) + + @qml.qnode(dev) + def func(): + qml.templates.StronglyEntanglingLayers(weights, wires=range(wires)) + return state() + + state_val = func() + assert np.allclose(state_val, func.device.state) + + @pytest.mark.usefixtures("skip_if_no_tf_support") + def test_interface_tf(self, skip_if_no_tf_support): + """Test that the state correctly outputs in the tensorflow interface""" + import tensorflow as tf + + dev = qml.device("default.qubit", wires=4) + + @qml.qnode(dev, interface="tf") + def func(): + for i in range(4): + qml.Hadamard(i) + return state() + + state_expected = 0.25 * tf.ones(16) + state_val = func() + + assert isinstance(state_val, tf.Tensor) + assert state_val.dtype == tf.complex128 + assert np.allclose(state_expected, state_val.numpy()) + assert state_val.shape == (16,) + + def test_interface_torch(self): + """Test that the state correctly outputs in the torch interface""" + torch = pytest.importorskip("torch", minversion="1.6") + + dev = qml.device("default.qubit", wires=4) + + @qml.qnode(dev, interface="torch") + def func(): + for i in range(4): + qml.Hadamard(i) + return state() + + state_expected = 0.25 * torch.ones(16, dtype=torch.complex128) + state_val = func() + + assert isinstance(state_val, torch.Tensor) + assert state_val.dtype == torch.complex128 + assert torch.allclose(state_expected, state_val) + assert state_val.shape == (16,) + + def test_jacobian_not_supported(self): + """Test if an error is raised if the jacobian method is called via qml.grad""" + dev = qml.device("default.qubit", wires=4) + + @qml.qnode(dev, diff_method="parameter-shift") + def func(x): + for i in range(4): + qml.RX(x, wires=i) + return state() + + d_func = qml.jacobian(func) + + with pytest.raises(ValueError, match="The jacobian method does not support"): + d_func(0.1) + + def test_no_state_capability(self, monkeypatch): + """Test if an error is raised for devices that are not capable of returning the state. + This is tested by changing the capability of default.qubit""" + dev = qml.device("default.qubit", wires=1) + capabilities = dev.capabilities().copy() + capabilities["returns_state"] = False + + @qml.qnode(dev) + def func(): + return state() + + with monkeypatch.context() as m: + m.setattr(DefaultQubit, "capabilities", lambda *args, **kwargs: capabilities) + with pytest.raises(QuantumFunctionError, match="The current device is not capable"): + func() + + def test_state_not_supported(self, monkeypatch): + """Test if an error is raised for devices inheriting from the base Device class, + which do not currently support returning the state""" + dev = qml.device("default.gaussian", wires=1) + + @qml.qnode(dev) + def func(): + return state() + + with pytest.raises(QuantumFunctionError, match="Returning the state is not supported"): + func() + + @pytest.mark.usefixtures("skip_if_no_tf_support") + @pytest.mark.parametrize( + "device", ["default.qubit", "default.qubit.tf", "default.qubit.autograd"] + ) + def test_devices(self, device, skip_if_no_tf_support): + """Test that the returned state is equal to the expected returned state for all of + PennyLane's built in statevector devices""" + + dev = qml.device(device, wires=4) + + @qml.qnode(dev, diff_method="parameter-shift") + def func(): + for i in range(4): + qml.Hadamard(i) + return state() + + state_val = func() + state_expected = 0.25 * np.ones(16) + + assert np.allclose(state_val, state_expected) + assert np.allclose(state_val, dev.state) + + @pytest.mark.usefixtures("skip_if_no_tf_support") + def test_gradient_with_passthru_tf(self, skip_if_no_tf_support): + """Test that the gradient of the state is accessible when using default.qubit.tf with the + backprop diff_method.""" + import tensorflow as tf + + dev = qml.device("default.qubit.tf", wires=1) + + @qml.qnode(dev, interface="tf", diff_method="backprop") + def func(x): + qml.RY(x, wires=0) + return state() + + x = tf.Variable(0.1, dtype=tf.complex128) + + with tf.GradientTape() as tape: + result = func(x) + + grad = tape.jacobian(result, x) + expected = tf.stack([-0.5 * tf.sin(x / 2), 0.5 * tf.cos(x / 2)]) + assert np.allclose(grad, expected) + + def test_gradient_with_passthru_autograd(self): + """Test that the gradient of the state is accessible when using default.qubit.autograd + with the backprop diff_method.""" + from pennylane import numpy as anp + + dev = qml.device("default.qubit.autograd", wires=1) + + @qml.qnode(dev, interface="autograd", diff_method="backprop") + def func(x): + qml.RY(x, wires=0) + return state() + + x = anp.array(0.1, requires_grad=True) + + def loss_fn(x): + res = func(x) + return anp.real(res) # This errors without the real. Likely an issue with complex + # numbers in autograd + + d_loss_fn = qml.jacobian(loss_fn) + + grad = d_loss_fn(x) + expected = np.array([-0.5 * np.sin(x / 2), 0.5 * np.cos(x / 2)]) + assert np.allclose(grad, expected) + + @pytest.mark.parametrize("wires", [[0, 2, 3, 1], ["a", -1, "b", 1000]]) + def test_custom_wire_labels(self, wires): + """Test if an error is raised when custom wire labels are used""" + dev = qml.device("default.qubit", wires=wires) + + @qml.qnode(dev, diff_method="parameter-shift") + def func(): + qml.Hadamard(wires=wires[0]) + for i in range(3): + qml.CNOT(wires=[wires[i], wires[i + 1]]) + return state() + + with pytest.raises(QuantumFunctionError, match="custom wire labels"): + func() + + +class TestDensityMatrix: + """Tests for the density matrix function""" + + @pytest.mark.parametrize("wires", range(2, 5)) + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) + def test_density_matrix_shape_and_dtype(self, dev_name, wires): + """Test that the density matrix is of correct size and dtype for a + trivial circuit""" + + dev = qml.device(dev_name, wires=wires) + + @qml.qnode(dev) + def circuit(): + return density_matrix([0]) + + state_val = circuit() + + assert state_val.shape == (2, 2) + assert state_val.dtype == np.complex128 + + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) + def test_return_type_is_state(self, dev_name): + """Test that the return type of the observable is State""" + + dev = qml.device(dev_name, wires=2) + + @qml.qnode(dev) + def func(): + qml.Hadamard(0) + return density_matrix(0) + + func() + obs = func.qtape.observables + assert len(obs) == 1 + assert obs[0].return_type is State + + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) + def test_correct_density_matrix_product_state_first(self, dev_name): + """Test that the correct density matrix is returned when + tracing out a product state""" + + dev = qml.device(dev_name, wires=2) + + @qml.qnode(dev) + def func(): + qml.Hadamard(wires=1) + qml.PauliY(wires=0) + return density_matrix(0) + + density_first = func() + + assert np.allclose( + np.array([[0.0 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, 1.0 + 0.0j]]), density_first + ) + + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) + def test_correct_density_matrix_product_state_second(self, dev_name): + """Test that the correct density matrix is returned when + tracing out a product state""" + + dev = qml.device(dev_name, wires=2) + + @qml.qnode(dev) + def func(): + qml.Hadamard(wires=1) + qml.PauliY(wires=0) + return density_matrix(1) + + density_second = func() + assert np.allclose( + np.array([[0.5 + 0.0j, 0.5 + 0.0j], [0.5 + 0.0j, 0.5 + 0.0j]]), density_second + ) + + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) + def test_correct_density_matrix_three_wires_first(self, dev_name): + """Test that the correct density matrix for an example with three wires""" + + dev = qml.device(dev_name, wires=3) + + @qml.qnode(dev) + def func(): + qml.Hadamard(wires=1) + qml.PauliY(wires=0) + return density_matrix([0, 1]) + + density_full = func() + assert np.allclose( + np.array( + [ + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j, 0.5 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j, 0.5 + 0.0j], + ] + ), + density_full, + ) + + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) + def test_correct_density_matrix_three_wires_second(self, dev_name): + """Test that the correct density matrix for an example with three wires""" + + dev = qml.device(dev_name, wires=3) + + @qml.qnode(dev) + def func(): + qml.Hadamard(0) + qml.Hadamard(1) + qml.CNOT(wires=[1, 2]) + return qml.density_matrix(wires=[1, 2]) + + density = func() + + assert np.allclose( + np.array( + [ + [ + [0.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j], + ] + ] + ), + density, + ) + + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) + def test_correct_density_matrix_mixed_state(self, dev_name): + """Test that the correct density matrix for an example with a mixed state""" + + dev = qml.device(dev_name, wires=2) + + @qml.qnode(dev) + def func(): + qml.Hadamard(0) + qml.CNOT(wires=[0, 1]) + return qml.density_matrix(wires=[1]) + + density = func() + + assert np.allclose(np.array([[0.5 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, 0.5 + 0.0j]]), density) + + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) + def test_correct_density_matrix_all_wires(self, dev_name): + """Test that the correct density matrix is returned when all wires are given""" + + dev = qml.device(dev_name, wires=2) + + @qml.qnode(dev) + def func(): + qml.Hadamard(0) + qml.CNOT(wires=[0, 1]) + return qml.density_matrix(wires=[0, 1]) + + density = func() + + assert np.allclose( + np.array( + [ + [0.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.5 + 0.0j], + ] + ), + density, + ) + + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) + def test_return_with_other_types(self, dev_name): + """Test that an exception is raised when a state is returned along with another return + type""" + + dev = qml.device(dev_name, wires=2) + + @qml.qnode(dev) + def func(): + qml.Hadamard(wires=0) + return density_matrix(0), expval(qml.PauliZ(1)) + + with pytest.raises( + QuantumFunctionError, + match="The state or density matrix" + " cannot be returned in combination" + " with other return types", + ): + func() + + def test_no_state_capability(self, monkeypatch): + """Test if an error is raised for devices that are not capable of returning + the density matrix. This is tested by changing the capability of default.qubit""" + dev = qml.device("default.qubit", wires=2) + capabilities = dev.capabilities().copy() + capabilities["returns_state"] = False + + @qml.qnode(dev) + def func(): + return density_matrix(0) + + with monkeypatch.context() as m: + m.setattr(DefaultQubit, "capabilities", lambda *args, **kwargs: capabilities) + with pytest.raises( + QuantumFunctionError, + match="The current device is not capable" " of returning the state", + ): + func() + + def test_density_matrix_not_supported(self): + """Test if an error is raised for devices inheriting from the base Device class, + which do not currently support returning the state""" + dev = qml.device("default.gaussian", wires=2) + + @qml.qnode(dev) + def func(): + return density_matrix(0) + + with pytest.raises(QuantumFunctionError, match="Returning the state is not supported"): + func() + + @pytest.mark.parametrize("wires", [[0, 2, 3, 1], ["a", -1, "b", 1000]]) + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) + def test_custom_wire_labels(self, wires, dev_name): + """Test if an error is raised when custom wire labels are used""" + dev = qml.device(dev_name, wires=wires) + + @qml.qnode(dev, diff_method="parameter-shift") + def func(): + qml.Hadamard(wires=wires[0]) + for i in range(3): + qml.CNOT(wires=[wires[i], wires[i + 1]]) + return density_matrix(0) + + with pytest.raises(QuantumFunctionError, match="custom wire labels"): + func() diff --git a/tests/test_optimize.py b/tests/test_optimize.py index 46ed81b8b7d..3e3f5c0eb9e 100644 --- a/tests/test_optimize.py +++ b/tests/test_optimize.py @@ -33,9 +33,6 @@ RotosolveOptimizer) -pytestmark = pytest.mark.usefixtures("tape_mode") - - x_vals = np.linspace(-10, 10, 16, endpoint=False) # Hyperparameters for optimizers diff --git a/tests/test_optimize_qng.py b/tests/test_optimize_qng.py index 92b07dedebc..ba903999b90 100644 --- a/tests/test_optimize_qng.py +++ b/tests/test_optimize_qng.py @@ -19,9 +19,6 @@ from pennylane import numpy as np -pytestmark = pytest.mark.usefixtures("tape_mode") - - class TestExceptions: """Test exceptions are raised for incorrect usage""" diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index c9f0463f2e3..0b88fb577e5 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -22,8 +22,6 @@ from pennylane.wires import Wires -pytestmark = pytest.mark.usefixtures("tape_mode") - ##################################################### diff --git a/tests/test_quantum_gradients.py b/tests/test_quantum_gradients.py index 1ab445380a6..94a7b8db6ac 100644 --- a/tests/test_quantum_gradients.py +++ b/tests/test_quantum_gradients.py @@ -614,7 +614,6 @@ def circuit(x): grad_fn(1.0) -@pytest.mark.usefixtures("tape_mode") class TestFourTermParameterShifts: """Tests for quantum gradients that require a 4-term shift formula""" diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py index 03c0e4dd07d..e92b482f860 100644 --- a/tests/test_qubit_device.py +++ b/tests/test_qubit_device.py @@ -24,7 +24,6 @@ from pennylane.qnodes.base import BaseQNode from pennylane.operation import Sample, Variance, Expectation, Probability, State from pennylane.circuit_graph import CircuitGraph -from pennylane.variable import Variable from pennylane.wires import Wires from pennylane.tape import QuantumTape from pennylane.tape.measure import state @@ -213,11 +212,6 @@ def test_unsupported_operations_raise_error(self, mock_qubit_device_with_paulis_ ] ] - variable = Variable(1) - symbolic_queue = [ - [qml.RX(variable, wires=[0])], - ] - observables = [ [qml.PauliZ(0)], [qml.PauliX(0)], @@ -225,7 +219,7 @@ def test_unsupported_operations_raise_error(self, mock_qubit_device_with_paulis_ ] @pytest.mark.parametrize("observables", observables) - @pytest.mark.parametrize("queue", numeric_queues + symbolic_queue) + @pytest.mark.parametrize("queue", numeric_queues) def test_passing_keyword_arguments_to_execute(self, mock_qubit_device_with_paulis_rotations_and_methods, monkeypatch, queue, observables): """Tests that passing keyword arguments to execute propagates those kwargs to the apply() diff --git a/tests/test_queuing.py b/tests/test_queuing.py index d0cf39d4f22..2085097f9ce 100644 --- a/tests/test_queuing.py +++ b/tests/test_queuing.py @@ -14,11 +14,29 @@ """ Unit tests for the :mod:`pennylane` :class:`QueuingContext` class. """ +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests for the :mod:`pennylane` :class:`QueuingContext` class. +""" +import contextlib import pytest import pennylane as qml import numpy as np -from pennylane import QueuingContext + +from pennylane.queuing import AnnotatedQueue, AnnotatedQueue, Queue, QueuingContext, OperationRecorder @pytest.fixture(scope="function") @@ -27,10 +45,14 @@ def mock_queuing_context(monkeypatch): with monkeypatch.context() as m: m.setattr(QueuingContext, "__abstractmethods__", frozenset()) m.setattr( - QueuingContext, "_append", lambda self, operator: self.queue.append(operator), + QueuingContext, + "_append", + lambda self, operator: self.queue.append(operator), ) m.setattr( - QueuingContext, "_remove", lambda self, operator: self.queue.remove(operator), + QueuingContext, + "_remove", + lambda self, operator: self.queue.remove(operator), ) context = QueuingContext() context.queue = [] @@ -44,10 +66,14 @@ def three_mock_queuing_contexts(monkeypatch): with monkeypatch.context() as m: m.setattr(QueuingContext, "__abstractmethods__", frozenset()) m.setattr( - QueuingContext, "_append", lambda self, operator: self.queue.append(operator), + QueuingContext, + "_append", + lambda self, operator: self.queue.append(operator), ) m.setattr( - QueuingContext, "_remove", lambda self, operator: self.queue.remove(operator), + QueuingContext, + "_remove", + lambda self, operator: self.queue.remove(operator), ) contexts = [QueuingContext() for _ in range(3)] @@ -110,7 +136,7 @@ def test_arbitrary_obj(self): """Tests that arbitrary objects can be appended to and removed from the queue.""" objs = [5, "hi", 1.2, np.einsum, lambda x: x + 1] - with qml._queuing.Queue() as q: + with Queue() as q: for obj in objs: q.append(obj) assert q.queue == objs @@ -122,21 +148,23 @@ def test_arbitrary_obj(self): assert q.queue == objs def test_remove_not_in_queue(self): - """Test that remove does not fail when the object to be removed is not in the queue.""" + """Test that remove fails when the object to be removed is not in the queue.""" - with qml._queuing.Queue() as q1: + with Queue() as q1: op1 = qml.PauliZ(0) op2 = qml.PauliZ(1) q1.append(op1) q1.append(op2) - with qml._queuing.Queue() as q2: + with Queue() as q2: q2.append(op1) - q2.remove(op2) + + with pytest.raises(ValueError, match="not in list"): + q2.remove(op2) def test_append_qubit_gates(self): """Test that gates are successfully appended to the queue.""" - with qml._queuing.Queue() as q: + with Queue() as q: ops = [ qml.RX(0.5, wires=0), qml.RY(-10.1, wires=1), @@ -149,7 +177,7 @@ def test_append_qubit_gates(self): def test_append_qubit_observables(self): """Test that ops that are also observables are successfully appended to the queue.""" - with qml._queuing.Queue() as q: + with Queue() as q: # wire repetition is deliberate, Queue contains no checks/logic # for circuits ops = [ @@ -162,145 +190,47 @@ def test_append_qubit_observables(self): def test_append_tensor_ops(self): """Test that ops which are used as inputs to `Tensor` - are successfully added to the queue, but no `Tensor` object is.""" + are successfully added to the queue, as well as the `Tensor` object.""" - with qml._queuing.Queue() as q: + with Queue() as q: A = qml.PauliZ(0) B = qml.PauliY(1) tensor_op = qml.operation.Tensor(A, B) - assert q.queue == [A, B] + assert q.queue == [A, B, tensor_op] assert tensor_op.obs == [A, B] - assert all(not isinstance(op, qml.operation.Tensor) for op in q.queue) def test_append_tensor_ops_overloaded(self): """Test that Tensor ops created using `@` - are successfully added to the queue, but no `Tensor` object is.""" + are successfully added to the queue, as well as the `Tensor` object.""" - with qml._queuing.Queue() as q: + with Queue() as q: A = qml.PauliZ(0) B = qml.PauliY(1) tensor_op = A @ B - assert q.queue == [A, B] + assert q.queue == [A, B, tensor_op] assert tensor_op.obs == [A, B] - assert all(not isinstance(op, qml.operation.Tensor) for op in q.queue) - - -class TestOperationRecorder: - """Test the OperationRecorder class.""" - - def test_circuit_integration(self): - """Tests that the OperationRecorder integrates well with the - core behaviour of PennyLane.""" - expected_output = ( - "Operations\n" - + "==========\n" - + "PauliY(wires=[0])\n" - + "PauliY(wires=[1])\n" - + "RZ(tensor(0.4, requires_grad=True), wires=[0])\n" - + "RZ(tensor(0.4, requires_grad=True), wires=[1])\n" - + "CNOT(wires=[0, 1])\n" - + "\n" - + "Observables\n" - + "==========\n" - ) - - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(a, b, c): - qml.RX(a, wires=0) - qml.RY(b, wires=1) - - with qml._queuing.OperationRecorder() as recorder: - ops = [ - qml.PauliY(0), - qml.PauliY(1), - qml.RZ(c, wires=0), - qml.RZ(c, wires=1), - qml.CNOT(wires=[0, 1]), - ] - - assert str(recorder) == expected_output - assert recorder.queue == ops - - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - circuit(0.1, 0.2, 0.4) - - def test_template_integration(self): - """Tests that the OperationRecorder integrates well with the - core behaviour of PennyLane.""" - expected_output = ( - "Operations\n" - + "==========\n" - + "RZ(0, wires=[0])\n" - + "RZ(3, wires=[0])\n" - + "RZ(6, wires=[0])\n" - + "RZ(9, wires=[0])\n" - + "RZ(12, wires=[0])\n" - + "\n" - + "Observables\n" - + "==========\n" - ) - - def template(x): - for i in range(5): - qml.RZ(i * x, wires=0) - - with qml._queuing.OperationRecorder() as recorder: - template(3) - - assert str(recorder) == expected_output - - def test_template_with_return_integration(self): - """Tests that the OperationRecorder integrates well with the - core behaviour of PennyLane.""" - expected_output = ( - "Operations\n" - + "==========\n" - + "RZ(0, wires=[0])\n" - + "RZ(3, wires=[0])\n" - + "RZ(6, wires=[0])\n" - + "RZ(9, wires=[0])\n" - + "RZ(12, wires=[0])\n" - + "\n" - + "Observables\n" - + "==========\n" - + "var(PauliZ(wires=[0]))\n" - + "sample(PauliX(wires=[1]))\n" - ) - - def template(x): - for i in range(5): - qml.RZ(i * x, wires=0) - - return qml.var(qml.PauliZ(0)), qml.sample(qml.PauliX(1)) - - with qml._queuing.OperationRecorder() as recorder: - template(3) - - assert str(recorder) == expected_output class TestAnnotatedQueue: """Tests for the annotated queue class""" def test_remove_not_in_queue(self): - """Test that remove does not fail when the object to be removed is not in the queue.""" + """Test that remove fails when the object to be removed is not in the queue.""" - with qml._queuing.AnnotatedQueue() as q1: + with AnnotatedQueue() as q1: op1 = qml.PauliZ(0) op2 = qml.PauliZ(1) q1.append(op1) q1.append(op2) - with qml._queuing.AnnotatedQueue() as q2: + with AnnotatedQueue() as q2: q2.append(op1) - q2.remove(op2) + with pytest.raises(KeyError): + q2.remove(op2) def test_append_qubit_gates(self): """Test that gates are successfully appended to the queue.""" - with qml._queuing.AnnotatedQueue() as q: + with AnnotatedQueue() as q: ops = [ qml.RX(0.5, wires=0), qml.RY(-10.1, wires=1), @@ -313,7 +243,7 @@ def test_append_qubit_gates(self): def test_append_qubit_observables(self): """Test that ops that are also observables are successfully appended to the queue.""" - with qml._queuing.AnnotatedQueue() as q: + with AnnotatedQueue() as q: # wire repetition is deliberate, Queue contains no checks/logic # for circuits ops = [ @@ -326,67 +256,74 @@ def test_append_qubit_observables(self): def test_append_tensor_ops(self): """Test that ops which are used as inputs to `Tensor` - are successfully added to the queue, but no `Tensor` object is.""" + are successfully added to the queue, as well as the `Tensor` object.""" - with qml._queuing.AnnotatedQueue() as q: + with AnnotatedQueue() as q: A = qml.PauliZ(0) B = qml.PauliY(1) tensor_op = qml.operation.Tensor(A, B) - assert q.queue == [A, B] + assert q.queue == [A, B, tensor_op] assert tensor_op.obs == [A, B] - assert all(not isinstance(op, qml.operation.Tensor) for op in q.queue) def test_append_tensor_ops_overloaded(self): """Test that Tensor ops created using `@` - are successfully added to the queue, but no `Tensor` object is.""" + are successfully added to the queue, as well as the `Tensor` object.""" - with qml._queuing.AnnotatedQueue() as q: + with AnnotatedQueue() as q: A = qml.PauliZ(0) B = qml.PauliY(1) tensor_op = A @ B - assert q.queue == [A, B] + assert q.queue == [A, B, tensor_op] assert tensor_op.obs == [A, B] - assert all(not isinstance(op, qml.operation.Tensor) for op in q.queue) def test_get_info(self): """Test that get_info correctly returns an annotation""" A = qml.RZ(0.5, wires=1) - with qml._queuing.AnnotatedQueue() as q: + with AnnotatedQueue() as q: q.append(A, inv=True) - assert q.get_info(A) == {"inv": True} + assert q._get_info(A) == {"inv": True} def test_get_info_error(self): """Test that an exception is raised if get_info is called for a non-existent object""" - with qml._queuing.AnnotatedQueue() as q: + with AnnotatedQueue() as q: A = qml.PauliZ(0) B = qml.PauliY(1) with pytest.raises(ValueError, match="not in the queue"): - q.get_info(B) + q._get_info(B) + + def test_get_info_none(self): + """Test that get_info returns None if there is no active queuing context""" + A = qml.RZ(0.5, wires=1) + + with AnnotatedQueue() as q: + q.append(A, inv=True) + + assert QueuingContext.get_info(A) is None def test_update_info(self): """Test that update_info correctly updates an annotation""" A = qml.RZ(0.5, wires=1) - with qml._queuing.AnnotatedQueue() as q: + with AnnotatedQueue() as q: q.append(A, inv=True) - assert qml.QueuingContext.get_info(A) == {"inv": True} + assert QueuingContext.get_info(A) == {"inv": True} - assert q.get_info(A) == {"inv": True} + assert q._get_info(A) == {"inv": True} q._update_info(A, inv=False, owner=None) - assert q.get_info(A) == {"inv": False, "owner": None} + assert q._get_info(A) == {"inv": False, "owner": None} def test_update_error(self): """Test that an exception is raised if get_info is called for a non-existent object""" - with qml._queuing.AnnotatedQueue() as q: + with AnnotatedQueue() as q: A = qml.PauliZ(0) B = qml.PauliY(1) @@ -406,22 +343,119 @@ def __init__(self, *args): self.queue() def queue(self): - qml.QueuingContext.append(self, owns=tuple(self.obs)) + QueuingContext.append(self, owns=tuple(self.obs)) for o in self.obs: try: - qml.QueuingContext.update_info(o, owner=self) + QueuingContext.update_info(o, owner=self) except AttributeError: pass return self - with qml._queuing.AnnotatedQueue() as q: + with AnnotatedQueue() as q: A = qml.PauliZ(0) B = qml.PauliY(1) tensor_op = AnnotatingTensor(A, B) assert q.queue == [A, B, tensor_op] - assert q.get_info(A) == {"owner": tensor_op} - assert q.get_info(B) == {"owner": tensor_op} - assert q.get_info(tensor_op) == {"owns": (A, B)} + assert q._get_info(A) == {"owner": tensor_op} + assert q._get_info(B) == {"owner": tensor_op} + assert q._get_info(tensor_op) == {"owns": (A, B)} + + +class TestOperationRecorder: + """Test the OperationRecorder class.""" + + def test_circuit_integration(self): + """Tests that the OperationRecorder integrates well with the + core behaviour of PennyLane.""" + expected_output = ( + "Operations\n" + + "==========\n" + + "PauliY(wires=[0])\n" + + "PauliY(wires=[1])\n" + + "RZ(tensor(0.4, requires_grad=True), wires=[0])\n" + + "RZ(tensor(0.4, requires_grad=True), wires=[1])\n" + + "CNOT(wires=[0, 1])\n" + + "\n" + + "Observables\n" + + "==========\n" + ) + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(a, b, c): + qml.RX(a, wires=0) + qml.RY(b, wires=1) + + with qml._queuing.OperationRecorder() as recorder: + ops = [ + qml.PauliY(0), + qml.PauliY(1), + qml.RZ(c, wires=0), + qml.RZ(c, wires=1), + qml.CNOT(wires=[0, 1]), + ] + + assert str(recorder) == expected_output + assert recorder.queue == ops + + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + + circuit(0.1, 0.2, 0.4) + + def test_template_integration(self): + """Tests that the OperationRecorder integrates well with the + core behaviour of PennyLane.""" + expected_output = ( + "Operations\n" + + "==========\n" + + "RZ(0, wires=[0])\n" + + "RZ(3, wires=[0])\n" + + "RZ(6, wires=[0])\n" + + "RZ(9, wires=[0])\n" + + "RZ(12, wires=[0])\n" + + "\n" + + "Observables\n" + + "==========\n" + ) + + def template(x): + for i in range(5): + qml.RZ(i * x, wires=0) + + with qml._queuing.OperationRecorder() as recorder: + template(3) + + assert str(recorder) == expected_output + + def test_template_with_return_integration(self): + """Tests that the OperationRecorder integrates well with the + core behaviour of PennyLane.""" + expected_output = ( + "Operations\n" + + "==========\n" + + "RZ(0, wires=[0])\n" + + "RZ(3, wires=[0])\n" + + "RZ(6, wires=[0])\n" + + "RZ(9, wires=[0])\n" + + "RZ(12, wires=[0])\n" + + "\n" + + "Observables\n" + + "==========\n" + + "var(PauliZ(wires=[0]))\n" + + "sample(PauliX(wires=[1]))\n" + ) + + def template(x): + for i in range(5): + qml.RZ(i * x, wires=0) + + return qml.var(qml.PauliZ(0)), qml.sample(qml.PauliX(1)) + + with qml._queuing.OperationRecorder() as recorder: + template(3) + + assert str(recorder) == expected_output diff --git a/tests/test_variable.py b/tests/test_variable.py deleted file mode 100644 index 2cb249295bc..00000000000 --- a/tests/test_variable.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for :mod:`pennylane.variable`. -""" -import pytest -import numpy.random as nr - -from pennylane.variable import Variable - - -# make test deterministic -nr.seed(42) - -n = 10 -keyword_par_names = ['foo', 'bar'] -par_inds = [0, 9] -par_mults = [1, 0.4, -2.7] - - -@pytest.fixture(scope="function") -def par_positional(): - "QNode: positional parameters" - temp = nr.randn(n) - Variable.positional_arg_values = temp # set the values - return temp - -@pytest.fixture(scope="function") -def par_keyword(): - "QNode: keyword parameters" - temp = {name: nr.randn(n) for name in keyword_par_names} - Variable.kwarg_values = temp # set the values - return temp - - -def test_variable_repr(): - """Variable string rep.""" - p = Variable(0) - assert repr(p) == "" - assert repr(-p) == "" - assert repr(1.2 * p * 0.4) == "" - assert repr(1.2 * p / 2.5) == "" - - p = Variable(0, name="kw1") - assert repr(p) == "" - assert repr(-p) == "" - assert repr(1.2 * p * 0.4) == "" - assert repr(1.2 * p / 2.5) == "" - -def test_variable_str(): - """Variable informal string rep.""" - p = Variable(0) - assert str(p) == "Variable: name = None, idx = 0" - assert str(-p) == "Variable: name = None, idx = 0, * -1" - - p = Variable(0, name="kw1") - assert str(p) == "Variable: name = kw1, idx = 0" - assert str(2.1 * p) == "Variable: name = kw1, idx = 0, * 2.1" - -def variable_eval_asserts(v, p, m, tol): - """Check that variable evaluation (with scalar multiplication) yields the expected results.""" - assert v.val == p # normal evaluation - assert (m * v).val == m * p # left scalar mul - assert (v * m).val == m * p # right scalar mul - assert (v / m).val == pytest.approx(p / m, abs=tol) # right scalar div - assert (-v).val == -p # unary minus - assert (m * -v * m).val == -m**2 * p # compound expression - - -@pytest.mark.parametrize("ind", par_inds) -@pytest.mark.parametrize("mult", par_mults) -def test_variable_val(par_positional, ind, mult, tol): - """Positional variable evaluation.""" - v = Variable(ind) - - assert v.name is None - assert v.mult == 1 - assert v.idx == ind - variable_eval_asserts(v, par_positional[ind], mult, tol) - - -@pytest.mark.parametrize("ind", par_inds) -@pytest.mark.parametrize("mult", par_mults) -@pytest.mark.parametrize("name", keyword_par_names) -def test_keyword_variable(par_keyword, name, ind, mult, tol): - """Keyword variable evaluation.""" - v = Variable(ind, name, is_kwarg=True) - - assert v.name == name - assert v.mult == 1 - assert v.idx == ind - variable_eval_asserts(v, par_keyword[name][ind], mult, tol) diff --git a/tests/test_vqe.py b/tests/test_vqe.py index 373c46cee12..b00984859c4 100644 --- a/tests/test_vqe.py +++ b/tests/test_vqe.py @@ -643,7 +643,6 @@ def test_arithmetic_errors(self): H -= A -@pytest.mark.usefixtures("tape_mode") class TestVQE: """Test the core functionality of the VQE module""" @@ -738,24 +737,10 @@ def test_passing_kwargs(self, coeffs, observables, expected): assert qnode.h == 123 assert qnode.order == 2 - def test_optimize_outside_tape_mode(self): - """Test that an error is raised if observable optimization is requested outside of tape - mode.""" - if qml.tape_mode_active(): - pytest.skip("This test is only intended for non-tape mode") - - dev = qml.device("default.qubit", wires=2) - hamiltonian = qml.vqe.Hamiltonian([1], [qml.PauliZ(0)]) - - with pytest.raises(ValueError, match="Observable optimization is only supported in tape"): - qml.ExpvalCost(lambda params, **kwargs: None, hamiltonian, dev, optimize=True) - @pytest.mark.parametrize("interface", ["tf", "torch", "autograd"]) def test_optimize(self, interface, tf_support, torch_support): """Test that an ExpvalCost with observable optimization gives the same result as another ExpvalCost without observable optimization.""" - if not qml.tape_mode_active(): - pytest.skip("This test is only intended for tape mode") if interface == "tf" and not tf_support: pytest.skip("This test requires TensorFlow") if interface == "torch" and not torch_support: @@ -798,9 +783,6 @@ def test_optimize(self, interface, tf_support, torch_support): def test_optimize_grad(self): """Test that the gradient of ExpvalCost is accessible and correct when using observable optimization and the autograd interface.""" - if not qml.tape_mode_active(): - pytest.skip("This test is only intended for tape mode") - dev = qml.device("default.qubit", wires=4) hamiltonian = big_hamiltonian @@ -827,8 +809,6 @@ def test_optimize_grad(self): def test_optimize_grad_torch(self, torch_support): """Test that the gradient of ExpvalCost is accessible and correct when using observable optimization and the Torch interface.""" - if not qml.tape_mode_active(): - pytest.skip("This test is only intended for tape mode") if not torch_support: pytest.skip("This test requires Torch") @@ -854,8 +834,6 @@ def test_optimize_grad_torch(self, torch_support): def test_optimize_grad_tf(self, tf_support): """Test that the gradient of ExpvalCost is accessible and correct when using observable optimization and the TensorFlow interface.""" - if not qml.tape_mode_active(): - pytest.skip("This test is only intended for tape mode") if not tf_support: pytest.skip("This test requires TensorFlow") @@ -875,11 +853,8 @@ def test_optimize_grad_tf(self, tf_support): assert np.allclose(dc, big_hamiltonian_grad) - def test_metric_tensor_tape_mode(self): - """Test that the metric tensor can be calculated in tape mode, and that it is equal to a - metric tensor calculated in non-tape mode.""" - if not qml.tape_mode_active(): - pytest.skip("This test is only intended for tape mode") + def test_metric_tensor(self): + """Test that the metric tensor can be calculated.""" dev = qml.device("default.qubit", wires=2) p = np.array([1., 1., 1.]) @@ -893,24 +868,8 @@ def ansatz(params, **kwargs): h = qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(1)]) qnodes = qml.ExpvalCost(ansatz, h, dev) mt = qml.metric_tensor(qnodes)(p) - assert qml.tape_mode_active() # Check that tape mode is still active - - try: - qml.disable_tape() - - @qml.qnode(dev) - def circuit(params): - qml.RX(params[0], wires=0) - qml.RY(params[1], wires=0) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(params[2], wires=1) - return qml.expval(qml.PauliZ(0)) - - mt2 = circuit.metric_tensor([p]) - finally: - qml.enable_tape() - - assert np.allclose(mt, mt2) + assert mt.shape == (3, 3) + assert isinstance(md, np.ndarray) def test_multiple_devices(self, mocker): """Test that passing multiple devices to ExpvalCost works correctly""" @@ -940,9 +899,6 @@ def test_multiple_devices(self, mocker): def test_multiple_devices_opt_true(self): """Test if a ValueError is raised when multiple devices are passed when optimize=True.""" - if not qml.tape_mode_active(): - pytest.skip("This test is only intended for tape mode") - dev = [qml.device("default.qubit", wires=2), qml.device("default.qubit", wires=2)] h = qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(1)]) @@ -951,7 +907,6 @@ def test_multiple_devices_opt_true(self): qml.ExpvalCost(qml.templates.StronglyEntanglingLayers, h, dev, optimize=True) -@pytest.mark.usefixtures("tape_mode") class TestAutogradInterface: """Tests for the Autograd interface (and the NumPy interface for backward compatibility)""" @@ -996,7 +951,6 @@ def ansatz(params, **kwargs): assert np.allclose(res, expected, atol=tol, rtol=0) -@pytest.mark.usefixtures("tape_mode") @pytest.mark.usefixtures("skip_if_no_torch_support") class TestTorchInterface: """Tests for the PyTorch interface""" @@ -1041,7 +995,6 @@ def ansatz(params, **kwargs): assert np.allclose(res, expected, atol=tol, rtol=0) -@pytest.mark.usefixtures("tape_mode") @pytest.mark.usefixtures("skip_if_no_tf_support") class TestTFInterface: """Tests for the TF interface""" @@ -1088,7 +1041,6 @@ def ansatz(params, **kwargs): assert np.allclose(res, expected, atol=tol, rtol=0) -@pytest.mark.usefixtures("tape_mode") @pytest.mark.usefixtures("skip_if_no_tf_support") @pytest.mark.usefixtures("skip_if_no_torch_support") class TestMultipleInterfaceIntegration: diff --git a/tests/tape/transforms/test_metric_tensor.py b/tests/transforms/test_metric_tensor.py similarity index 97% rename from tests/tape/transforms/test_metric_tensor.py rename to tests/transforms/test_metric_tensor.py index bf6c27a3181..7fd09c865f1 100644 --- a/tests/tape/transforms/test_metric_tensor.py +++ b/tests/transforms/test_metric_tensor.py @@ -22,9 +22,6 @@ from gate_data import Y, Z -pytestmark = pytest.mark.usefixtures("in_tape_mode") - - class TestMetricTensor: """Tests for metric tensor subcircuit construction and evaluation""" From bab8c0823350d9a4ea156355768c548669538c1d Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Fri, 29 Jan 2021 22:12:42 +0800 Subject: [PATCH 02/64] more --- pennylane/__init__.py | 27 ++++---- pennylane/_device.py | 5 +- pennylane/_qubit_device.py | 11 ++-- pennylane/measure.py | 7 +- pennylane/ops/qubit.py | 2 +- pennylane/queuing.py | 76 ---------------------- pennylane/tape/__init__.py | 1 + pennylane/tape/tape.py | 3 +- pennylane/templates/decorator.py | 4 +- pennylane/utils.py | 3 +- tests/templates/test_broadcast.py | 12 ++-- tests/templates/test_embeddings.py | 10 +-- tests/templates/test_layers.py | 34 +++++----- tests/templates/test_state_preparations.py | 4 +- tests/templates/test_subroutines.py | 28 ++++---- tests/test_operation.py | 12 ++-- tests/test_utils.py | 12 ++-- 17 files changed, 87 insertions(+), 164 deletions(-) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index cad288e9826..1ddd32b3925 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -26,31 +26,36 @@ from .queuing import QueuingContext # pylint: disable=wrong-import-order import pennylane.operation -import pennylane.math -import pennylane.tape -import pennylane.init import pennylane.templates -import pennylane.qnn -import pennylane.qaoa as qaoa -from pennylane.templates import template, broadcast, layer -from pennylane.about import about -from pennylane.vqe import Hamiltonian, ExpvalCost, VQECost -from pennylane.transforms import draw, metric_tensor, measurement_grouping + from .circuit_graph import CircuitGraph from .configuration import Configuration from ._device import Device, DeviceError -from .collections import apply, map, sum, dot, QNodeCollection from ._qubit_device import QubitDevice from .measure import expval, var, sample, state, density_matrix, probs from .ops import * from .optimize import * -from .qnodes import qnode, QNode, QuantumFunctionError +from .qnode import qnode, QNode, QuantumFunctionError from .utils import inv from ._version import __version__ from .io import * from ._grad import jacobian, grad +import pennylane.math +import pennylane.tape + +import pennylane.init +import pennylane.qnn +import pennylane.qaoa as qaoa + +from pennylane.collections import apply, map, sum, dot, QNodeCollection +from pennylane.templates import template, broadcast, layer +from pennylane.about import about +from pennylane.vqe import Hamiltonian, ExpvalCost, VQECost +from pennylane.transforms import draw, metric_tensor, measurement_grouping + + # Look for an existing configuration file default_config = Configuration("config.toml") diff --git a/pennylane/_device.py b/pennylane/_device.py index 355ffe3dd69..c656e2675a7 100644 --- a/pennylane/_device.py +++ b/pennylane/_device.py @@ -32,7 +32,6 @@ Probability, Tensor, ) -from pennylane.qnodes import QuantumFunctionError from pennylane.wires import Wires, WireError @@ -313,10 +312,10 @@ def execute(self, queue, observables, parameters={}, **kwargs): results.append(list(self.probability(wires=wires).values())) elif obs.return_type is State: - raise QuantumFunctionError("Returning the state is not supported") + raise qml.QuantumFunctionError("Returning the state is not supported") elif obs.return_type is not None: - raise QuantumFunctionError( + raise qml.QuantumFunctionError( "Unsupported return type specified for observable {}".format(obs.name) ) diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index 0a9d06e49b3..3bfa9425b44 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -33,7 +33,6 @@ State, operation_derivative, ) -from pennylane.qnodes import QuantumFunctionError from pennylane import Device from pennylane.math import sum as qmlsum from pennylane.wires import Wires @@ -347,12 +346,12 @@ def statistics(self, observables): elif obs.return_type is State: if len(observables) > 1: - raise QuantumFunctionError( + raise qml.QuantumFunctionError( "The state or density matrix cannot be returned in combination" " with other return types" ) if self.wires.labels != tuple(range(self.num_wires)): - raise QuantumFunctionError( + raise qml.QuantumFunctionError( "Returning the state is not supported when using custom wire labels" ) # Check if the state is accessible and decide to return the state or the density @@ -360,7 +359,7 @@ def statistics(self, observables): results.append(self.access_state(wires=obs.wires)) elif obs.return_type is not None: - raise QuantumFunctionError( + raise qml.QuantumFunctionError( "Unsupported return type specified for observable {}".format(obs.name) ) @@ -379,12 +378,12 @@ def access_state(self, wires=None): array or tensor: the state or the density matrix of the device """ if not self.capabilities().get("returns_state"): - raise QuantumFunctionError("The current device is not capable of returning the state") + raise qml.QuantumFunctionError("The current device is not capable of returning the state") state = getattr(self, "state", None) if state is None: - raise QuantumFunctionError("The state is not available in the current device") + raise qml.QuantumFunctionError("The state is not available in the current device") if wires: density_matrix = self.density_matrix(wires) diff --git a/pennylane/measure.py b/pennylane/measure.py index 11c349fba9d..c7bd27c9724 100644 --- a/pennylane/measure.py +++ b/pennylane/measure.py @@ -23,7 +23,6 @@ import pennylane as qml from pennylane.operation import Expectation, Observable, Probability, Sample, State, Variance -from pennylane.qnodes import QuantumFunctionError from pennylane.wires import Wires @@ -230,7 +229,7 @@ def circuit(x): QuantumFunctionError: `op` is not an instance of :class:`~.Observable` """ if not isinstance(op, Observable): - raise QuantumFunctionError( + raise qml.QuantumFunctionError( "{} is not an observable: cannot be used with expval".format(op.name) ) @@ -265,7 +264,7 @@ def circuit(x): QuantumFunctionError: `op` is not an instance of :class:`~.Observable` """ if not isinstance(op, Observable): - raise QuantumFunctionError( + raise qml.QuantumFunctionError( "{} is not an observable: cannot be used with var".format(op.name) ) @@ -306,7 +305,7 @@ def circuit(x): QuantumFunctionError: `op` is not an instance of :class:`~.Observable` """ if not isinstance(op, Observable): - raise QuantumFunctionError( + raise qml.QuantumFunctionError( "{} is not an observable: cannot be used with sample".format(op.name) ) diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index 2a6e2483690..255677f94ac 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -21,7 +21,7 @@ import functools import numpy as np -from pennylane.templates import template +from pennylane.templates.decorator import template from pennylane.operation import AnyWires, Observable, Operation, DiagonalOperation from pennylane.templates.state_preparations import BasisStatePreparation, MottonenStatePreparation from pennylane.utils import pauli_eigs, expand diff --git a/pennylane/queuing.py b/pennylane/queuing.py index e47a358c353..3657257e1f4 100644 --- a/pennylane/queuing.py +++ b/pennylane/queuing.py @@ -246,79 +246,3 @@ def _get_info(self, obj): def queue(self): """Returns a list of objects in the annotated queue""" return list(self._queue.keys()) - - -class OperationRecorder(QuantumTape): - """A template and quantum function inspector, - allowing easy introspection of operators that have been - applied without requiring a QNode. - - **Example**: - - The OperationRecorder is a context manager. Executing templates - or quantum functions stores applied operators in the - recorder, which can then be printed. - - >>> weights = qml.init.strong_ent_layers_normal(n_layers=1, n_wires=2) - >>> - >>> with OperationRecorder() as rec: - >>> qml.templates.layers.StronglyEntanglingLayers(*weights, wires=[0, 1]) - >>> - >>> print(rec) - Operations - ========== - Rot(-0.10832656163640327, 0.14429091013664083, -0.010835826725765343, wires=[0]) - Rot(-0.11254523669444501, 0.0947222564914006, -0.09139600968423377, wires=[1]) - CNOT(wires=[0, 1]) - CNOT(wires=[1, 0]) - - Alternatively, the :attr:`~.OperationRecorder.queue` attribute can be used - to directly access the applied :class:`~.Operation` and :class:`~.Observable` - objects. - - Attributes: - queue (List[Operator]): list of operators applied within - the OperatorRecorder context, includes operations and observables - operations (List[Operation]): list of operations applied within - the OperatorRecorder context - observables (List[Observable]): list of observables applied within - the OperatorRecorder context - """ - - def __init__(self): - super().__init__() - self.ops = None - self.obs = None - - def _process_queue(self): - super()._process_queue() - - for obj, info in self._queue.items(): - QueuingContext.append(obj, **info) - - # remove the operation recorder from the queuing - # context - QueuingContext.remove(self) - - new_tape = self.expand(depth=5, stop_at=lambda obj: not isinstance(obj, QuantumTape)) - self.ops = new_tape.operations - self.obs = new_tape.observables - - def __str__(self): - output = "" - output += "Operations\n" - output += "==========\n" - for op in self.ops: - output += repr(op) + "\n" - - output += "\n" - output += "Observables\n" - output += "==========\n" - for op in self.obs: - output += repr(op) + "\n" - - return output - - @property - def queue(self): - return self.ops + self.obs diff --git a/pennylane/tape/__init__.py b/pennylane/tape/__init__.py index ee2b309c1de..099854a2635 100644 --- a/pennylane/tape/__init__.py +++ b/pennylane/tape/__init__.py @@ -20,3 +20,4 @@ from .cv_param_shift import CVParamShiftTape from .qubit_param_shift import QubitParamShiftTape from .reversible import ReversibleTape +from .operation_recorder import OperationRecorder diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py index 09395e0af76..244b90de4b1 100644 --- a/pennylane/tape/tape.py +++ b/pennylane/tape/tape.py @@ -23,7 +23,6 @@ import numpy as np import pennylane as qml -from pennylane.grouping import diagonalize_qwc_pauli_words from pennylane.queuing import AnnotatedQueue, QueuingContext from pennylane.operation import Sample @@ -102,7 +101,7 @@ def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False): # expansion acts on the original tape in place. if tape._obs_sharing_wires: try: - rotations, diag_obs = diagonalize_qwc_pauli_words(tape._obs_sharing_wires) + rotations, diag_obs = qml.grouping.diagonalize_qwc_pauli_words(tape._obs_sharing_wires) except ValueError as e: raise qml.QuantumFunctionError( "Only observables that are qubit-wise commuting " diff --git a/pennylane/templates/decorator.py b/pennylane/templates/decorator.py index 4fc15f13b93..bc626bd6d66 100644 --- a/pennylane/templates/decorator.py +++ b/pennylane/templates/decorator.py @@ -16,8 +16,6 @@ """ from functools import wraps -from pennylane.queuing import OperationRecorder - def template(func): """Register a quantum template with PennyLane. @@ -59,7 +57,7 @@ def circuit(): @wraps(func) def wrapper(*args, **kwargs): - with OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: func(*args, **kwargs) return rec.queue diff --git a/pennylane/utils.py b/pennylane/utils.py index af5a0254123..4bfe87dfd83 100644 --- a/pennylane/utils.py +++ b/pennylane/utils.py @@ -27,7 +27,6 @@ import numpy as np import pennylane as qml -from pennylane.variable import Variable def decompose_hamiltonian(H, hide_identity=False): @@ -147,7 +146,7 @@ def _unflatten(flat, model): Union[array, list, Any], array: first elements of flat arranged into the nested structure of model, unused elements of flat """ - if isinstance(model, (numbers.Number, Variable, str)): + if isinstance(model, (numbers.Number, str)): return flat[0], flat[1:] if isinstance(model, np.ndarray): diff --git a/tests/templates/test_broadcast.py b/tests/templates/test_broadcast.py index 3254e2a9b19..fa4d989b818 100644 --- a/tests/templates/test_broadcast.py +++ b/tests/templates/test_broadcast.py @@ -135,7 +135,7 @@ class TestBuiltinPatterns: def test_correct_queue_for_gate_unitary(self, unitary, parameters): """Tests that correct gate queue is created when 'unitary' is a single gate.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: broadcast(unitary=unitary, pattern="single", wires=range(3), parameters=parameters) for gate in rec.queue: @@ -148,7 +148,7 @@ def test_correct_queue_for_gate_unitary(self, unitary, parameters): def test_correct_queue_for_template_unitary(self, unitary, gates, parameters): """Tests that correct gate queue is created when 'unitary' is a template.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: broadcast(unitary=unitary, pattern="single", wires=range(3), parameters=parameters) first_gate = gates[0] @@ -166,7 +166,7 @@ def test_correct_queue_for_template_unitary(self, unitary, gates, parameters): def test_correct_queue_for_template_unitary_with_keyword(self, template, kwarg, target_queue, parameters): """Tests that correct gate queue is created when 'unitary' is a template that uses a keyword.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: broadcast(unitary=template, pattern="single", wires=range(2), parameters=parameters, kwargs={'a': kwarg}) @@ -179,10 +179,10 @@ def test_correct_queue_for_template_unitary_with_keyword(self, template, kwarg, def test_correct_queue_same_gate_unitary_different_parameter_formats(self, pars1, pars2, gate): """Tests that specific parameter inputs have the same output.""" - with pennylane._queuing.OperationRecorder() as rec1: + with pennylane.tape.OperationRecorder() as rec1: broadcast(unitary=gate, pattern="single", wires=range(3), parameters=pars1) - with pennylane._queuing.OperationRecorder() as rec2: + with pennylane.tape.OperationRecorder() as rec2: broadcast(unitary=gate, pattern="single", wires=range(3), parameters=pars2) for g1, g2 in zip(rec1.queue, rec2.queue): @@ -192,7 +192,7 @@ def test_correct_queue_same_gate_unitary_different_parameter_formats(self, pars1 def test_correct_parameters_in_queue(self, pattern, n_wires, gate, parameters): """Tests that gate queue has correct parameters.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: broadcast(unitary=gate, pattern=pattern, wires=range(n_wires), parameters=parameters) for target_par, g in zip(parameters, rec.queue): diff --git a/tests/templates/test_embeddings.py b/tests/templates/test_embeddings.py index e9dab158888..3d9e1d3f3fb 100644 --- a/tests/templates/test_embeddings.py +++ b/tests/templates/test_embeddings.py @@ -419,7 +419,7 @@ class TestIQPEmbedding: def test_queue_default_pattern(self, n_wires, expected_queue, n_repeats): """Checks the queue for the default pattern.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.templates.IQPEmbedding(features=list(range(n_wires)), wires=range(n_wires), n_repeats=n_repeats) expected_queue = expected_queue * n_repeats @@ -436,7 +436,7 @@ def test_queue_default_pattern(self, n_wires, expected_queue, n_repeats): def test_queue_parameters(self, features, expected_params, wires): """Checks the queued parameters, for consecutive and non-consecutive ``wires`` argument.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.templates.IQPEmbedding(features=features, wires=wires) # compare all nonempty gate parameters to expected ones @@ -454,7 +454,7 @@ def test_queue_correct_wires(self, wires, expected_queue_wires): """Checks the queued wires for a consecutive and non-consecutive sequence of indices in the ``wires`` argument.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.templates.IQPEmbedding(features=list(range(3)), wires=wires) # compare all gate wires to expected ones @@ -466,7 +466,7 @@ def test_queue_correct_wires(self, wires, expected_queue_wires): def test_wires_custom_pattern(self, pattern): """Checks the queue for a custom pattern.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.templates.IQPEmbedding(features=list(range(4)), wires=range(4), pattern=pattern) counter = 0 @@ -521,7 +521,7 @@ class TestQAOAEmbedding: def test_queue(self, n_wires, weight_shape, expected_queue): """Checks the queue for the default settings.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: QAOAEmbedding(features=list(range(n_wires)), weights=np.zeros(shape=weight_shape), wires=range(n_wires)) for gate, expected_gate in zip(rec.queue, expected_queue): diff --git a/tests/templates/test_layers.py b/tests/templates/test_layers.py index 2e35c5e9bcb..f74e74a14f9 100644 --- a/tests/templates/test_layers.py +++ b/tests/templates/test_layers.py @@ -120,7 +120,7 @@ def weights(self): def test_cvneuralnet_uses_correct_weights(self, weights): """Tests that the CVNeuralNetLayers template uses the weigh parameters correctly.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: CVNeuralNetLayers(*weights, wires=range(4)) # Test that gates appear in the right order for each layer: @@ -204,7 +204,7 @@ class TestStronglyEntangling: @pytest.mark.parametrize("n_layers", range(1, 4)) def test_single_qubit(self, n_layers): weights = np.zeros((n_layers, 1, 3)) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: StronglyEntanglingLayers(weights, wires=range(1)) assert len(rec.queue) == n_layers @@ -219,7 +219,7 @@ def test_uses_correct_weights(self, n_subsystems): weights = np.random.randn(n_layers, num_wires, 3) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: StronglyEntanglingLayers(weights, wires=range(num_wires)) # Test that gates appear in the right order @@ -246,7 +246,7 @@ def test_uses_correct_number_of_imprimitives(self, n_layers, n_subsystems): imprimitive = CZ weights = np.random.randn(n_layers, n_subsystems, 3) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: StronglyEntanglingLayers( weights=weights, wires=range(n_subsystems), imprimitive=imprimitive ) @@ -363,7 +363,7 @@ def test_random_layers_nlayers(self, n_layers): impr = CNOT weights = np.random.randn(n_layers, n_rots) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: RandomLayers(weights=weights, wires=range(n_wires)) types = [type(q) for q in rec.queue] @@ -376,7 +376,7 @@ def test_random_layer_ratio_imprimitive(self, ratio): impr = CNOT weights = np.random.randn(n_rots) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: random_layer( weights=weights, wires=Wires(range(n_wires)), @@ -395,7 +395,7 @@ def test_random_layer_gate_types(self, n_subsystems, impr, rots): n_rots = 20 weights = np.random.randn(n_rots) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: random_layer( weights=weights, wires=Wires(range(n_subsystems)), @@ -415,7 +415,7 @@ def test_random_layer_numgates(self, n_subsystems): n_rots = 5 weights = np.random.randn(n_rots) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: random_layer( weights=weights, wires=Wires(range(n_subsystems)), @@ -433,7 +433,7 @@ def test_random_layer_randomwires(self, n_subsystems): n_rots = 500 weights = np.random.randn(n_rots) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: random_layer( weights=weights, wires=Wires(range(n_subsystems)), @@ -454,7 +454,7 @@ def test_random_layer_weights(self, n_subsystems, tol): n_rots = 5 weights = np.random.randn(n_rots) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: random_layer( weights=weights, wires=Wires(range(n_subsystems)), @@ -515,7 +515,7 @@ def test_circuit_queue(self, n_wires, n_layers, shape_weights): initial_layer = np.random.randn(n_wires) weights = np.random.randn(*shape_weights) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: SimplifiedTwoDesign(initial_layer, weights, wires=range(n_wires)) # Test that gates appear in the right order @@ -538,7 +538,7 @@ def test_circuit_parameters(self, n_wires, n_layers, shape_weights): initial_layer = np.random.randn(n_wires) weights = np.random.randn(*shape_weights) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: SimplifiedTwoDesign(initial_layer, weights, wires=range(n_wires)) # test the device parameters @@ -620,7 +620,7 @@ def test_circuit_queue(self, n_wires, n_cnots): weights = np.random.randn(n_layers, n_wires) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: BasicEntanglerLayers(weights, wires=range(n_wires)) # Test that gates appear in the right order @@ -639,7 +639,7 @@ def test_circuit_parameters(self, n_wires, n_cnots): weights = np.random.randn(n_layers, n_wires) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: BasicEntanglerLayers(weights, wires=range(n_wires)) # test the device parameters @@ -660,7 +660,7 @@ def test_custom_rotation(self, rotation): n_wires = 4 weights = np.ones(shape=(n_layers, n_wires)) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: BasicEntanglerLayers(weights, wires=range(n_wires), rotation=rotation) # assert queue contains the custom rotations and CNOTs only @@ -734,7 +734,7 @@ def test_u2_operations(self, layers, qubits, init_state): [qml.RZ] * qubits + ([qml.CNOT] + [qml.CRX] + [qml.CNOT]) * (qubits - 1) ) * layers - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: ParticleConservingU2(weights, wires=range(qubits), init_state=init_state) # number of gates @@ -914,7 +914,7 @@ def test_particle_conserving_u1_operations(self): nm_wires = [wires.subset([l, l + 1]) for l in range(0, qubits - 1, 2)] nm_wires += [wires.subset([l, l + 1]) for l in range(1, qubits - 1, 2)] - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: ParticleConservingU1(weights, wires, init_state=np.array([1, 1, 0, 0])) assert gate_count == len(rec.queue) diff --git a/tests/templates/test_state_preparations.py b/tests/templates/test_state_preparations.py index e27d19860f1..e08641d230c 100644 --- a/tests/templates/test_state_preparations.py +++ b/tests/templates/test_state_preparations.py @@ -368,7 +368,7 @@ def test_correct_gates_single_wire(self): """Test that the correct gates are applied on a single wire.""" weights = np.array([0, 1], dtype=float) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: ArbitraryStatePreparation(weights, wires=[0]) assert rec.queue[0].name == "PauliRot" @@ -386,7 +386,7 @@ def test_correct_gates_two_wires(self): """Test that the correct gates are applied on on two wires.""" weights = np.array([0, 1, 2, 3, 4, 5], dtype=float) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: ArbitraryStatePreparation(weights, wires=[0, 1]) assert rec.queue[0].name == "PauliRot" diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py index e3d7e2129e7..f56de94be5e 100644 --- a/tests/templates/test_subroutines.py +++ b/tests/templates/test_subroutines.py @@ -148,12 +148,12 @@ def test_clements_beamsplitter_convention(self, tol): phi = [0.234] varphi = [0.42342, 0.1121] - with pennylane._queuing.OperationRecorder() as rec_rect: + with pennylane.tape.OperationRecorder() as rec_rect: Interferometer( theta, phi, varphi, mesh="rectangular", beamsplitter="clements", wires=wires ) - with pennylane._queuing.OperationRecorder() as rec_tria: + with pennylane.tape.OperationRecorder() as rec_tria: Interferometer( theta, phi, varphi, mesh="triangular", beamsplitter="clements", wires=wires ) @@ -177,7 +177,7 @@ def test_one_mode(self, tol): """Test that a one mode interferometer correctly gives a rotation gate""" varphi = [0.42342] - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: Interferometer(theta=[], phi=[], varphi=varphi, wires=0) assert len(rec.queue) == 1 @@ -194,7 +194,7 @@ def test_two_mode_rect(self, tol): phi = [0.234] varphi = [0.42342, 0.1121] - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: Interferometer(theta, phi, varphi, wires=wires) isinstance(rec.queue[0], qml.Beamsplitter) @@ -216,7 +216,7 @@ def test_two_mode_triangular(self, tol): phi = [0.234] varphi = [0.42342, 0.1121] - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: Interferometer(theta, phi, varphi, mesh="triangular", wires=wires) assert len(rec.queue) == 3 @@ -239,10 +239,10 @@ def test_three_mode(self, tol): phi = [0.234, 0.324, 0.234] varphi = [0.42342, 0.234, 0.1121] - with pennylane._queuing.OperationRecorder() as rec_rect: + with pennylane.tape.OperationRecorder() as rec_rect: Interferometer(theta, phi, varphi, wires=wires) - with pennylane._queuing.OperationRecorder() as rec_tria: + with pennylane.tape.OperationRecorder() as rec_tria: Interferometer(theta, phi, varphi, wires=wires) for rec in [rec_rect, rec_tria]: @@ -270,7 +270,7 @@ 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] - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: Interferometer(theta, phi, varphi, wires=wires) assert len(rec.queue) == 10 @@ -296,7 +296,7 @@ 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] - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: Interferometer(theta, phi, varphi, mesh="triangular", wires=wires) assert len(rec.queue) == 10 @@ -439,7 +439,7 @@ def test_single_ex_unitary_operations(self, single_wires, ref_gates): sqg = 10 cnots = 4 * (len(single_wires) - 1) weight = np.pi / 3 - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: SingleExcitationUnitary(weight, wires=single_wires) assert len(rec.queue) == sqg + cnots @@ -513,7 +513,7 @@ def test_correct_gates_single_wire(self): """Test that the correct gates are applied on a single wire.""" weights = np.arange(3, dtype=float) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: ArbitraryUnitary(weights, wires=[0]) assert all(op.name == "PauliRot" and op.wires == Wires([0]) for op in rec.queue) @@ -528,7 +528,7 @@ def test_correct_gates_two_wires(self): """Test that the correct gates are applied on two wires.""" weights = np.arange(15, dtype=float) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: ArbitraryUnitary(weights, wires=[0, 1]) assert all(op.name == "PauliRot" and op.wires == Wires([0, 1]) for op in rec.queue) @@ -743,7 +743,7 @@ def test_double_ex_unitary_operations(self, wires1, wires2, ref_gates): sqg = 72 cnots = 16 * (len(wires1) - 1 + len(wires2) - 1 + 1) weight = np.pi / 3 - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: DoubleExcitationUnitary(weight, wires1=wires1, wires2=wires2) assert len(rec.queue) == sqg + cnots @@ -905,7 +905,7 @@ def test_uccsd_operations(self, s_wires, d_wires, weights, ref_gates): ref_state = np.array([1, 1, 0, 0, 0, 0]) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: UCCSD(weights, wires, s_wires=s_wires, d_wires=d_wires, init_state=ref_state) assert len(rec.queue) == sqg + cnots + 1 diff --git a/tests/test_operation.py b/tests/test_operation.py index 66d7cbf1ff6..f851f652c07 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -1164,7 +1164,7 @@ def test_rotation_decomposition(self): theta = 0.654 omega = -5.43 - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.Rot.decomposition(phi, theta, omega, wires=0) assert len(rec.queue) == 3 @@ -1183,7 +1183,7 @@ def test_crx_decomposition(self): qubit rotation""" phi = 0.432 - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.CRX.decomposition(phi, wires=[0, 1]) assert len(rec.queue) == 6 @@ -1236,7 +1236,7 @@ def test_cry_decomposition(self): operation_wires = [0, 1] - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.CRY.decomposition(phi, wires=operation_wires) assert len(rec.queue) == 4 @@ -1274,7 +1274,7 @@ def test_crz_decomposition(self): operation_wires = [0, 1] - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.CRZ.decomposition(phi, wires=operation_wires) assert len(rec.queue) == 4 @@ -1310,7 +1310,7 @@ def test_U2_decomposition(self): phi = 0.432 lam = 0.654 - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.U2.decomposition(phi, lam, wires=0) assert len(rec.queue) == 3 @@ -1330,7 +1330,7 @@ def test_U3_decomposition(self): phi = 0.432 lam = 0.654 - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.U3.decomposition(theta, phi, lam, wires=0) assert len(rec.queue) == 3 diff --git a/tests/test_utils.py b/tests/test_utils.py index 01dadab29f9..99416348cdf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -516,7 +516,7 @@ def test_template_double_inversion(self): def test_inversion_with_context(self): """Test that a sequence of operations is properly inverted when a context is present.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.Hadamard(wires=[0]) qml.CNOT(wires=[0, 1]) pu.inv([qml.RX(1, wires=[0]), qml.RY(2, wires=[0]), qml.RZ(3, wires=[0])]) @@ -543,7 +543,7 @@ def test_non_queued_inversion_with_context(self): Test that this also works for operations that were not queued.""" inv_ops = [qml.RX(1, wires=[0]), qml.RY(2, wires=[0]), qml.RZ(3, wires=[0])] - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.Hadamard(wires=[0]) qml.CNOT(wires=[0, 1]) pu.inv(inv_ops) @@ -571,7 +571,7 @@ def test_mixed_inversion_with_context(self): X0 = qml.PauliX(0) Z0 = qml.PauliZ(0) - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.Hadamard(wires=[0]) qml.CNOT(wires=[0, 1]) pu.inv([X0, qml.RX(1, wires=[0]), Z0, qml.RY(2, wires=[0])]) @@ -600,8 +600,8 @@ def test_mixed_inversion_with_nested_context(self): X0 = qml.PauliX(0) Z0 = qml.PauliZ(0) - with pennylane._queuing.OperationRecorder() as rec1: - with pennylane._queuing.OperationRecorder() as rec2: + with pennylane.tape.OperationRecorder() as rec1: + with pennylane.tape.OperationRecorder() as rec2: qml.Hadamard(wires=[0]) qml.CNOT(wires=[0, 1]) pu.inv([X0, qml.RX(1, wires=[0]), Z0, qml.RY(2, wires=[0])]) @@ -631,7 +631,7 @@ def test_mixed_inversion_with_nested_context(self): def test_template_inversion_with_context(self): """Test that a template is properly inverted when a context is present.""" - with pennylane._queuing.OperationRecorder() as rec: + with pennylane.tape.OperationRecorder() as rec: qml.Hadamard(wires=[0]) qml.CNOT(wires=[0, 1]) pu.inv(dummy_template([0, 1, 2])) From 6a62534dbb6df1bf030146f19ed4d22772743f47 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Sat, 30 Jan 2021 00:49:19 +0800 Subject: [PATCH 03/64] more tests --- doc/code/qml_tape.rst | 168 +----------------- pennylane/__init__.py | 6 +- pennylane/_qubit_device.py | 6 +- pennylane/circuit_graph.py | 8 +- pennylane/interfaces/torch.py | 27 ++- pennylane/operation.py | 20 ++- pennylane/optimize/qng.py | 5 +- pennylane/qnn/keras.py | 1 - pennylane/qnn/torch.py | 1 - pennylane/qnode.py | 6 +- pennylane/tape/tape.py | 17 -- pennylane/templates/decorator.py | 2 +- .../layers/particle_conserving_u1.py | 4 +- .../templates/layers/simplified_two_design.py | 4 +- .../templates/layers/strongly_entangling.py | 12 +- .../subroutines/arbitrary_unitary.py | 4 +- .../subroutines/double_excitation_unitary.py | 1 + pennylane/transforms/draw.py | 2 + pennylane/transforms/metric_tensor.py | 3 +- pennylane/utils.py | 2 +- tests/circuit_drawer/test_circuit_drawer.py | 8 +- .../circuit_graph/test_circuit_graph_hash.py | 146 +++++++-------- tests/circuit_graph/test_qasm.py | 10 +- tests/collections/test_collections.py | 60 +++---- tests/devices/test_default_qubit_jax.py | 5 - tests/devices/test_default_qubit_tf.py | 10 +- tests/interfaces/test_qnode_autograd.py | 5 +- tests/interfaces/test_qnode_tf.py | 6 +- tests/interfaces/test_qnode_torch.py | 5 +- tests/interfaces/test_tape_autograd.py | 4 +- tests/interfaces/test_tape_tf.py | 4 +- tests/interfaces/test_tape_torch.py | 5 +- tests/qnn/test_keras.py | 2 +- tests/tape/test_qnode.py | 17 +- tests/tape/test_qubit_param_shift.py | 4 +- tests/tape/test_reversible.py | 7 +- tests/tape/test_tape.py | 7 +- tests/templates/test_broadcast.py | 13 +- tests/templates/test_embeddings.py | 11 +- tests/templates/test_layer.py | 2 +- tests/templates/test_layers.py | 35 ++-- tests/templates/test_state_preparations.py | 5 +- tests/templates/test_subroutines.py | 47 +++-- tests/templates/test_templ_utils.py | 6 +- tests/test_about.py | 1 - tests/test_device.py | 8 +- tests/test_hermitian_edge_cases.py | 2 - tests/test_measure.py | 35 ++-- tests/test_numpy_wrapper.py | 4 +- tests/test_operation.py | 64 +------ tests/test_qaoa.py | 4 +- tests/test_qubit_device.py | 22 ++- tests/test_qubit_device_adjoint_jacobian.py | 2 +- tests/test_queuing.py | 9 +- tests/test_utils.py | 2 +- tests/test_vqe.py | 7 +- 56 files changed, 327 insertions(+), 556 deletions(-) diff --git a/doc/code/qml_tape.rst b/doc/code/qml_tape.rst index 43550846a48..68ca8039b97 100644 --- a/doc/code/qml_tape.rst +++ b/doc/code/qml_tape.rst @@ -1,176 +1,18 @@ qml.tape ======== -.. warning:: +Quantum tapes are responsible for recording quantum operations, executing devices, or computing +gradients. - The new PennyLane tape mode is **experimental**, and does not currently have feature-parity with - the existing QNode. `Feedback and bug reports - `__ are encouraged and will help improve the - new tape mode. - - Tape mode can be enabled globally via the :func:`~.enable_tape` function, without changing your - PennyLane code: - - >>> qml.enable_tape() - - Once enabled, tape mode can be disabled via :func:`~.disable_tape`. - - -Tape-mode QNodes ----------------- - -The PennyLane tape module provides a new QNode class, rewritten from the ground-up, -that uses a :class:`~.QuantumTape` to represent the internal variational quantum circuit. -Tape mode provides several advantages over the standard PennyLane QNode. - -* **Support for in-QNode classical processing**: Tape mode allows for differentiable classical - processing within the QNode. - - .. code-block:: python3 - - qml.enable_tape() - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev, interface="tf") - def circuit(p): - qml.RX(tf.sin(p[0])**2 + p[1], wires=0) - return qml.expval(qml.PauliZ(0)) - - The classical processing functions used within the QNode must match - the QNode interface. Here, we use TensorFlow: - - >>> params = tf.Variable([0.5, 0.1], dtype=tf.float64) - >>> with tf.GradientTape() as tape: - ... res = circuit(params) - >>> grad = tape.gradient(res, params) - >>> print(res) - tf.Tensor(0.9460913127754935, shape=(), dtype=float64) - >>> print(grad) - tf.Tensor([-0.27255248 -0.32390003], shape=(2,), dtype=float64) - - As a result of this change, quantum decompositions that require classical processing - are fully supported and end-to-end differentiable in tape mode. - -* **No more Variable wrapping**: In tape mode, QNode arguments no longer become :class:`~.Variable` - objects within the QNode. - - .. code-block:: python3 - - qml.enable_tape() - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev) - def circuit(x): - print("Parameter value:", x) - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - Internal QNode parameters can be easily inspected, printed, and manipulated: - - >>> circuit(0.5) - Parameter value: 0.5 - tensor(0.87758256, requires_grad=True) - -* **Return the quantum state**: In tape mode, QNodes bound to statevector simulators - can return the quantum state using the :func:`~.state` function: - - .. code-block:: python3 - - qml.enable_tape() - dev = qml.device("default.qubit", wires=2) - - @qml.qnode(dev) - def circuit(): - qml.Hadamard(wires=1) - return qml.state() - - >>> circuit() - array([0.70710678+0.j, 0.70710678+0.j, 0. +0.j, 0. +0.j]) - - Calculating the derivative of :func:`~.state` is currently only supported when using the - classical backpropagation differentiation method (``diff_method="backprop"``) with a - compatible device. - -* **Less restrictive QNode signatures**: There is no longer any restriction on the QNode signature; the QNode can be - defined and called following the same rules as standard Python functions. - - For example, the following QNode uses positional, named, and variable - keyword arguments: - - .. code-block:: python - - qml.enable_tape() - - x = torch.tensor(0.1, requires_grad=True) - y = torch.tensor([0.2, 0.3], requires_grad=True) - z = torch.tensor(0.4, requires_grad=True) - - @qml.qnode(dev, interface="torch") - def circuit(p1, p2=y, **kwargs): - qml.RX(p1, wires=0) - qml.RY(p2[0] * p2[1], wires=0) - qml.RX(kwargs["p3"], wires=0) - return qml.var(qml.PauliZ(0)) - - When we call the QNode, we may pass the arguments by name - even if defined positionally; any argument not provided will - use the default value. - - >>> res = circuit(p1=x, p3=z) - >>> print(res) - tensor(0.2327, dtype=torch.float64, grad_fn=) - >>> res.backward() - >>> print(x.grad, y.grad, z.grad) - tensor(0.8396) tensor([0.0289, 0.0193]) tensor(0.8387) - -* **Unifying all QNodes**: The tape-mode QNode merges all QNodes (including the :class:`~.JacobianQNode` - and the :class:`~.PassthruQNode`) into a single unified QNode, with identical behaviour regardless - of the differentiation type. - - In addition, it is now possible to inspect the internal variational quantum circuit structure - of QNodes when using classical backpropagation (which is not support in the standard - :class:`~.PassthruQNode`). - -* **Optimizations**: Tape mode provides various performance optimizations, reducing pre- and post-processing - overhead, and reduces the number of quantum evaluations in certain cases. - - -Quantum tapes -------------- - -Under the hood, tape mode is able to provide these new features by significantly overhauling -the internal structure of the QNode. When tape mode is enabled, the QNode is no longer -responsible for recording quantum operations, executing devices, or computing gradients---these -tasks have been delegated to an internal object that is created by the QNode, the **quantum tape**. - -In addition to being created internally by QNodes in tape mode, quantum tapes can also be created, +In addition to being created internally by QNodes, quantum tapes can also be created, nested, expanded (via :meth:`~.QuantumTape.expand`), and executed manually. Tape subclasses also provide additional gradient methods: -.. autosummary:: - - ~pennylane.tape.QuantumTape - ~pennylane.tape.QubitParamShiftTape - ~pennylane.tape.CVParamShiftTape - ~pennylane.tape.ReversibleTape - Finally, quantum tapes are fully compatible with autodifferentiating via NumPy/Autograd, -TensorFlow, and PyTorch: +TensorFlow, and PyTorch. -.. autosummary:: - :toctree: api - - ~pennylane.tape.interfaces.tf.TFInterface - ~pennylane.tape.interfaces.torch.TorchInterface - ~pennylane.tape.interfaces.autograd.AutogradInterface - -For more details and examples, please see the tape documentation. - - -.. automodapi:: pennylane.tape.transforms - :include-all-objects: .. automodapi:: pennylane.tape :no-main-docstr: :include-all-objects: - :skip: QNode, qnode, enable_tape, disable_tape + diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 1ddd32b3925..2104581b043 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -36,7 +36,7 @@ from .measure import expval, var, sample, state, density_matrix, probs from .ops import * from .optimize import * -from .qnode import qnode, QNode, QuantumFunctionError +from .qnode import qnode, QNode from .utils import inv from ._version import __version__ from .io import * @@ -60,6 +60,10 @@ default_config = Configuration("config.toml") +class QuantumFunctionError(Exception): + """Exception raised when an illegal operation is defined in a quantum function.""" + + def _get_device_entrypoints(): """Returns a dictionary mapping the device short name to the loadable entrypoint""" diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index 3bfa9425b44..caf16032601 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -378,7 +378,9 @@ def access_state(self, wires=None): array or tensor: the state or the density matrix of the device """ if not self.capabilities().get("returns_state"): - raise qml.QuantumFunctionError("The current device is not capable of returning the state") + raise qml.QuantumFunctionError( + "The current device is not capable of returning the state" + ) state = getattr(self, "state", None) @@ -752,7 +754,7 @@ def adjoint_jacobian(self, tape): ops = op.decomposition(*op.parameters, wires=op.wires) expanded_ops.extend(reversed(ops)) else: - raise QuantumFunctionError( + raise qml.QuantumFunctionError( f"The {op.name} operation is not supported using " 'the "adjoint" differentiation method' ) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index ecad78fb9d8..84bc977c100 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -155,6 +155,8 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): self.par_info = par_info self.trainable_params = trainable_params + queue = ops + obs + self._depth = None self._grid = {} @@ -166,7 +168,7 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): Required to translate between wires and indices of the wires on the device.""" self.num_wires = len(wires) """int: number of wires the circuit contains""" - for k, op in enumerate(ops): + for k, op in enumerate(queue): op.queue_idx = k # store the queue index in the Operator if hasattr(op, "return_type") and op.return_type is qml.operation.State: @@ -665,9 +667,7 @@ def update_node(self, old, new): self._operations = self.operations_in_order self._observables = self.observables_in_order - def draw( - self, charset="unicode", wire_order=None, show_all_wires=False - ): + def draw(self, charset="unicode", wire_order=None, show_all_wires=False): """Draw the CircuitGraph as a circuit diagram. Args: diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py index 92479879239..6ef0e0212d7 100644 --- a/pennylane/interfaces/torch.py +++ b/pennylane/interfaces/torch.py @@ -21,13 +21,38 @@ import torch from pennylane import QuantumFunctionError -from pennylane.interfaces.torch import args_to_numpy from pennylane.queuing import AnnotatedQueue COMPLEX_SUPPORT = semantic_version.match(">=1.6.0", torch.__version__) +def args_to_numpy(args): + """Converts all Torch tensors in a list to NumPy arrays + + Args: + args (list): list containing QNode arguments, including Torch tensors + + Returns: + list: returns the same list, with all Torch tensors converted to NumPy arrays + """ + res = [] + + for i in args: + if isinstance(i, torch.Tensor): + if i.is_cuda: # pragma: no cover + res.append(i.cpu().detach().numpy()) + else: + res.append(i.detach().numpy()) + else: + res.append(i) + + # if NumPy array is scalar, convert to a Python float + res = [i.tolist() if (isinstance(i, np.ndarray) and not i.shape) else i for i in res] + + return res + + class _TorchInterface(torch.autograd.Function): @staticmethod def forward(ctx, input_kwargs, *input_): diff --git a/pennylane/operation.py b/pennylane/operation.py index d0cbdd3d5cc..2ba60282c5b 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -252,6 +252,7 @@ class Operator(abc.ABC): do_queue (bool): Indicates whether the operator should be immediately pushed into the Operator queue. """ + def __copy__(self): cls = self.__class__ copied_op = cls.__new__(cls) @@ -635,8 +636,11 @@ def inv(self): Returns: :class:`Operator`: operation to be inverted """ - current_inv = qml.QueuingContext.get_info(self).get("inverse", False) - qml.QueuingContext.update_info(self, inverse=not current_inv) + if qml.QueuingContext.recording(): + current_inv = qml.QueuingContext.get_info(self).get("inverse", False) + qml.QueuingContext.update_info(self, inverse=not current_inv) + else: + self.inverse = not self._inverse return self @property @@ -1206,18 +1210,20 @@ def __matmul__(self, other): else: raise ValueError("Can only perform tensor products between observables.") - owning_info = qml.QueuingContext.get_info(self)["owns"] + (other,) + if qml.QueuingContext.recording(): + owning_info = qml.QueuingContext.get_info(self)["owns"] + (other,) - # update the annotated queue information - qml.QueuingContext.update_info(self, owns=owning_info) - qml.QueuingContext.update_info(other, owner=self) + # update the annotated queue information + qml.QueuingContext.update_info(self, owns=owning_info) + qml.QueuingContext.update_info(other, owner=self) return self def __rmatmul__(self, other): if isinstance(other, Observable): self.obs[:0] = [other] - qml.QueuingContext.update_info(other, owner=self) + if qml.QueuingContext.recording(): + qml.QueuingContext.update_info(other, owner=self) return self raise ValueError("Can only perform tensor products between observables.") diff --git a/pennylane/optimize/qng.py b/pennylane/optimize/qng.py index 7a6c6e07945..cb4cc70f37a 100644 --- a/pennylane/optimize/qng.py +++ b/pennylane/optimize/qng.py @@ -174,10 +174,7 @@ def step_and_cost(self, qnode, x, recompute_tensor=True, metric_tensor_fn=None): prior to the step """ # pylint: disable=arguments-differ - if ( - not isinstance(qnode, (qml.QNode, qml.ExpvalCost)) - and metric_tensor_fn is None - ): + if not isinstance(qnode, (qml.QNode, qml.ExpvalCost)) and metric_tensor_fn is None: raise ValueError( "The objective function must either be encoded as a single QNode or " "an ExpvalCost object for the natural gradient to be automatically computed. " diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index e1d2702ff01..8bb6c85af8d 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -22,7 +22,6 @@ try: import tensorflow as tf from tensorflow.keras.layers import Layer - from pennylane.interfaces.tf import to_tf CORRECT_TF_VERSION = int(tf.__version__.split(".")[0]) > 1 except ImportError: diff --git a/pennylane/qnn/torch.py b/pennylane/qnn/torch.py index cb15fe5b07e..898cadd3c5c 100644 --- a/pennylane/qnn/torch.py +++ b/pennylane/qnn/torch.py @@ -22,7 +22,6 @@ try: import torch from torch.nn import Module - from pennylane.interfaces.torch import to_torch TORCH_IMPORTED = True except ImportError: diff --git a/pennylane/qnode.py b/pennylane/qnode.py index 442d89ee539..325efd7407e 100644 --- a/pennylane/qnode.py +++ b/pennylane/qnode.py @@ -533,7 +533,7 @@ def metric_tensor(self, *args, diag_approx=False, only_construct=False, **kwargs Returns: array[float]: metric tensor """ - return metric_tensor(self, diag_approx=diag_approx, only_construct=only_construct)( + return qml.metric_tensor(self, diag_approx=diag_approx, only_construct=only_construct)( *args, **kwargs ) @@ -645,7 +645,7 @@ def to_tf(self, dtype=None): # pylint: disable=import-outside-toplevel try: import tensorflow as tf - from pennylane.tape.interfaces.tf import TFInterface + from pennylane.interfaces.tf import TFInterface if self.interface != "tf" and self.interface is not None: # Since the interface is changing, need to re-validate the tape class. @@ -685,7 +685,7 @@ def to_torch(self, dtype=None): # pylint: disable=import-outside-toplevel try: import torch - from pennylane.tape.interfaces.torch import TorchInterface + from pennylane.interfaces.torch import TorchInterface if self.interface != "torch" and self.interface is not None: # Since the interface is changing, need to re-validate the tape class. diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py index 244b90de4b1..64f2ec4331e 100644 --- a/pennylane/tape/tape.py +++ b/pennylane/tape/tape.py @@ -15,7 +15,6 @@ This module contains the base quantum tape. """ # pylint: disable=too-many-instance-attributes,protected-access,too-many-branches,too-many-public-methods -import contextlib import copy from collections import Counter from threading import RLock @@ -255,8 +254,6 @@ def __init__(self, name=None): self.all_sampled = False self.inverse = False - self._stack = None - self._obs_sharing_wires = [] """list[.Observable]: subset of the observables that share wires with another observable, i.e., that do not have their own unique set of wires.""" @@ -268,14 +265,6 @@ def __repr__(self): def __enter__(self): QuantumTape._lock.acquire() try: - if not QueuingContext.recording(): - # if the tape is the first active queuing context - # monkeypatch the operations to support the new queuing context - with contextlib.ExitStack() as stack: - for mock in mock_operations(): - stack.enter_context(mock) - self._stack = stack.pop_all() - QueuingContext.append(self) return super().__enter__() except Exception as _: @@ -285,11 +274,6 @@ def __enter__(self): def __exit__(self, exception_type, exception_value, traceback): try: super().__exit__(exception_type, exception_value, traceback) - - if not QueuingContext.recording(): - # remove the monkeypatching - self._stack.__exit__(exception_type, exception_value, traceback) - self._process_queue() finally: QuantumTape._lock.release() @@ -961,7 +945,6 @@ def draw(self, charset="unicode", wire_order=None, show_all_wires=False): """ return self.graph.draw( charset=charset, - show_variable_names=False, wire_order=wire_order, show_all_wires=show_all_wires, ) diff --git a/pennylane/templates/decorator.py b/pennylane/templates/decorator.py index bc626bd6d66..3de42506efa 100644 --- a/pennylane/templates/decorator.py +++ b/pennylane/templates/decorator.py @@ -52,7 +52,7 @@ def circuit(): Returns: callable: The wrapper function """ - # pylint: disable=import-outside-toplevel + import pennylane as qml # pylint: disable=import-outside-toplevel @wraps(func) def wrapper(*args, **kwargs): diff --git a/pennylane/templates/layers/particle_conserving_u1.py b/pennylane/templates/layers/particle_conserving_u1.py index 4c274b3907a..3eda4f73c50 100644 --- a/pennylane/templates/layers/particle_conserving_u1.py +++ b/pennylane/templates/layers/particle_conserving_u1.py @@ -61,9 +61,7 @@ def _preprocess(weights, wires, init_state): ) if shape[2] != 2: - raise ValueError( - f"Weights tensor must have third dimension of length 2; got {shape[2]}" - ) + raise ValueError(f"Weights tensor must have third dimension of length 2; got {shape[2]}") repeat = shape[0] diff --git a/pennylane/templates/layers/simplified_two_design.py b/pennylane/templates/layers/simplified_two_design.py index 194a2b8e48b..ba73c1be178 100644 --- a/pennylane/templates/layers/simplified_two_design.py +++ b/pennylane/templates/layers/simplified_two_design.py @@ -56,9 +56,7 @@ def _preprocess(weights, initial_layer_weights, wires): shape2 = qml.math.shape(initial_layer_weights) if shape2 != (len(wires),): - raise ValueError( - f"Initial layer weights must be of shape {(len(wires),)}; got {shape2}" - ) + raise ValueError(f"Initial layer weights must be of shape {(len(wires),)}; got {shape2}") return repeat diff --git a/pennylane/templates/layers/strongly_entangling.py b/pennylane/templates/layers/strongly_entangling.py index 8752345c093..6fdf0775757 100644 --- a/pennylane/templates/layers/strongly_entangling.py +++ b/pennylane/templates/layers/strongly_entangling.py @@ -54,9 +54,14 @@ def _preprocess(weights, wires, ranges): ) if shape[2] != 3: - raise ValueError( - f"Weights tensor must have third dimension of length 3; got {shape[2]}" - ) + raise ValueError(f"Weights tensor must have third dimension of length 3; got {shape[2]}") + + if len(wires) > 1: + if ranges is None: + # tile ranges with iterations of range(1, n_wires) + ranges = [(l % (len(wires) - 1)) + 1 for l in range(repeat)] + else: + ranges = [0] * repeat return repeat, ranges @@ -113,7 +118,6 @@ def StronglyEntanglingLayers(weights, wires, ranges=None, imprimitive=CNOT): Raises: ValueError: if inputs do not have the correct format """ - wires = Wires(wires) repeat, ranges = _preprocess(weights, wires, ranges) diff --git a/pennylane/templates/subroutines/arbitrary_unitary.py b/pennylane/templates/subroutines/arbitrary_unitary.py index 622fd527201..0f4f942345d 100644 --- a/pennylane/templates/subroutines/arbitrary_unitary.py +++ b/pennylane/templates/subroutines/arbitrary_unitary.py @@ -33,9 +33,7 @@ def _preprocess(weights, wires): """ shape = qml.math.shape(weights) if shape != (4 ** len(wires) - 1,): - raise ValueError( - f"Weights tensor must be of shape {(4 ** len(wires) - 1,)}; got {shape}." - ) + raise ValueError(f"Weights tensor must be of shape {(4 ** len(wires) - 1,)}; got {shape}.") def _tuple_to_word(index_tuple): diff --git a/pennylane/templates/subroutines/double_excitation_unitary.py b/pennylane/templates/subroutines/double_excitation_unitary.py index 8335d1289f3..dadb27559fb 100644 --- a/pennylane/templates/subroutines/double_excitation_unitary.py +++ b/pennylane/templates/subroutines/double_excitation_unitary.py @@ -54,6 +54,7 @@ def _preprocess(weight, wires1, wires2): if shape != (): raise ValueError(f"Weight must be a scalar; got shape {shape}.") + def _layer1(weight, s, r, q, p, set_cnot_wires): r"""Implement the first layer of the circuit to exponentiate the double-excitation operator entering the UCCSD ansatz. diff --git a/pennylane/transforms/draw.py b/pennylane/transforms/draw.py index b97de92bb5f..c46e8aca1ee 100644 --- a/pennylane/transforms/draw.py +++ b/pennylane/transforms/draw.py @@ -14,6 +14,7 @@ """ Contains the drawing function. """ +from functools import wraps def draw(qnode, charset="unicode", wire_order=None, show_all_wires=False): @@ -80,6 +81,7 @@ def circuit(): a: ──╰C──RX(0.2)──┤ -1: ───H───────────┤ """ + @wraps(qnode) def wrapper(*args, **kwargs): qnode.construct(args, kwargs) diff --git a/pennylane/transforms/metric_tensor.py b/pennylane/transforms/metric_tensor.py index e415f91bbc0..905f03e26fb 100644 --- a/pennylane/transforms/metric_tensor.py +++ b/pennylane/transforms/metric_tensor.py @@ -14,6 +14,8 @@ """ Contains the metric tensor transform """ +import warnings + import numpy as np import pennylane as qml @@ -175,7 +177,6 @@ def processing_fn(probs): return metric_tensor_tapes, processing_fn - def metric_tensor(qnode, diag_approx=False, only_construct=False): """Returns a function that returns the value of the metric tensor of a given QNode. diff --git a/pennylane/utils.py b/pennylane/utils.py index 4bfe87dfd83..5cb0cfef5bc 100644 --- a/pennylane/utils.py +++ b/pennylane/utils.py @@ -329,7 +329,7 @@ def circuit2(): try: # remove the queued operation to be inverted # from the existing queuing context - qml.tape.QueuingContext.remove(op) + qml.QueuingContext.remove(op) except KeyError: # operation to be inverted does not # exist on the queuing context diff --git a/tests/circuit_drawer/test_circuit_drawer.py b/tests/circuit_drawer/test_circuit_drawer.py index f0d4440810f..fda145e3422 100644 --- a/tests/circuit_drawer/test_circuit_drawer.py +++ b/tests/circuit_drawer/test_circuit_drawer.py @@ -667,14 +667,14 @@ def test_qubit_circuit_with_values( self, parameterized_qubit_qnode, drawn_parameterized_qubit_circuit_with_values ): """Test that a parametrized qubit circuit renders correctly with values.""" - output = parameterized_qubit_qnode.draw(show_variable_names=False) + output = parameterized_qubit_qnode.draw() assert output == drawn_parameterized_qubit_circuit_with_values def test_wide_qubit_circuit_with_values( self, parameterized_wide_qubit_qnode, drawn_parameterized_wide_qubit_qnode_with_values ): """Test that a wide parametrized qubit circuit renders correctly with values.""" - output = parameterized_wide_qubit_qnode.draw(show_variable_names=False) + output = parameterized_wide_qubit_qnode.draw() assert output == drawn_parameterized_wide_qubit_qnode_with_values @@ -682,7 +682,7 @@ def test_qubit_circuit_with_interesting_wires( self, qubit_circuit_with_interesting_wires, drawn_qubit_circuit_with_interesting_wires ): """Test that non-consecutive wires show correctly.""" - output = qubit_circuit_with_interesting_wires.draw(show_variable_names=False) + output = qubit_circuit_with_interesting_wires.draw() assert output == drawn_qubit_circuit_with_interesting_wires @@ -696,7 +696,7 @@ def test_cv_circuit_with_values( self, parameterized_cv_qnode, drawn_parameterized_cv_qnode_with_values ): """Test that a parametrized CV circuit renders correctly with values.""" - output = parameterized_cv_qnode.draw(show_variable_names=False) + output = parameterized_cv_qnode.draw() assert output == drawn_parameterized_cv_qnode_with_values def test_qubit_circuit_with_unused_wires( diff --git a/tests/circuit_graph/test_circuit_graph_hash.py b/tests/circuit_graph/test_circuit_graph_hash.py index 3b2a9a2e855..36856c8bd0f 100644 --- a/tests/circuit_graph/test_circuit_graph_hash.py +++ b/tests/circuit_graph/test_circuit_graph_hash.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Unit and integration tests for creating the :mod:`pennylane` :attr:`QNode.circuit.hash` attribute. +Unit and integration tests for creating the :mod:`pennylane` :attr:`QNode.qtape.graph.hash` attribute. """ import pytest import numpy as np @@ -46,8 +46,8 @@ class TestCircuitGraphHash: @pytest.mark.parametrize("queue, observable_queue, expected_string", numeric_queues) def test_serialize_numeric_arguments(self, queue, observable_queue, expected_string): """Tests that the same hash is created for two circuitgraphs that have numeric arguments.""" - circuit_graph_1 = CircuitGraph(queue + observable_queue, {}, Wires([0, 1, 2])) - circuit_graph_2 = CircuitGraph(queue + observable_queue, {}, Wires([0, 1, 2])) + circuit_graph_1 = CircuitGraph(queue, observable_queue, Wires([0, 1, 2])) + circuit_graph_2 = CircuitGraph(queue, observable_queue, Wires([0, 1, 2])) assert circuit_graph_1.serialize() == circuit_graph_2.serialize() assert expected_string == circuit_graph_1.serialize() @@ -83,8 +83,8 @@ def test_serialize_numeric_arguments(self, queue, observable_queue, expected_str def test_serialize_numeric_arguments_observables(self, queue, observable_queue, expected_string): """Tests that the same hash is created for two circuitgraphs that have identical queues and empty variable_deps.""" - circuit_graph_1 = CircuitGraph(queue + observable_queue, {}, Wires([0, 1])) - circuit_graph_2 = CircuitGraph(queue + observable_queue, {}, Wires([0, 1])) + circuit_graph_1 = CircuitGraph(queue, observable_queue, Wires([0, 1])) + circuit_graph_2 = CircuitGraph(queue, observable_queue, Wires([0, 1])) assert circuit_graph_1.serialize() == circuit_graph_2.serialize() assert expected_string == circuit_graph_1.serialize() @@ -108,7 +108,7 @@ def circuit1(): node1 = qml.QNode(circuit1, dev) node1.evaluate([], {}) - circuit_hash_1 = node1.circuit.hash + circuit_hash_1 = node1.qtape.graph.hash def circuit2(): qml.RX(a, wires=[0]) @@ -118,7 +118,7 @@ def circuit2(): node2 = qml.QNode(circuit2, dev) node2.evaluate([], {}) - circuit_hash_2 = node2.circuit.hash + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 == circuit_hash_2 @@ -137,8 +137,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.RX(x, wires=[0]) @@ -147,8 +147,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 == circuit_hash_2 @@ -168,8 +168,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.RX(x, wires=[0]) @@ -179,8 +179,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 == circuit_hash_2 @@ -203,8 +203,8 @@ def circuit1(a, b): return qml.expval(qml.PauliZ(0)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([a, b], {}) - circuit_hash_1 = node1.circuit.hash + node1(a, b) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.RX(x, wires=[0]) @@ -213,8 +213,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 == circuit_hash_2 @@ -235,8 +235,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.RX(x, wires=[0]) @@ -246,8 +246,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 == circuit_hash_2 @@ -266,8 +266,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.Rot(x, y, 0.3, wires=[0]) @@ -275,7 +275,7 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) + node2(x, y) circuit_hash_2 = dev.circuit_hash assert circuit_hash_1 == circuit_hash_2 @@ -294,8 +294,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.Rot(x, y, 0.3, wires=[0]) @@ -303,8 +303,8 @@ def circuit2(x, y): return qml.var(qml.PauliZ(0) @ qml.PauliX(1)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash def circuit3(x, y): qml.Rot(x, y, 0.3, wires=[0]) @@ -312,8 +312,8 @@ def circuit3(x, y): return qml.sample(qml.PauliZ(0) @ qml.PauliX(1)) node3 = qml.QNode(circuit1, dev) - node3.evaluate([x, y], {}) - circuit_hash_3 = node3.circuit.hash + node3(x, y) + circuit_hash_3 = node3.qtape.graph.hash assert circuit_hash_1 == circuit_hash_2 == circuit_hash_3 @@ -333,8 +333,8 @@ def circuit1(x, y): return qml.expval(qml.Hermitian(matrix, wires=[0]) @ qml.PauliX(1)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.Rot(x, y, 0.3, wires=[0]) @@ -342,8 +342,8 @@ def circuit2(x, y): return qml.expval(qml.Hermitian(matrix, wires=[0]) @ qml.PauliX(1)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 == circuit_hash_2 @@ -365,7 +365,7 @@ def circuit1(): node1 = qml.QNode(circuit1, dev) node1.evaluate([], {}) - circuit_hash_1 = node1.circuit.hash + circuit_hash_1 = node1.qtape.graph.hash c = 0.6 @@ -377,7 +377,7 @@ def circuit2(): node2 = qml.QNode(circuit2, dev) node2.evaluate([], {}) - circuit_hash_2 = node2.circuit.hash + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -393,7 +393,7 @@ def circuit1(): node1 = qml.QNode(circuit1, dev) node1.evaluate([], {}) - circuit_hash_1 = node1.circuit.hash + circuit_hash_1 = node1.qtape.graph.hash def circuit2(): qml.RY(a, wires=[0]) @@ -401,7 +401,7 @@ def circuit2(): node2 = qml.QNode(circuit2, dev) node2.evaluate([], {}) - circuit_hash_2 = node2.circuit.hash + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -422,8 +422,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.RX(x, wires=[0]) @@ -433,8 +433,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -454,8 +454,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0)) # <------------- qml.PauliZ(0) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.RX(x, wires=[0]) @@ -465,8 +465,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) # <------------- qml.PauliZ(0) @ qml.PauliX(1) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -485,8 +485,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.Rot(x, y, 0.3, wires=[0]) # <------------- x, y, 0.3 @@ -494,8 +494,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -514,8 +514,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.Rot(x, y, 0.5, wires=[0]) # <------------- 0.5 @@ -523,8 +523,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -543,8 +543,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.Rot(x, y, 0.3, wires=[0]) @@ -552,8 +552,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -572,8 +572,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) # <----- (0) @ (1) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.Rot(x, y, 0.3, wires=[0]) @@ -581,8 +581,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(2)) # <----- (0) @ (2) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -603,8 +603,8 @@ def circuit1(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.RX(x, wires=[0]) @@ -614,8 +614,8 @@ def circuit2(x, y): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -638,8 +638,8 @@ def circuit1(x, y): return qml.expval(qml.Hermitian(matrix_1, wires=[0]) @ qml.PauliX(1)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([x, y], {}) - circuit_hash_1 = node1.circuit.hash + node1(x, y) + circuit_hash_1 = node1.qtape.graph.hash def circuit2(x, y): qml.Rot(x, y, 0.3, wires=[0]) @@ -647,8 +647,8 @@ def circuit2(x, y): return qml.expval(qml.Hermitian(matrix_2, wires=[0]) @ qml.PauliX(1)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([x, y], {}) - circuit_hash_2 = node2.circuit.hash + node2(x, y) + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -668,7 +668,7 @@ def circuit(params, wires): hashes = set() for qnode in qnodes: - hashes.add(qnode.circuit.hash) + hashes.add(qnode.qtape.graph.hash) assert len(hashes) == 1 diff --git a/tests/circuit_graph/test_qasm.py b/tests/circuit_graph/test_qasm.py index 96ae0f36df9..1e809e433a2 100644 --- a/tests/circuit_graph/test_qasm.py +++ b/tests/circuit_graph/test_qasm.py @@ -49,7 +49,7 @@ def test_native_qasm_gates(self): qml.PauliX(wires=1), ] - circuit = CircuitGraph(ops, {}, Wires([0, 1, 2])) + circuit = CircuitGraph(ops, [], Wires([0, 1, 2])) res = circuit.to_openqasm() expected = dedent( @@ -83,7 +83,7 @@ def test_native_inverse_gates(self): qml.T(wires=0).inv(), ] - circuit = CircuitGraph(ops, {}, Wires([0])) + circuit = CircuitGraph(ops, [], Wires([0])) res = circuit.to_openqasm() expected = dedent( @@ -109,7 +109,7 @@ def test_unused_wires(self): qml.CNOT(wires=[1, 0]), ] - circuit = CircuitGraph(ops, {}, Wires([0, 1, 2, 3, 4])) + circuit = CircuitGraph(ops, [], Wires([0, 1, 2, 3, 4])) res = circuit.to_openqasm() expected = dedent( @@ -231,7 +231,7 @@ def test_unsupported_gate(self): U = np.array([[1, 1], [1, -1]]) / np.sqrt(2) ops = [qml.S(wires=0), qml.QubitUnitary(U, wires=[0, 1])] - circuit = CircuitGraph(ops, {}, Wires([0, 1])) + circuit = CircuitGraph(ops, [], Wires([0, 1])) with pytest.raises( ValueError, match="QubitUnitary not supported by the QASM serializer" @@ -248,7 +248,7 @@ def test_rotations(self): qml.expval(qml.Hadamard(2)), ] - circuit = CircuitGraph(ops, {}, Wires([0, 1, 2])) + circuit = CircuitGraph(ops, [], Wires([0, 1, 2])) res = circuit.to_openqasm() expected = dedent( diff --git a/tests/collections/test_collections.py b/tests/collections/test_collections.py index c667f792ab2..960215be522 100644 --- a/tests/collections/test_collections.py +++ b/tests/collections/test_collections.py @@ -63,13 +63,13 @@ def test_mapping_over_observables(self): # evaluate collection so that queue is populated qc(1) - assert len(qc[0].ops) == 2 - assert qc[0].ops[0].name == "RX" - assert qc[0].ops[1].name == "PauliX" + assert len(qc[0].qtape.operations) == 2 + assert qc[0].qtape.operations[0].name == "RX" + assert qc[0].qtape.operations[1].name == "PauliX" - assert len(qc[1].ops) == 2 - assert qc[1].ops[0].name == "RX" - assert qc[1].ops[1].name == "PauliY" + assert len(qc[1].qtape.operations) == 2 + assert qc[1].qtape.operations[0].name == "RX" + assert qc[1].qtape.operations[1].name == "PauliY" def test_mapping_over_observables_as_tuples(self): """Test that mapping over a tuple of observables produces @@ -86,13 +86,13 @@ def test_mapping_over_observables_as_tuples(self): # evaluate collection so that queue is populated qc(1) - assert len(qc[0].ops) == 2 - assert qc[0].ops[0].name == "RX" - assert qc[0].ops[1].name == "PauliX" + assert len(qc[0].qtape.operations) == 2 + assert qc[0].qtape.operations[0].name == "RX" + assert qc[0].qtape.operations[1].name == "PauliX" - assert len(qc[1].ops) == 2 - assert qc[1].ops[0].name == "RX" - assert qc[1].ops[1].name == "PauliY" + assert len(qc[1].qtape.operations) == 2 + assert qc[1].qtape.operations[0].name == "RX" + assert qc[1].qtape.operations[1].name == "PauliY" def test_mapping_over_devices(self): """Test that mapping over a list of devices produces @@ -109,13 +109,13 @@ def test_mapping_over_devices(self): # evaluate collection so that queue is populated qc(1) - assert len(qc[0].ops) == 2 - assert qc[0].ops[0].name == "RX" - assert qc[0].ops[1].name == "PauliX" + assert len(qc[0].qtape.operations) == 2 + assert qc[0].qtape.operations[0].name == "RX" + assert qc[0].qtape.operations[1].name == "PauliX" - assert len(qc[1].ops) == 2 - assert qc[1].ops[0].name == "RX" - assert qc[1].ops[1].name == "PauliY" + assert len(qc[1].qtape.operations) == 2 + assert qc[1].qtape.operations[0].name == "RX" + assert qc[1].qtape.operations[1].name == "PauliY" # test that device is not broadcast assert qc[0].device is not qc[1].device @@ -135,15 +135,15 @@ def test_mapping_over_measurements(self): # evaluate collection so that queue is populated qc(1) - assert len(qc[0].ops) == 2 - assert qc[0].ops[0].name == "RX" - assert qc[0].ops[1].name == "PauliX" - assert qc[0].ops[1].return_type == qml.operation.Expectation + assert len(qc[0].qtape.operations) == 2 + assert qc[0].qtape.operations[0].name == "RX" + assert qc[0].qtape.operations[1].name == "PauliX" + assert qc[0].qtape.operations[1].return_type == qml.operation.Expectation - assert len(qc[1].ops) == 2 - assert qc[1].ops[0].name == "RX" - assert qc[1].ops[1].name == "PauliY" - assert qc[1].ops[1].return_type == qml.operation.Variance + assert len(qc[1].qtape.operations) == 2 + assert qc[1].qtape.operations[0].name == "RX" + assert qc[1].qtape.operations[1].name == "PauliY" + assert qc[1].qtape.operations[1].return_type == qml.operation.Variance def test_invalid_observable(self): """Test that an invalid observable raises an exception""" @@ -171,12 +171,12 @@ def test_passing_kwargs(self): assert len(qc) == 2 # Checking the h attribute which contains the step size - assert qc[0].h == 123 - assert qc[1].h == 123 + assert qc[0].diff_options["h"] == 123 + assert qc[1].diff_options["h"] == 123 # Checking that the order is set in each QNode - assert qc[0].order == 2 - assert qc[1].order == 2 + assert qc[0].diff_options["order"] == 2 + assert qc[1].diff_options["order"] == 2 class TestApply: diff --git a/tests/devices/test_default_qubit_jax.py b/tests/devices/test_default_qubit_jax.py index 1c76e7b6005..9d042d3c447 100644 --- a/tests/devices/test_default_qubit_jax.py +++ b/tests/devices/test_default_qubit_jax.py @@ -332,11 +332,6 @@ def circuit(x, weights, w=None): operation(x, weights[0], weights[1], wires=w) return qml.expval(qml.PauliX(w)) - # Check that the correct QNode type is being used. - if diff_method == "backprop": - assert isinstance(circuit, qml.qnodes.PassthruQNode) - assert not hasattr(circuit, "jacobian") - def cost(params): """Perform some classical processing""" return (circuit(params[0], params[1:], w=0) ** 2).reshape(()) diff --git a/tests/devices/test_default_qubit_tf.py b/tests/devices/test_default_qubit_tf.py index 3cb86794485..31d42006f45 100644 --- a/tests/devices/test_default_qubit_tf.py +++ b/tests/devices/test_default_qubit_tf.py @@ -425,7 +425,7 @@ def test_single_wire_expectation(self, gate, obs, expected, theta, phi, varphi, for i in range(len(observables)): observables[i].return_type = qml.operation.Expectation - res = dev.execute(qml.CircuitGraph(queue + observables, {}, Wires([0, 1, 2]))) + res = dev.execute(qml.CircuitGraph(queue, observables, Wires([0, 1, 2]))) assert np.allclose(res, expected(theta, phi), atol=tol, rtol=0) def test_hermitian_expectation(self, theta, phi, varphi, tol): @@ -437,7 +437,7 @@ def test_hermitian_expectation(self, theta, phi, varphi, tol): for i in range(len(observables)): observables[i].return_type = qml.operation.Expectation - res = dev.execute(qml.CircuitGraph(queue + observables, {}, Wires([0, 1]))) + res = dev.execute(qml.CircuitGraph(queue, observables, Wires([0, 1]))) a = A[0, 0] re_b = A[0, 1].real @@ -466,7 +466,7 @@ def test_multi_mode_hermitian_expectation(self, theta, phi, varphi, tol): for i in range(len(observables)): observables[i].return_type = qml.operation.Expectation - res = dev.execute(qml.CircuitGraph(queue + observables, {}, Wires([0, 1]))) + res = dev.execute(qml.CircuitGraph(queue, observables, Wires([0, 1]))) # below is the analytic expectation value for this circuit with arbitrary # Hermitian observable A @@ -706,7 +706,7 @@ def test_var(self, theta, phi, varphi, tol): for i in range(len(observables)): observables[i].return_type = qml.operation.Variance - res = dev.execute(qml.CircuitGraph(queue + observables, {}, Wires([0]))) + res = dev.execute(qml.CircuitGraph(queue, observables, Wires([0]))) expected = 0.25 * (3 - np.cos(2 * theta) - 2 * np.cos(theta) ** 2 * np.cos(2 * phi)) assert np.allclose(res, expected, atol=tol, rtol=0) @@ -722,7 +722,7 @@ def test_var_hermitian(self, theta, phi, varphi, tol): for i in range(len(observables)): observables[i].return_type = qml.operation.Variance - res = dev.execute(qml.CircuitGraph(queue + observables, {}, Wires([0]))) + res = dev.execute(qml.CircuitGraph(queue, observables, Wires([0]))) expected = 0.5 * ( 2 * np.sin(2 * theta) * np.cos(phi) ** 2 + 24 * np.sin(phi) * np.cos(phi) * (np.sin(theta) - np.cos(theta)) diff --git a/tests/interfaces/test_qnode_autograd.py b/tests/interfaces/test_qnode_autograd.py index f7e70a15db0..c26a4fc02e6 100644 --- a/tests/interfaces/test_qnode_autograd.py +++ b/tests/interfaces/test_qnode_autograd.py @@ -16,7 +16,8 @@ from pennylane import numpy as np import pennylane as qml -from pennylane.tape import JacobianTape, qnode, QNode, QubitParamShiftTape +from pennylane import qnode, QNode +from pennylane.tape import JacobianTape, QubitParamShiftTape @pytest.mark.parametrize( @@ -383,7 +384,6 @@ def circuit(U, a): def test_differentiable_expand(self, dev_name, diff_method, mocker, tol): """Test that operation and nested tapes expansion is differentiable""" - mock = mocker.patch.object(qml.operation.Operation, "do_check_domain", False) class U3(qml.U3): def expand(self): @@ -637,7 +637,6 @@ def construct(self, args, kwargs): ) def test_transform(dev_name, diff_method, monkeypatch, tol): """Test an example transform""" - monkeypatch.setattr(qml.operation.Operation, "do_check_domain", False) dev = qml.device(dev_name, wires=1) diff --git a/tests/interfaces/test_qnode_tf.py b/tests/interfaces/test_qnode_tf.py index 37759b8e832..636fc9b1d6a 100644 --- a/tests/interfaces/test_qnode_tf.py +++ b/tests/interfaces/test_qnode_tf.py @@ -19,7 +19,8 @@ import numpy as np import pennylane as qml -from pennylane.tape import JacobianTape, qnode, QNode +from pennylane import qnode, QNode +from pennylane.tape import JacobianTape @pytest.mark.parametrize( @@ -370,8 +371,6 @@ def circuit(U, a): def test_differentiable_expand(self, dev_name, diff_method, mocker, tol): """Test that operation and nested tapes expansion is differentiable""" - mock = mocker.patch.object(qml.operation.Operation, "do_check_domain", False) - class U3(qml.U3): def expand(self): theta, phi, lam = self.data @@ -565,7 +564,6 @@ def construct(self, args, kwargs): ) def test_transform(dev_name, diff_method, monkeypatch, tol): """Test an example transform""" - monkeypatch.setattr(qml.operation.Operation, "do_check_domain", False) dev = qml.device(dev_name, wires=1) diff --git a/tests/interfaces/test_qnode_torch.py b/tests/interfaces/test_qnode_torch.py index ec2bd33953d..e7a684d0299 100644 --- a/tests/interfaces/test_qnode_torch.py +++ b/tests/interfaces/test_qnode_torch.py @@ -19,7 +19,8 @@ import numpy as np import pennylane as qml -from pennylane.tape import JacobianTape, qnode, QNode +from pennylane import qnode, QNode +from pennylane.tape import JacobianTape @pytest.mark.parametrize( @@ -353,7 +354,6 @@ def circuit(U, a): def test_differentiable_expand(self, dev_name, diff_method, mocker, tol): """Test that operation and nested tapes expansion is differentiable""" - mock = mocker.patch.object(qml.operation.Operation, "do_check_domain", False) class U3(qml.U3): def expand(self): @@ -566,7 +566,6 @@ def construct(self, args, kwargs): def test_transform(monkeypatch, tol): """Test an example transform""" - monkeypatch.setattr(qml.operation.Operation, "do_check_domain", False) dev = qml.device("default.qubit", wires=1) diff --git a/tests/interfaces/test_tape_autograd.py b/tests/interfaces/test_tape_autograd.py index b1c873cbf80..81af633f98a 100644 --- a/tests/interfaces/test_tape_autograd.py +++ b/tests/interfaces/test_tape_autograd.py @@ -17,7 +17,7 @@ import pennylane as qml from pennylane.tape import JacobianTape -from pennylane.tape.interfaces.autograd import AutogradInterface +from pennylane.interfaces.autograd import AutogradInterface class TestAutogradQuantumTape: @@ -254,7 +254,6 @@ def cost(a, U, device): def test_differentiable_expand(self, mocker, tol): """Test that operation and nested tapes expansion is differentiable""" - mock = mocker.patch.object(qml.operation.Operation, "do_check_domain", False) class U3(qml.U3): def expand(self): @@ -565,7 +564,6 @@ def test_differentiable_expand(self, mocker, tol): """Test that operation and nested tapes expansion is differentiable""" spy = mocker.spy(JacobianTape, "jacobian") - mock = mocker.patch.object(qml.operation.Operation, "do_check_domain", False) class U3(qml.U3): def expand(self): diff --git a/tests/interfaces/test_tape_tf.py b/tests/interfaces/test_tape_tf.py index eba88efa536..30978851cca 100644 --- a/tests/interfaces/test_tape_tf.py +++ b/tests/interfaces/test_tape_tf.py @@ -20,7 +20,7 @@ import pennylane as qml from pennylane.tape import JacobianTape -from pennylane.tape.interfaces.tf import TFInterface +from pennylane.interfaces.tf import TFInterface class TestTFQuantumTape: @@ -316,7 +316,6 @@ def test_matrix_parameter(self, U, tol): def test_differentiable_expand(self, mocker, tol): """Test that operation and nested tapes expansion is differentiable""" - mock = mocker.patch.object(qml.operation.Operation, "do_check_domain", False) class U3(qml.U3): def expand(self): @@ -631,7 +630,6 @@ def test_differentiable_expand(self, mocker, tol): """Test that operation and nested tapes expansion is differentiable""" spy = mocker.spy(JacobianTape, "jacobian") - mock = mocker.patch.object(qml.operation.Operation, "do_check_domain", False) class U3(qml.U3): def expand(self): diff --git a/tests/interfaces/test_tape_torch.py b/tests/interfaces/test_tape_torch.py index 67385580471..3867053ad0d 100644 --- a/tests/interfaces/test_tape_torch.py +++ b/tests/interfaces/test_tape_torch.py @@ -20,7 +20,7 @@ import pennylane as qml from pennylane.tape import JacobianTape -from pennylane.tape.interfaces.torch import TorchInterface +from pennylane.interfaces.torch import TorchInterface class TestTorchQuantumTape: @@ -290,7 +290,6 @@ def test_matrix_parameter(self, U, tol): def test_differentiable_expand(self, mocker, tol): """Test that operation and nested tapes expansion is differentiable""" - mock = mocker.patch.object(qml.operation.Operation, "do_check_domain", False) class U3(qml.U3): def expand(self): @@ -451,6 +450,6 @@ def test_complex_min_version(self, monkeypatch): in the apply() method""" with monkeypatch.context() as m: - m.setattr(qml.tape.interfaces.torch, "COMPLEX_SUPPORT", False) + m.setattr(qml.interfaces.torch, "COMPLEX_SUPPORT", False) with pytest.raises(qml.QuantumFunctionError, match="Version 1.6.0 or above of PyTorch"): TorchInterface.apply(JacobianTape(), dtype=torch.complex128) diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 504ab79e5ea..e4993db3580 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -120,7 +120,7 @@ def circuit(inputs, w1, w2, *args): KerasLayer(circuit, weight_shapes, output_dim=1) @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) - def test_var_keywordself): + def test_var_keyword(self): """Test that variable number of keyword arguments works""" n_qubits = 2 output_dim = 2 diff --git a/tests/tape/test_qnode.py b/tests/tape/test_qnode.py index 185426595c3..56ceddd300b 100644 --- a/tests/tape/test_qnode.py +++ b/tests/tape/test_qnode.py @@ -17,7 +17,9 @@ import pennylane as qml from pennylane import QNodeCollection -from pennylane.tape import JacobianTape, QNode, draw, qnode, QubitParamShiftTape, CVParamShiftTape +from pennylane import qnode, QNode +from pennylane.transforms import draw +from pennylane.tape import JacobianTape, QubitParamShiftTape, CVParamShiftTape class TestValidation: @@ -190,13 +192,13 @@ def test_diff_method(self, mocker): quantum tape, interface, and diff method.""" dev = qml.device("default.qubit", wires=1) - mock_best = mocker.patch("pennylane.tape.QNode.get_best_method") + mock_best = mocker.patch("pennylane.QNode.get_best_method") mock_best.return_value = 1, 2, 3, {"method": "best"} - mock_backprop = mocker.patch("pennylane.tape.QNode._validate_backprop_method") + mock_backprop = mocker.patch("pennylane.QNode._validate_backprop_method") mock_backprop.return_value = 4, 5, 6, {"method": "backprop"} - mock_device = mocker.patch("pennylane.tape.QNode._validate_device_method") + mock_device = mocker.patch("pennylane.QNode._validate_device_method") mock_device.return_value = 7, 8, 9, {"method": "device"} qn = QNode(None, dev, diff_method="best") @@ -427,7 +429,6 @@ def circuit(p1, p2, **kwargs): assert result == expected def test_draw_transform_raises(self): - qml.disable_tape() dev = qml.device("default.qubit", wires=2) @qml.qnode(dev, interface="autograd") def circuit(p1, p2, **kwargs): @@ -440,8 +441,6 @@ def circuit(p1, p2, **kwargs): with pytest.raises(ValueError, match="only works when tape mode is enabled"): result = draw(circuit, charset="ascii") - qml.enable_tape() - def test_drawing(self): """Test circuit drawing""" from pennylane import numpy as anp @@ -582,7 +581,7 @@ class TestTFInterface: def test_import_error(self, mocker): """Test that an exception is caught on import error""" tf = pytest.importorskip("tensorflow", minversion="2.1") - mock = mocker.patch("pennylane.tape.interfaces.tf.TFInterface.apply") + mock = mocker.patch("pennylane.interfaces.tf.TFInterface.apply") mock.side_effect = ImportError() def func(x, y): @@ -635,7 +634,7 @@ class TestTorchInterface: def test_import_error(self, mocker): """Test that an exception is caught on import error""" torch = pytest.importorskip("torch", minversion="1.3") - mock = mocker.patch("pennylane.tape.interfaces.torch.TorchInterface.apply") + mock = mocker.patch("pennylane.interfaces.torch.TorchInterface.apply") mock.side_effect = ImportError() def func(x, y): diff --git a/tests/tape/test_qubit_param_shift.py b/tests/tape/test_qubit_param_shift.py index 3160f11b6f1..045c6538945 100644 --- a/tests/tape/test_qubit_param_shift.py +++ b/tests/tape/test_qubit_param_shift.py @@ -16,8 +16,8 @@ import numpy as np import pennylane as qml -from pennylane.tape import QubitParamShiftTape, qnode -from pennylane.tape.measure import MeasurementProcess +from pennylane.tape import QubitParamShiftTape +from pennylane.measure import MeasurementProcess class TestGradMethod: diff --git a/tests/tape/test_reversible.py b/tests/tape/test_reversible.py index 28e0f2d5c40..d220e537312 100644 --- a/tests/tape/test_reversible.py +++ b/tests/tape/test_reversible.py @@ -16,9 +16,10 @@ from pennylane import numpy as np import pennylane as qml -from pennylane.tape.interfaces.autograd import AutogradInterface -from pennylane.tape import JacobianTape, ReversibleTape, QNode, qnode -from pennylane.tape.measure import MeasurementProcess +from pennylane.interfaces.autograd import AutogradInterface +from pennylane.tape import JacobianTape, ReversibleTape +from pennylane import QNode, qnode +from pennylane.measure import MeasurementProcess thetas = np.linspace(-2 * np.pi, 2 * np.pi, 8) diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index 94cf27987a4..ec3385c7a6c 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -18,8 +18,9 @@ import pytest import pennylane as qml -from pennylane.tape import QuantumTape, TapeCircuitGraph -from pennylane.tape.measure import MeasurementProcess, expval, sample, var +from pennylane import CircuitGraph +from pennylane.tape import QuantumTape +from pennylane.measure import MeasurementProcess, expval, sample, var def TestOperationMonkeypatching(): @@ -281,7 +282,7 @@ class TestGraph: def test_graph_creation(self, mocker): """Test that the circuit graph is correctly created""" - spy = mocker.spy(TapeCircuitGraph, "__init__") + spy = mocker.spy(CircuitGraph, "__init__") with QuantumTape() as tape: op = qml.RX(1.0, wires=0) diff --git a/tests/templates/test_broadcast.py b/tests/templates/test_broadcast.py index fa4d989b818..6742804a0e1 100644 --- a/tests/templates/test_broadcast.py +++ b/tests/templates/test_broadcast.py @@ -20,7 +20,6 @@ from math import pi import numpy as np import pennylane as qml -import pennylane._queuing from pennylane.templates import template, broadcast from pennylane.ops import RX, RY, T, S, Rot, CRX, CRot, CNOT from pennylane.templates.broadcast import wires_pyramid, wires_all_to_all, wires_ring @@ -135,7 +134,7 @@ class TestBuiltinPatterns: def test_correct_queue_for_gate_unitary(self, unitary, parameters): """Tests that correct gate queue is created when 'unitary' is a single gate.""" - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: broadcast(unitary=unitary, pattern="single", wires=range(3), parameters=parameters) for gate in rec.queue: @@ -148,7 +147,7 @@ def test_correct_queue_for_gate_unitary(self, unitary, parameters): def test_correct_queue_for_template_unitary(self, unitary, gates, parameters): """Tests that correct gate queue is created when 'unitary' is a template.""" - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: broadcast(unitary=unitary, pattern="single", wires=range(3), parameters=parameters) first_gate = gates[0] @@ -166,7 +165,7 @@ def test_correct_queue_for_template_unitary(self, unitary, gates, parameters): def test_correct_queue_for_template_unitary_with_keyword(self, template, kwarg, target_queue, parameters): """Tests that correct gate queue is created when 'unitary' is a template that uses a keyword.""" - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: broadcast(unitary=template, pattern="single", wires=range(2), parameters=parameters, kwargs={'a': kwarg}) @@ -179,10 +178,10 @@ def test_correct_queue_for_template_unitary_with_keyword(self, template, kwarg, def test_correct_queue_same_gate_unitary_different_parameter_formats(self, pars1, pars2, gate): """Tests that specific parameter inputs have the same output.""" - with pennylane.tape.OperationRecorder() as rec1: + with qml.tape.OperationRecorder() as rec1: broadcast(unitary=gate, pattern="single", wires=range(3), parameters=pars1) - with pennylane.tape.OperationRecorder() as rec2: + with qml.tape.OperationRecorder() as rec2: broadcast(unitary=gate, pattern="single", wires=range(3), parameters=pars2) for g1, g2 in zip(rec1.queue, rec2.queue): @@ -192,7 +191,7 @@ def test_correct_queue_same_gate_unitary_different_parameter_formats(self, pars1 def test_correct_parameters_in_queue(self, pattern, n_wires, gate, parameters): """Tests that gate queue has correct parameters.""" - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: broadcast(unitary=gate, pattern=pattern, wires=range(n_wires), parameters=parameters) for target_par, g in zip(parameters, rec.queue): diff --git a/tests/templates/test_embeddings.py b/tests/templates/test_embeddings.py index 3d9e1d3f3fb..91fced71c39 100644 --- a/tests/templates/test_embeddings.py +++ b/tests/templates/test_embeddings.py @@ -20,7 +20,6 @@ from math import pi import numpy as np import pennylane as qml -import pennylane._queuing from pennylane.templates.embeddings import (AngleEmbedding, BasisEmbedding, AmplitudeEmbedding, @@ -419,7 +418,7 @@ class TestIQPEmbedding: def test_queue_default_pattern(self, n_wires, expected_queue, n_repeats): """Checks the queue for the default pattern.""" - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: qml.templates.IQPEmbedding(features=list(range(n_wires)), wires=range(n_wires), n_repeats=n_repeats) expected_queue = expected_queue * n_repeats @@ -436,7 +435,7 @@ def test_queue_default_pattern(self, n_wires, expected_queue, n_repeats): def test_queue_parameters(self, features, expected_params, wires): """Checks the queued parameters, for consecutive and non-consecutive ``wires`` argument.""" - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: qml.templates.IQPEmbedding(features=features, wires=wires) # compare all nonempty gate parameters to expected ones @@ -454,7 +453,7 @@ def test_queue_correct_wires(self, wires, expected_queue_wires): """Checks the queued wires for a consecutive and non-consecutive sequence of indices in the ``wires`` argument.""" - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: qml.templates.IQPEmbedding(features=list(range(3)), wires=wires) # compare all gate wires to expected ones @@ -466,7 +465,7 @@ def test_queue_correct_wires(self, wires, expected_queue_wires): def test_wires_custom_pattern(self, pattern): """Checks the queue for a custom pattern.""" - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: qml.templates.IQPEmbedding(features=list(range(4)), wires=range(4), pattern=pattern) counter = 0 @@ -521,7 +520,7 @@ class TestQAOAEmbedding: def test_queue(self, n_wires, weight_shape, expected_queue): """Checks the queue for the default settings.""" - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: QAOAEmbedding(features=list(range(n_wires)), weights=np.zeros(shape=weight_shape), wires=range(n_wires)) for gate, expected_gate in zip(rec.queue, expected_queue): diff --git a/tests/templates/test_layer.py b/tests/templates/test_layer.py index ace1106f1ca..0ad41abdeeb 100644 --- a/tests/templates/test_layer.py +++ b/tests/templates/test_layer.py @@ -109,7 +109,7 @@ def unitary(param, wire): def test_layer(self, unitary, depth, arguments, keywords, gates): """Tests that the layering function is yielding the correct sequence of gates""" - with qml._queuing.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: layer(unitary, depth, *arguments, **keywords) for i, gate in enumerate(rec.operations): diff --git a/tests/templates/test_layers.py b/tests/templates/test_layers.py index f74e74a14f9..8c1bf90c3c0 100644 --- a/tests/templates/test_layers.py +++ b/tests/templates/test_layers.py @@ -18,7 +18,6 @@ # pylint: disable=protected-access,cell-var-from-loop import pytest import pennylane as qml -import pennylane._queuing from pennylane import numpy as np from pennylane.templates.layers import ( CVNeuralNetLayers, @@ -120,7 +119,7 @@ def weights(self): def test_cvneuralnet_uses_correct_weights(self, weights): """Tests that the CVNeuralNetLayers template uses the weigh parameters correctly.""" - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: CVNeuralNetLayers(*weights, wires=range(4)) # Test that gates appear in the right order for each layer: @@ -204,7 +203,7 @@ class TestStronglyEntangling: @pytest.mark.parametrize("n_layers", range(1, 4)) def test_single_qubit(self, n_layers): weights = np.zeros((n_layers, 1, 3)) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: StronglyEntanglingLayers(weights, wires=range(1)) assert len(rec.queue) == n_layers @@ -219,7 +218,7 @@ def test_uses_correct_weights(self, n_subsystems): weights = np.random.randn(n_layers, num_wires, 3) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: StronglyEntanglingLayers(weights, wires=range(num_wires)) # Test that gates appear in the right order @@ -246,7 +245,7 @@ def test_uses_correct_number_of_imprimitives(self, n_layers, n_subsystems): imprimitive = CZ weights = np.random.randn(n_layers, n_subsystems, 3) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: StronglyEntanglingLayers( weights=weights, wires=range(n_subsystems), imprimitive=imprimitive ) @@ -363,7 +362,7 @@ def test_random_layers_nlayers(self, n_layers): impr = CNOT weights = np.random.randn(n_layers, n_rots) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: RandomLayers(weights=weights, wires=range(n_wires)) types = [type(q) for q in rec.queue] @@ -376,7 +375,7 @@ def test_random_layer_ratio_imprimitive(self, ratio): impr = CNOT weights = np.random.randn(n_rots) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: random_layer( weights=weights, wires=Wires(range(n_wires)), @@ -395,7 +394,7 @@ def test_random_layer_gate_types(self, n_subsystems, impr, rots): n_rots = 20 weights = np.random.randn(n_rots) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: random_layer( weights=weights, wires=Wires(range(n_subsystems)), @@ -415,7 +414,7 @@ def test_random_layer_numgates(self, n_subsystems): n_rots = 5 weights = np.random.randn(n_rots) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: random_layer( weights=weights, wires=Wires(range(n_subsystems)), @@ -433,7 +432,7 @@ def test_random_layer_randomwires(self, n_subsystems): n_rots = 500 weights = np.random.randn(n_rots) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: random_layer( weights=weights, wires=Wires(range(n_subsystems)), @@ -454,7 +453,7 @@ def test_random_layer_weights(self, n_subsystems, tol): n_rots = 5 weights = np.random.randn(n_rots) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: random_layer( weights=weights, wires=Wires(range(n_subsystems)), @@ -515,7 +514,7 @@ def test_circuit_queue(self, n_wires, n_layers, shape_weights): initial_layer = np.random.randn(n_wires) weights = np.random.randn(*shape_weights) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: SimplifiedTwoDesign(initial_layer, weights, wires=range(n_wires)) # Test that gates appear in the right order @@ -538,7 +537,7 @@ def test_circuit_parameters(self, n_wires, n_layers, shape_weights): initial_layer = np.random.randn(n_wires) weights = np.random.randn(*shape_weights) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: SimplifiedTwoDesign(initial_layer, weights, wires=range(n_wires)) # test the device parameters @@ -620,7 +619,7 @@ def test_circuit_queue(self, n_wires, n_cnots): weights = np.random.randn(n_layers, n_wires) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: BasicEntanglerLayers(weights, wires=range(n_wires)) # Test that gates appear in the right order @@ -639,7 +638,7 @@ def test_circuit_parameters(self, n_wires, n_cnots): weights = np.random.randn(n_layers, n_wires) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: BasicEntanglerLayers(weights, wires=range(n_wires)) # test the device parameters @@ -660,7 +659,7 @@ def test_custom_rotation(self, rotation): n_wires = 4 weights = np.ones(shape=(n_layers, n_wires)) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: BasicEntanglerLayers(weights, wires=range(n_wires), rotation=rotation) # assert queue contains the custom rotations and CNOTs only @@ -734,7 +733,7 @@ def test_u2_operations(self, layers, qubits, init_state): [qml.RZ] * qubits + ([qml.CNOT] + [qml.CRX] + [qml.CNOT]) * (qubits - 1) ) * layers - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: ParticleConservingU2(weights, wires=range(qubits), init_state=init_state) # number of gates @@ -914,7 +913,7 @@ def test_particle_conserving_u1_operations(self): nm_wires = [wires.subset([l, l + 1]) for l in range(0, qubits - 1, 2)] nm_wires += [wires.subset([l, l + 1]) for l in range(1, qubits - 1, 2)] - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: ParticleConservingU1(weights, wires, init_state=np.array([1, 1, 0, 0])) assert gate_count == len(rec.queue) diff --git a/tests/templates/test_state_preparations.py b/tests/templates/test_state_preparations.py index e08641d230c..e8ef54d96c6 100644 --- a/tests/templates/test_state_preparations.py +++ b/tests/templates/test_state_preparations.py @@ -22,7 +22,6 @@ import numpy as np import pytest import pennylane as qml -import pennylane._queuing from pennylane.templates.state_preparations import (BasisStatePreparation, MottonenStatePreparation, ArbitraryStatePreparation) @@ -368,7 +367,7 @@ def test_correct_gates_single_wire(self): """Test that the correct gates are applied on a single wire.""" weights = np.array([0, 1], dtype=float) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: ArbitraryStatePreparation(weights, wires=[0]) assert rec.queue[0].name == "PauliRot" @@ -386,7 +385,7 @@ def test_correct_gates_two_wires(self): """Test that the correct gates are applied on on two wires.""" weights = np.array([0, 1, 2, 3, 4, 5], dtype=float) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: ArbitraryStatePreparation(weights, wires=[0, 1]) assert rec.queue[0].name == "PauliRot" diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py index f56de94be5e..1fffe6f4b5e 100644 --- a/tests/templates/test_subroutines.py +++ b/tests/templates/test_subroutines.py @@ -18,7 +18,6 @@ # pylint: disable=protected-access,cell-var-from-loop import pytest import pennylane as qml -import pennylane._queuing from pennylane import numpy as np from pennylane.wires import Wires @@ -148,12 +147,12 @@ def test_clements_beamsplitter_convention(self, tol): phi = [0.234] varphi = [0.42342, 0.1121] - with pennylane.tape.OperationRecorder() as rec_rect: + with qml.tape.OperationRecorder() as rec_rect: Interferometer( theta, phi, varphi, mesh="rectangular", beamsplitter="clements", wires=wires ) - with pennylane.tape.OperationRecorder() as rec_tria: + with qml.tape.OperationRecorder() as rec_tria: Interferometer( theta, phi, varphi, mesh="triangular", beamsplitter="clements", wires=wires ) @@ -177,7 +176,7 @@ def test_one_mode(self, tol): """Test that a one mode interferometer correctly gives a rotation gate""" varphi = [0.42342] - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: Interferometer(theta=[], phi=[], varphi=varphi, wires=0) assert len(rec.queue) == 1 @@ -194,7 +193,7 @@ def test_two_mode_rect(self, tol): phi = [0.234] varphi = [0.42342, 0.1121] - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: Interferometer(theta, phi, varphi, wires=wires) isinstance(rec.queue[0], qml.Beamsplitter) @@ -216,7 +215,7 @@ def test_two_mode_triangular(self, tol): phi = [0.234] varphi = [0.42342, 0.1121] - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: Interferometer(theta, phi, varphi, mesh="triangular", wires=wires) assert len(rec.queue) == 3 @@ -239,10 +238,10 @@ def test_three_mode(self, tol): phi = [0.234, 0.324, 0.234] varphi = [0.42342, 0.234, 0.1121] - with pennylane.tape.OperationRecorder() as rec_rect: + with qml.tape.OperationRecorder() as rec_rect: Interferometer(theta, phi, varphi, wires=wires) - with pennylane.tape.OperationRecorder() as rec_tria: + with qml.tape.OperationRecorder() as rec_tria: Interferometer(theta, phi, varphi, wires=wires) for rec in [rec_rect, rec_tria]: @@ -270,7 +269,7 @@ 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] - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: Interferometer(theta, phi, varphi, wires=wires) assert len(rec.queue) == 10 @@ -296,7 +295,7 @@ 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] - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: Interferometer(theta, phi, varphi, mesh="triangular", wires=wires) assert len(rec.queue) == 10 @@ -439,7 +438,7 @@ def test_single_ex_unitary_operations(self, single_wires, ref_gates): sqg = 10 cnots = 4 * (len(single_wires) - 1) weight = np.pi / 3 - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: SingleExcitationUnitary(weight, wires=single_wires) assert len(rec.queue) == sqg + cnots @@ -513,7 +512,7 @@ def test_correct_gates_single_wire(self): """Test that the correct gates are applied on a single wire.""" weights = np.arange(3, dtype=float) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: ArbitraryUnitary(weights, wires=[0]) assert all(op.name == "PauliRot" and op.wires == Wires([0]) for op in rec.queue) @@ -528,7 +527,7 @@ def test_correct_gates_two_wires(self): """Test that the correct gates are applied on two wires.""" weights = np.arange(15, dtype=float) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: ArbitraryUnitary(weights, wires=[0, 1]) assert all(op.name == "PauliRot" and op.wires == Wires([0, 1]) for op in rec.queue) @@ -743,7 +742,7 @@ def test_double_ex_unitary_operations(self, wires1, wires2, ref_gates): sqg = 72 cnots = 16 * (len(wires1) - 1 + len(wires2) - 1 + 1) weight = np.pi / 3 - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: DoubleExcitationUnitary(weight, wires1=wires1, wires2=wires2) assert len(rec.queue) == sqg + cnots @@ -905,7 +904,7 @@ def test_uccsd_operations(self, s_wires, d_wires, weights, ref_gates): ref_state = np.array([1, 1, 0, 0, 0, 0]) - with pennylane.tape.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: UCCSD(weights, wires, s_wires=s_wires, d_wires=d_wires, init_state=ref_state) assert len(rec.queue) == sqg + cnots + 1 @@ -1152,7 +1151,7 @@ def test_evolution_operations(self, time, hamiltonian, steps, gates): n_wires = 2 - with qml._queuing.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: ApproxTimeEvolution(hamiltonian, time, steps) for i, gate in enumerate(rec.operations): @@ -1298,8 +1297,8 @@ def two_cycle(): two_cycle() # Ensure all operations are SWAPs, and that the wires are the same - assert all(op.name == "SWAP" for op in two_cycle.ops[:-1]) - assert [op.wires.labels for op in two_cycle.ops[:-1]] == expected_wires + assert all(op.name == "SWAP" for op in two_cycle.qtape.operations[:-1]) + assert [op.wires.labels for op in two_cycle.qtape.operations[:-1]] == expected_wires @pytest.mark.parametrize( # For tape need to specify the wire labels @@ -1344,8 +1343,8 @@ def cycle(): cycle() # Ensure all operations are SWAPs, and that the wires are the same - assert all(op.name == "SWAP" for op in cycle.ops[:-1]) - assert [op.wires.labels for op in cycle.ops[:-1]] == expected_wires + assert all(op.name == "SWAP" for op in cycle.qtape.operations[:-1]) + assert [op.wires.labels for op in cycle.qtape.operations[:-1]] == expected_wires @pytest.mark.parametrize( "permutation_order,wire_order,expected_wires", @@ -1386,8 +1385,8 @@ def arbitrary_perm(): arbitrary_perm() # Ensure all operations are SWAPs, and that the wires are the same - assert all(op.name == "SWAP" for op in arbitrary_perm.ops[:-1]) - assert [op.wires.labels for op in arbitrary_perm.ops[:-1]] == expected_wires + assert all(op.name == "SWAP" for op in arbitrary_perm.qtape.operations[:-1]) + assert [op.wires.labels for op in arbitrary_perm.qtape.operations[:-1]] == expected_wires @pytest.mark.parametrize( "permutation_order,wire_order,expected_wires", @@ -1438,8 +1437,8 @@ def subset_perm(): subset_perm() # Ensure all operations are SWAPs, and that the wires are the same - assert all(op.name == "SWAP" for op in subset_perm.ops[:-1]) - assert [op.wires.labels for op in subset_perm.ops[:-1]] == expected_wires + assert all(op.name == "SWAP" for op in subset_perm.qtape.operations[:-1]) + assert [op.wires.labels for op in subset_perm.qtape.operations[:-1]] == expected_wires @pytest.mark.parametrize( "wire_labels,permutation_order,wire_subset,expected_wires", diff --git a/tests/templates/test_templ_utils.py b/tests/templates/test_templ_utils.py index b5c5282c384..eb9429271dc 100644 --- a/tests/templates/test_templ_utils.py +++ b/tests/templates/test_templ_utils.py @@ -99,12 +99,12 @@ TYPE_PASS = [(["a"], list, type(None)), (1, int, type(None)), ("a", int, str), - (list, int) + (list, str, int) ] TYPE_FAIL = [("a", list, type(None)), - (int, list), - (1., type(None)) + (type(None), int, list), + (1., "a", type(None)) ] diff --git a/tests/test_about.py b/tests/test_about.py index a1314362945..1187744325b 100644 --- a/tests/test_about.py +++ b/tests/test_about.py @@ -45,7 +45,6 @@ def test_about(): assert "default.gaussian" in out -@pytest.mark.skip(reason="This test causes tape mode to be reset") def test_qchem_not_installed_error(monkeypatch): """Test QChem causes an import error on access if not installed""" diff --git a/tests/test_device.py b/tests/test_device.py index 3c91c916b63..ab0b4c8730d 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -20,9 +20,7 @@ import pytest import numpy as np import pennylane as qml -from pennylane import Device, DeviceError -from pennylane.qnodes import QuantumFunctionError -from pennylane.qnodes.base import BaseQNode +from pennylane import Device, DeviceError, QuantumFunctionError from pennylane.wires import Wires from collections import OrderedDict @@ -446,7 +444,7 @@ def circuit_gauss(mag_alpha, phase_alpha, phi): qml.Rotation(phi, wires=0) return qml.expval(qml.NumberOperator(0)) - node_gauss = BaseQNode(circuit_gauss, dev_gauss) + node_gauss = qml.QNode(circuit_gauss, dev_gauss) num_evals_gauss = 12 for i in range(num_evals_gauss): @@ -665,7 +663,7 @@ def test_unsupported_observable_return_type_raise_error(self, mock_device_with_p obs.return_type = "SomeUnsupportedReturnType" observables = [obs] - with pytest.raises(QuantumFunctionError, match="Unsupported return type specified for observable"): + with pytest.raises(qml.QuantumFunctionError, match="Unsupported return type specified for observable"): dev.execute(queue, observables) diff --git a/tests/test_hermitian_edge_cases.py b/tests/test_hermitian_edge_cases.py index ebaca1159f1..f6221d1e403 100644 --- a/tests/test_hermitian_edge_cases.py +++ b/tests/test_hermitian_edge_cases.py @@ -4,8 +4,6 @@ from scipy.linalg import block_diag import pennylane as qml -from pennylane.qnodes.qubit import QubitQNode -from pennylane.qnodes.base import QuantumFunctionError from gate_data import Y, Z diff --git a/tests/test_measure.py b/tests/test_measure.py index d3f644e9e9b..ef6ea68ba74 100644 --- a/tests/test_measure.py +++ b/tests/test_measure.py @@ -16,7 +16,6 @@ import numpy as np import pennylane as qml -from pennylane import QuantumFunctionError from pennylane.devices import DefaultQubit from pennylane.queuing import AnnotatedQueue @@ -46,7 +45,7 @@ def circuit(x): qml.RX(x, wires=0) return qml.PauliY(0) - with pytest.raises(QuantumFunctionError, match="must return either a single measurement"): + with pytest.raises(qml.QuantumFunctionError, match="must return either a single measurement"): res = circuit(0.65) @@ -69,7 +68,7 @@ def circuit(x): assert np.allclose(res, expected, atol=tol, rtol=0) def test_not_an_observable(self): - """Test that a QuantumFunctionError is raised if the provided + """Test that a qml.QuantumFunctionError is raised if the provided argument is not an observable""" dev = qml.device("default.qubit", wires=2) @@ -78,7 +77,7 @@ def circuit(): qml.RX(0.52, wires=0) return qml.expval(qml.CNOT(wires=[0, 1])) - with pytest.raises(QuantumFunctionError, match="CNOT is not an observable"): + with pytest.raises(qml.QuantumFunctionError, match="CNOT is not an observable"): res = circuit() def test_observable_return_type_is_expectation(self): @@ -113,7 +112,7 @@ def circuit(x): assert np.allclose(res, expected, atol=tol, rtol=0) def test_not_an_observable(self): - """Test that a QuantumFunctionError is raised if the provided + """Test that a qml.QuantumFunctionError is raised if the provided argument is not an observable""" dev = qml.device("default.qubit", wires=2) @@ -122,7 +121,7 @@ def circuit(): qml.RX(0.52, wires=0) return qml.var(qml.CNOT(wires=[0, 1])) - with pytest.raises(QuantumFunctionError, match="CNOT is not an observable"): + with pytest.raises(qml.QuantumFunctionError, match="CNOT is not an observable"): res = circuit() def test_observable_return_type_is_variance(self): @@ -231,7 +230,7 @@ def circuit(): assert np.array_equal(result[2].shape, (n_sample,)) def test_not_an_observable(self): - """Test that a QuantumFunctionError is raised if the provided + """Test that a qml.QuantumFunctionError is raised if the provided argument is not an observable""" dev = qml.device("default.qubit", wires=2) @@ -240,7 +239,7 @@ def circuit(): qml.RX(0.52, wires=0) return qml.sample(qml.CNOT(wires=[0, 1])) - with pytest.raises(QuantumFunctionError, match="CNOT is not an observable"): + with pytest.raises(qml.QuantumFunctionError, match="CNOT is not an observable"): sample = circuit() def test_observable_return_type_is_sample(self): @@ -361,7 +360,7 @@ class TestBetaStatisticsError: """Tests for errors arising for the beta statistics functions""" def test_not_an_observable(self, stat_func): - """Test that a QuantumFunctionError is raised if the provided + """Test that a qml.QuantumFunctionError is raised if the provided argument is not an observable""" dev = qml.device("default.qubit", wires=2) @@ -370,7 +369,7 @@ def circuit(): qml.RX(0.52, wires=0) return stat_func(qml.CNOT(wires=[0, 1])) - with pytest.raises(QuantumFunctionError, match="CNOT is not an observable"): + with pytest.raises(qml.QuantumFunctionError, match="CNOT is not an observable"): res = circuit() @@ -568,7 +567,7 @@ def func(): return state(), expval(qml.PauliZ(1)) with pytest.raises( - QuantumFunctionError, + qml.QuantumFunctionError, match="The state or density matrix" " cannot be returned in combination" " with other return types", @@ -661,7 +660,7 @@ def func(): with monkeypatch.context() as m: m.setattr(DefaultQubit, "capabilities", lambda *args, **kwargs: capabilities) - with pytest.raises(QuantumFunctionError, match="The current device is not capable"): + with pytest.raises(qml.QuantumFunctionError, match="The current device is not capable"): func() def test_state_not_supported(self, monkeypatch): @@ -673,7 +672,7 @@ def test_state_not_supported(self, monkeypatch): def func(): return state() - with pytest.raises(QuantumFunctionError, match="Returning the state is not supported"): + with pytest.raises(qml.QuantumFunctionError, match="Returning the state is not supported"): func() @pytest.mark.usefixtures("skip_if_no_tf_support") @@ -757,7 +756,7 @@ def func(): qml.CNOT(wires=[wires[i], wires[i + 1]]) return state() - with pytest.raises(QuantumFunctionError, match="custom wire labels"): + with pytest.raises(qml.QuantumFunctionError, match="custom wire labels"): func() @@ -943,7 +942,7 @@ def func(): return density_matrix(0), expval(qml.PauliZ(1)) with pytest.raises( - QuantumFunctionError, + qml.QuantumFunctionError, match="The state or density matrix" " cannot be returned in combination" " with other return types", @@ -964,7 +963,7 @@ def func(): with monkeypatch.context() as m: m.setattr(DefaultQubit, "capabilities", lambda *args, **kwargs: capabilities) with pytest.raises( - QuantumFunctionError, + qml.QuantumFunctionError, match="The current device is not capable" " of returning the state", ): func() @@ -978,7 +977,7 @@ def test_density_matrix_not_supported(self): def func(): return density_matrix(0) - with pytest.raises(QuantumFunctionError, match="Returning the state is not supported"): + with pytest.raises(qml.QuantumFunctionError, match="Returning the state is not supported"): func() @pytest.mark.parametrize("wires", [[0, 2, 3, 1], ["a", -1, "b", 1000]]) @@ -994,5 +993,5 @@ def func(): qml.CNOT(wires=[wires[i], wires[i + 1]]) return density_matrix(0) - with pytest.raises(QuantumFunctionError, match="custom wire labels"): + with pytest.raises(qml.QuantumFunctionError, match="custom wire labels"): func() diff --git a/tests/test_numpy_wrapper.py b/tests/test_numpy_wrapper.py index f89b1025d95..53cfeb9852b 100644 --- a/tests/test_numpy_wrapper.py +++ b/tests/test_numpy_wrapper.py @@ -512,7 +512,7 @@ def circuit(phi=None): phi = np.tensor([[0.04439891, 0.14490549, 3.29725643, 2.51240058]]) - with qml._queuing.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: circuit(phi=phi) for i in range(phi.shape[1]): @@ -534,7 +534,7 @@ def circuit(phi=None): phi = np.tensor([[0.04439891, 0.14490549, 3.29725643]]) - with qml._queuing.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: circuit(phi=phi) # Test the rotation applied diff --git a/tests/test_operation.py b/tests/test_operation.py index f851f652c07..59c3ace98ed 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -22,7 +22,7 @@ from numpy.linalg import multi_dot import pennylane as qml -import pennylane._queuing +import pennylane.queuing from pennylane.operation import Tensor, operation_derivative from gate_data import I, X, Y, Rotx, Roty, Rotz, CRotx, CRoty, CRotz, CNOT, Rot3, Rphi @@ -166,39 +166,6 @@ def test_operation_init(self, test_class, monkeypatch): if n == 0: return - # wrong parameter types - if test_class.do_check_domain: - if test_class.par_domain == "A": - # params must be arrays - with pytest.raises(TypeError, match="Array parameter expected"): - test_class(*n * [0.0], wires=ww) - # params must not be Variables - with pytest.raises(TypeError, match="Array parameter expected"): - test_class(*n * [qml.variable.Variable(0)], wires=ww) - elif test_class.par_domain == "N": - # params must be natural numbers - with pytest.raises(TypeError, match="Natural number"): - test_class(*n * [0.7], wires=ww) - with pytest.raises(TypeError, match="Natural number"): - test_class(*n * [-1], wires=ww) - elif test_class.par_domain == "R": - # params must be real numbers - with pytest.raises(TypeError, match="Real scalar parameter expected"): - test_class(*n * [1j], wires=ww) - elif test_class.par_domain == "L": - # params must be list of numpy arrays - with pytest.raises(TypeError, match="List parameter"): - test_class(*n * [np.eye(2)], wires=ww) - - # if par_domain ever gets overridden to an unsupported value, should raise exception - monkeypatch.setattr(test_class, "par_domain", "junk") - with pytest.raises(ValueError, match="Unknown parameter domain"): - test_class(*pars, wires=ww) - - monkeypatch.setattr(test_class, "par_domain", 7) - with pytest.raises(ValueError, match="Unknown parameter domain"): - test_class(*pars, wires=ww) - @pytest.fixture(scope="function") def qnode_for_inverse(self, mock_device): """Provides a QNode for the subsequent tests of inv""" @@ -392,35 +359,6 @@ class DummyOp(qml.operation.Operation): with pytest.raises(TypeError, match="List elements must be Numpy arrays."): DummyOp([[np.eye(2), "a"]], wires=[0]) - def test_variable_instead_of_array(self): - """Test that an exception is raised if an array is expected but a variable is passed""" - - class DummyOp(qml.operation.Operation): - r"""Dummy custom operation""" - num_wires = 1 - num_params = 1 - par_domain = "A" - grad_method = "F" - - with pytest.raises(TypeError, match="Array parameter expected, got a Variable"): - DummyOp(qml.variable.Variable(0), wires=[0]) - - def test_array_instead_of_flattened_array(self): - """Test that an exception is raised if an array is expected, but an array is passed - to check_domain when flattened=True. In the initial release of the library, this is not - accessible by the developer or the user, but is kept in case it will be used in the future.""" - - class DummyOp(qml.operation.Operation): - r"""Dummy custom operation""" - num_wires = 1 - num_params = 1 - par_domain = "A" - grad_method = "F" - - with pytest.raises(TypeError, match="Flattened array parameter expected"): - op = DummyOp(np.array([1]), wires=[0]) - op.check_domain(np.array([1]), True) - def test_scalar_instead_of_array(self): """Test that an exception is raised if an array is expected but a scalar is passed""" diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index 0b88fb577e5..024b3114509 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -590,7 +590,7 @@ def test_mixer_layer_output(self, mixer, gates): alpha = 1 - with qml._queuing.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: qaoa.mixer_layer(alpha, mixer) for i, j in zip(rec.operations, gates): @@ -620,7 +620,7 @@ def test_cost_layer_output(self, cost, gates): gamma = 1 - with qml._queuing.OperationRecorder() as rec: + with qml.tape.OperationRecorder() as rec: qaoa.cost_layer(gamma, cost) for i, j in zip(rec.operations, gates): diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py index e92b482f860..779529a63b0 100644 --- a/tests/test_qubit_device.py +++ b/tests/test_qubit_device.py @@ -19,14 +19,12 @@ from random import random import pennylane as qml -from pennylane import QubitDevice, DeviceError -from pennylane.qnodes import QuantumFunctionError -from pennylane.qnodes.base import BaseQNode +from pennylane import QubitDevice, DeviceError, QuantumFunctionError from pennylane.operation import Sample, Variance, Expectation, Probability, State from pennylane.circuit_graph import CircuitGraph from pennylane.wires import Wires from pennylane.tape import QuantumTape -from pennylane.tape.measure import state +from pennylane.measure import state mock_qubit_device_paulis = ["PauliX", "PauliY", "PauliZ"] mock_qubit_device_rotations = ["RX", "RY", "RZ"] @@ -166,7 +164,7 @@ def test_op_queue_is_filled_during_execution( observables = [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(1)), qml.sample(qml.PauliZ(2))] - circuit_graph = CircuitGraph(queue + observables, {}, Wires([0, 1, 2])) + circuit_graph = CircuitGraph(queue, observables, Wires([0, 1, 2])) call_history = [] @@ -195,7 +193,7 @@ def test_unsupported_operations_raise_error(self, mock_qubit_device_with_paulis_ observables = [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(1)), qml.sample(qml.PauliZ(2))] - circuit_graph = CircuitGraph(queue + observables, {}, Wires([0, 1, 2])) + circuit_graph = CircuitGraph(queue, observables, Wires([0, 1, 2])) with pytest.raises(DeviceError, match="Gate Hadamard not supported on device"): dev = mock_qubit_device_with_paulis_and_methods() @@ -224,7 +222,7 @@ def test_passing_keyword_arguments_to_execute(self, mock_qubit_device_with_pauli monkeypatch, queue, observables): """Tests that passing keyword arguments to execute propagates those kwargs to the apply() method""" - circuit_graph = CircuitGraph(queue + observables, {}, Wires([0, 1, 2])) + circuit_graph = CircuitGraph(queue, observables, Wires([0, 1, 2])) call_history = {} @@ -262,7 +260,7 @@ def test_unsupported_observables_raise_error(self, mock_qubit_device_with_paulis qml.sample(qml.PauliZ(2)), ] - circuit_graph = CircuitGraph(queue + observables, {}, Wires([0, 1, 2])) + circuit_graph = CircuitGraph(queue, observables, Wires([0, 1, 2])) with pytest.raises(DeviceError, match="Observable Hadamard not supported on device"): dev = mock_qubit_device_with_paulis_and_methods() @@ -280,7 +278,7 @@ def test_unsupported_observable_return_type_raise_error( obs.return_type = "SomeUnsupportedReturnType" observables = [obs] - circuit_graph = CircuitGraph(queue + observables, {}, Wires([0])) + circuit_graph = CircuitGraph(queue, observables, Wires([0])) with monkeypatch.context() as m: m.setattr(QubitDevice, "apply", lambda self, x, **kwargs: None) @@ -724,7 +722,7 @@ def circuit_1(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node_1 = BaseQNode(circuit_1, dev_1) + node_1 = qml.QNode(circuit_1, dev_1) num_evals_1 = 10 for _ in range(num_evals_1): @@ -739,7 +737,7 @@ def circuit_2(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node_2 = BaseQNode(circuit_2, dev_2) + node_2 = qml.QNode(circuit_2, dev_2) num_evals_2 = 5 for _ in range(num_evals_2): @@ -752,7 +750,7 @@ def circuit_3(x, y): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - node_3 = BaseQNode(circuit_3, dev_1) + node_3 = qml.QNode(circuit_3, dev_1) num_evals_3 = 7 for _ in range(num_evals_3): diff --git a/tests/test_qubit_device_adjoint_jacobian.py b/tests/test_qubit_device_adjoint_jacobian.py index 2320e3a7215..ae4de3f32f0 100644 --- a/tests/test_qubit_device_adjoint_jacobian.py +++ b/tests/test_qubit_device_adjoint_jacobian.py @@ -18,7 +18,7 @@ import pennylane as qml from pennylane import numpy as np -from pennylane.tape import QNode, qnode +from pennylane import QNode, qnode class TestAdjointJacobian: diff --git a/tests/test_queuing.py b/tests/test_queuing.py index 2085097f9ce..c1499dc54ec 100644 --- a/tests/test_queuing.py +++ b/tests/test_queuing.py @@ -36,7 +36,8 @@ import pennylane as qml import numpy as np -from pennylane.queuing import AnnotatedQueue, AnnotatedQueue, Queue, QueuingContext, OperationRecorder +from pennylane.queuing import AnnotatedQueue, AnnotatedQueue, Queue, QueuingContext +from pennylane.tape import OperationRecorder @pytest.fixture(scope="function") @@ -390,7 +391,7 @@ def circuit(a, b, c): qml.RX(a, wires=0) qml.RY(b, wires=1) - with qml._queuing.OperationRecorder() as recorder: + with qml.tape.OperationRecorder() as recorder: ops = [ qml.PauliY(0), qml.PauliY(1), @@ -426,7 +427,7 @@ def template(x): for i in range(5): qml.RZ(i * x, wires=0) - with qml._queuing.OperationRecorder() as recorder: + with qml.tape.OperationRecorder() as recorder: template(3) assert str(recorder) == expected_output @@ -455,7 +456,7 @@ def template(x): return qml.var(qml.PauliZ(0)), qml.sample(qml.PauliX(1)) - with qml._queuing.OperationRecorder() as recorder: + with qml.tape.OperationRecorder() as recorder: template(3) assert str(recorder) == expected_output diff --git a/tests/test_utils.py b/tests/test_utils.py index 99416348cdf..f3efe45b373 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -22,7 +22,7 @@ import numpy as np import pennylane as qml -import pennylane._queuing +import pennylane.queuing import pennylane.utils as pu from pennylane import Identity, PauliX, PauliY, PauliZ diff --git a/tests/test_vqe.py b/tests/test_vqe.py index b00984859c4..236099e7ce0 100644 --- a/tests/test_vqe.py +++ b/tests/test_vqe.py @@ -656,7 +656,6 @@ def test_circuits_valid_init(self, ansatz, observables, mock_device): assert len(circuits) == len(observables) assert all(callable(c) for c in circuits) assert all(c.device == dev for c in circuits) - assert all(hasattr(c, "jacobian") for c in circuits) @pytest.mark.parametrize("ansatz, params", CIRCUITS) @pytest.mark.parametrize("observables", OBSERVABLES) @@ -734,8 +733,8 @@ def test_passing_kwargs(self, coeffs, observables, expected): # Checking that the qnodes contain the step size and order for qnode in cost.qnodes: - assert qnode.h == 123 - assert qnode.order == 2 + assert qnode.diff_options["h"] == 123 + assert qnode.diff_options["order"] == 2 @pytest.mark.parametrize("interface", ["tf", "torch", "autograd"]) def test_optimize(self, interface, tf_support, torch_support): @@ -869,7 +868,7 @@ def ansatz(params, **kwargs): qnodes = qml.ExpvalCost(ansatz, h, dev) mt = qml.metric_tensor(qnodes)(p) assert mt.shape == (3, 3) - assert isinstance(md, np.ndarray) + assert isinstance(mt, np.ndarray) def test_multiple_devices(self, mocker): """Test that passing multiple devices to ExpvalCost works correctly""" From 25e6085eb06c21c681cf29b9b2ab736f847f414f Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Sat, 30 Jan 2021 01:30:12 +0800 Subject: [PATCH 04/64] more tests --- pennylane/circuit_graph.py | 121 ---------------------- pennylane/tape/tape.py | 122 ++++++++++++++++++++++ pennylane/transforms/draw.py | 2 + pennylane/transforms/metric_tensor.py | 2 +- tests/circuit_graph/test_qasm.py | 137 +++++++++++++------------ tests/collections/test_collections.py | 62 ++++++----- tests/devices/test_default_qubit_tf.py | 47 ++++----- tests/qnn/test_keras.py | 2 +- tests/tape/test_qnode.py | 15 --- tests/tape/test_tape.py | 8 +- tests/templates/test_embeddings.py | 21 ---- tests/test_operation.py | 68 ------------ tests/test_qubit_device.py | 57 +++++----- 13 files changed, 280 insertions(+), 384 deletions(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 84bc977c100..65b6f94bac6 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -25,45 +25,6 @@ from .circuit_drawer import CHARSETS, CircuitDrawer -OPENQASM_GATES = { - "CNOT": "cx", - "CZ": "cz", - "U3": "u3", - "U2": "u2", - "U1": "u1", - "Identity": "id", - "PauliX": "x", - "PauliY": "y", - "PauliZ": "z", - "Hadamard": "h", - "S": "s", - "S.inv": "sdg", - "T": "t", - "T.inv": "tdg", - "RX": "rx", - "RY": "ry", - "RZ": "rz", - "CRX": "crx", - "CRY": "cry", - "CRZ": "crz", - "SWAP": "swap", - "Toffoli": "ccx", - "CSWAP": "cswap", - "PhaseShift": "u1", -} -""" -dict[str, str]: Maps PennyLane gate names to equivalent QASM gate names. - -Note that QASM has two native gates: - -- ``U`` (equivalent to :class:`~.U3`) -- ``CX`` (equivalent to :class:`~.CNOT`) - -All other gates are defined in the file qelib1.inc: -https://github.com/Qiskit/openqasm/blob/master/examples/generic/qelib1.inc -""" - - def _by_idx(x): """Sorting key for Operators: queue index aka temporal order. @@ -268,88 +229,6 @@ def hash(self): """ return hash(self.serialize()) - def to_openqasm(self, wires=None, rotations=True): - """Serialize the circuit as an OpenQASM 2.0 program. - - Only operations are serialized; all measurements - are assumed to take place in the computational basis. - - .. note:: - - The serialized OpenQASM program assumes that gate definitions - in ``qelib1.inc`` are available. - - Args: - wires (Wires or None): the wires to use when serializing the circuit - rotations (bool): in addition to serializing user-specified - operations, also include the gates that diagonalize the - measured wires such that they are in the eigenbasis of the circuit observables. - - Returns: - str: OpenQASM serialization of the circuit - """ - # We import decompose_queue here to avoid a circular import - wires = wires or self.wires - - # add the QASM headers - qasm_str = "OPENQASM 2.0;\n" - qasm_str += 'include "qelib1.inc";\n' - - if self.num_wires == 0: - # empty circuit - return qasm_str - - # create the quantum and classical registers - qasm_str += "qreg q[{}];\n".format(len(wires)) - qasm_str += "creg c[{}];\n".format(len(wires)) - - # get the user applied circuit operations - operations = self.operations - - if rotations: - # if requested, append diagonalizing gates corresponding - # to circuit observables - operations += self.diagonalizing_gates - - with qml.tape.QuantumTape() as tape: - for op in operations: - op.queue() - - if op.inverse: - op.inv() - - # decompose the queue - operations = tape.expand(stop_at=lambda obj: obj.name in OPENQASM_GATES).operations - - # create the QASM code representing the operations - for op in operations: - try: - gate = OPENQASM_GATES[op.name] - except KeyError as e: - raise ValueError(f"Operation {op.name} not supported by the QASM serializer") from e - - wire_labels = ",".join([f"q[{wires.index(w)}]" for w in op.wires.tolist()]) - params = "" - - if op.num_params > 0: - # If the operation takes parameters, construct a string - # with parameter values. - params = "(" + ",".join([str(p) for p in op.parameters]) + ")" - - qasm_str += "{name}{params} {wires};\n".format( - name=gate, params=params, wires=wire_labels - ) - - # apply computational basis measurements to each quantum register - # NOTE: This is not strictly necessary, we could inspect self.observables, - # and then only measure wires which are requested by the user. However, - # some devices which consume QASM require all registers be measured, so - # measure all wires to be safe. - for wire in range(len(wires)): - qasm_str += "measure q[{wire}] -> c[{wire}];\n".format(wire=wire) - - return qasm_str - @property def observables_in_order(self): """Observables in the circuit, in a fixed topological order. diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py index 64f2ec4331e..3bb0b84d82b 100644 --- a/pennylane/tape/tape.py +++ b/pennylane/tape/tape.py @@ -22,6 +22,7 @@ import numpy as np import pennylane as qml +import pennylane.grouping from pennylane.queuing import AnnotatedQueue, QueuingContext from pennylane.operation import Sample @@ -39,6 +40,45 @@ ) +OPENQASM_GATES = { + "CNOT": "cx", + "CZ": "cz", + "U3": "u3", + "U2": "u2", + "U1": "u1", + "Identity": "id", + "PauliX": "x", + "PauliY": "y", + "PauliZ": "z", + "Hadamard": "h", + "S": "s", + "S.inv": "sdg", + "T": "t", + "T.inv": "tdg", + "RX": "rx", + "RY": "ry", + "RZ": "rz", + "CRX": "crx", + "CRY": "cry", + "CRZ": "crz", + "SWAP": "swap", + "Toffoli": "ccx", + "CSWAP": "cswap", + "PhaseShift": "u1", +} +""" +dict[str, str]: Maps PennyLane gate names to equivalent QASM gate names. + +Note that QASM has two native gates: + +- ``U`` (equivalent to :class:`~.U3`) +- ``CX`` (equivalent to :class:`~.CNOT`) + +All other gates are defined in the file qelib1.inc: +https://github.com/Qiskit/openqasm/blob/master/examples/generic/qelib1.inc +""" + + def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False): """Expand all objects in a tape to a specific depth. @@ -949,6 +989,88 @@ def draw(self, charset="unicode", wire_order=None, show_all_wires=False): show_all_wires=show_all_wires, ) + def to_openqasm(self, wires=None, rotations=True): + """Serialize the circuit as an OpenQASM 2.0 program. + + Only operations are serialized; all measurements + are assumed to take place in the computational basis. + + .. note:: + + The serialized OpenQASM program assumes that gate definitions + in ``qelib1.inc`` are available. + + Args: + wires (Wires or None): the wires to use when serializing the circuit + rotations (bool): in addition to serializing user-specified + operations, also include the gates that diagonalize the + measured wires such that they are in the eigenbasis of the circuit observables. + + Returns: + str: OpenQASM serialization of the circuit + """ + # We import decompose_queue here to avoid a circular import + wires = wires or self.wires + + # add the QASM headers + qasm_str = "OPENQASM 2.0;\n" + qasm_str += 'include "qelib1.inc";\n' + + if self.num_wires == 0: + # empty circuit + return qasm_str + + # create the quantum and classical registers + qasm_str += "qreg q[{}];\n".format(len(wires)) + qasm_str += "creg c[{}];\n".format(len(wires)) + + # get the user applied circuit operations + operations = self.operations + + if rotations: + # if requested, append diagonalizing gates corresponding + # to circuit observables + operations += self.diagonalizing_gates + + with QuantumTape() as tape: + for op in operations: + op.queue() + + if op.inverse: + op.inv() + + # decompose the queue + operations = tape.expand(stop_at=lambda obj: obj.name in OPENQASM_GATES).operations + + # create the QASM code representing the operations + for op in operations: + try: + gate = OPENQASM_GATES[op.name] + except KeyError as e: + raise ValueError(f"Operation {op.name} not supported by the QASM serializer") from e + + wire_labels = ",".join([f"q[{wires.index(w)}]" for w in op.wires.tolist()]) + params = "" + + if op.num_params > 0: + # If the operation takes parameters, construct a string + # with parameter values. + params = "(" + ",".join([str(p) for p in op.parameters]) + ")" + + qasm_str += "{name}{params} {wires};\n".format( + name=gate, params=params, wires=wire_labels + ) + + # apply computational basis measurements to each quantum register + # NOTE: This is not strictly necessary, we could inspect self.observables, + # and then only measure wires which are requested by the user. However, + # some devices which consume QASM require all registers be measured, so + # measure all wires to be safe. + for wire in range(len(wires)): + qasm_str += "measure q[{wire}] -> c[{wire}];\n".format(wire=wire) + + return qasm_str + @property def data(self): """Alias to :meth:`~.get_parameters` and :meth:`~.set_parameters` diff --git a/pennylane/transforms/draw.py b/pennylane/transforms/draw.py index c46e8aca1ee..c929c34c7c8 100644 --- a/pennylane/transforms/draw.py +++ b/pennylane/transforms/draw.py @@ -16,6 +16,8 @@ """ from functools import wraps +import pennylane as qml + def draw(qnode, charset="unicode", wire_order=None, show_all_wires=False): """Create a function that draws the given qnode. diff --git a/pennylane/transforms/metric_tensor.py b/pennylane/transforms/metric_tensor.py index 905f03e26fb..4a0c1f25c6b 100644 --- a/pennylane/transforms/metric_tensor.py +++ b/pennylane/transforms/metric_tensor.py @@ -142,7 +142,7 @@ def metric_tensor_tape(tape, diag_approx=False, wrt=None): obs_list[-1].append(gen(w)) else: - raise qml.qnodes.QuantumFunctionError( + raise qml.QuantumFunctionError( "Can't generate metric tensor, generator {}" "has no corresponding observable".format(gen) ) diff --git a/tests/circuit_graph/test_qasm.py b/tests/circuit_graph/test_qasm.py index 1e809e433a2..57f6ecdb7ef 100644 --- a/tests/circuit_graph/test_qasm.py +++ b/tests/circuit_graph/test_qasm.py @@ -31,7 +31,7 @@ class TestToQasmUnitTests: def test_empty_circuit(self): """Test that an empty circuit graph is properly serialized into an empty QASM program.""" - circuit = CircuitGraph([], {}, Wires([])) + circuit = qml.tape.QuantumTape() res = circuit.to_openqasm() expected = 'OPENQASM 2.0;\ninclude "qelib1.inc";\n' assert res == expected @@ -39,17 +39,15 @@ def test_empty_circuit(self): def test_native_qasm_gates(self): """Test that a circuit containing solely native QASM gates is properly serialized.""" - ops = [ - qml.RX(0.43, wires=0), - qml.RY(0.35, wires=1), - qml.RZ(0.35, wires=2), - qml.CNOT(wires=[0, 1]), - qml.Hadamard(wires=2), - qml.CNOT(wires=[2, 0]), - qml.PauliX(wires=1), - ] - - circuit = CircuitGraph(ops, [], Wires([0, 1, 2])) + with qml.tape.QuantumTape() as circuit: + qml.RX(0.43, wires=0) + qml.RY(0.35, wires=1) + qml.RZ(0.35, wires=2) + qml.CNOT(wires=[0, 1]) + qml.Hadamard(wires=2) + qml.CNOT(wires=[2, 0]) + qml.PauliX(wires=1) + res = circuit.to_openqasm() expected = dedent( @@ -76,14 +74,12 @@ def test_native_qasm_gates(self): def test_native_inverse_gates(self): """Test that a circuit containing inverse gates that are supported natively by QASM, such as sdg, are correctly serialized.""" - ops = [ - qml.S(wires=0), - qml.S(wires=0).inv(), - qml.T(wires=0), + with qml.tape.QuantumTape() as circuit: + qml.S(wires=0) + qml.S(wires=0).inv() + qml.T(wires=0) qml.T(wires=0).inv(), - ] - circuit = CircuitGraph(ops, [], Wires([0])) res = circuit.to_openqasm() expected = dedent( @@ -104,13 +100,11 @@ def test_native_inverse_gates(self): def test_unused_wires(self): """Test that unused wires are correctly taken into account""" - ops = [ - qml.Hadamard(wires=4), - qml.CNOT(wires=[1, 0]), - ] + with qml.tape.QuantumTape() as circuit: + qml.Hadamard(wires=4) + qml.CNOT(wires=[1, 0]) - circuit = CircuitGraph(ops, [], Wires([0, 1, 2, 3, 4])) - res = circuit.to_openqasm() + res = circuit.to_openqasm(wires=Wires([0, 1, 2, 3, 4])) expected = dedent( """\ @@ -133,13 +127,15 @@ def test_unused_wires(self): def test_rotation_gate_decomposition(self): """Test that gates not natively supported by QASM, such as the rotation gate, are correctly decomposed and serialized.""" - ops1 = [qml.Rot(0.3, 0.1, 0.2, wires=1)] - circuit1 = CircuitGraph(ops1, {}, Wires([0, 1])) - qasm1 = circuit1.to_openqasm() + with qml.tape.QuantumTape() as circuit1: + qml.Rot(0.3, 0.1, 0.2, wires=1) - ops2 = qml.Rot.decomposition(0.3, 0.1, 0.2, wires=1) - circuit2 = CircuitGraph(ops2, {}, Wires([0, 1])) - qasm2 = circuit2.to_openqasm() + qasm1 = circuit1.to_openqasm(wires=Wires([0, 1])) + + with qml.tape.QuantumTape() as circuit2: + qml.Rot.decomposition(0.3, 0.1, 0.2, wires=1) + + qasm2 = circuit2.to_openqasm(wires=Wires([0, 1])) expected = dedent( """\ @@ -163,13 +159,15 @@ def test_state_initialization_decomposition(self): is correctly applied.""" psi = np.array([1, -1, -1, 1]) / np.sqrt(4) - ops1 = [qml.QubitStateVector(psi, wires=[0, 1])] - circuit1 = CircuitGraph(ops1, {}, Wires([0, 1])) + with qml.tape.QuantumTape() as circuit1: + qml.QubitStateVector(psi, wires=[0, 1]) + qasm1 = circuit1.to_openqasm() - ops2 = qml.QubitStateVector.decomposition(psi, wires=[0, 1]) - circuit2 = CircuitGraph(ops2, {}, Wires([0, 1])) - qasm2 = circuit2.to_openqasm() + with qml.tape.QuantumTape() as circuit2: + qml.QubitStateVector.decomposition(psi, wires=[0, 1]) + + qasm2 = circuit2.to_openqasm(wires=Wires([0, 1])) expected = dedent( """\ @@ -194,17 +192,18 @@ def test_state_initialization_decomposition(self): def test_basis_state_initialization_decomposition(self): """Test that the basis state preparation decomposition - is correctly applied.""" basis_state = np.array([1, 0, 1, 1]) - ops1 = [qml.BasisState(basis_state, wires=[0, 1, 2, 3])] - circuit1 = CircuitGraph(ops1, {}, Wires([0, 1, 2, 3])) + with qml.tape.QuantumTape() as circuit1: + qml.BasisState(basis_state, wires=[0, 1, 2, 3]) + qasm1 = circuit1.to_openqasm() - ops2 = qml.BasisState.decomposition(basis_state, wires=[0, 1, 2, 3]) - circuit2 = CircuitGraph(ops2, {}, Wires([0, 1, 2, 3])) - qasm2 = circuit2.to_openqasm() + with qml.tape.QuantumTape() as circuit2: + qml.BasisState.decomposition(basis_state, wires=[0, 1, 2, 3]) + + qasm2 = circuit2.to_openqasm(wires=[0, 1, 2, 3]) expected = dedent( """\ @@ -229,9 +228,9 @@ def test_unsupported_gate(self): """Test an exception is raised if an unsupported operation is applied.""" U = np.array([[1, 1], [1, -1]]) / np.sqrt(2) - ops = [qml.S(wires=0), qml.QubitUnitary(U, wires=[0, 1])] - circuit = CircuitGraph(ops, [], Wires([0, 1])) + with qml.tape.QuantumTape() as circuit: + qml.S(wires=0), qml.QubitUnitary(U, wires=[0, 1]) with pytest.raises( ValueError, match="QubitUnitary not supported by the QASM serializer" @@ -240,15 +239,14 @@ def test_unsupported_gate(self): def test_rotations(self): """Test that observable rotations are correctly applied.""" - ops = [ - qml.Hadamard(wires=0), - qml.CNOT(wires=[0, 1]), - qml.expval(qml.PauliX(0)), - qml.expval(qml.PauliZ(1)), - qml.expval(qml.Hadamard(2)), - ] - - circuit = CircuitGraph(ops, [], Wires([0, 1, 2])) + + with qml.tape.QuantumTape() as circuit: + qml.Hadamard(wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliX(0)) + qml.expval(qml.PauliZ(1)) + qml.expval(qml.Hadamard(2)) + res = circuit.to_openqasm() expected = dedent( @@ -270,7 +268,10 @@ def test_rotations(self): assert res == expected ops2 = circuit.operations + circuit.diagonalizing_gates - circuit2 = CircuitGraph(ops2, {}, Wires([0, 1, 2])) + + with qml.tape.QuantumTape() as circuit2: + [o.queue() for o in ops2] + qasm2 = circuit2.to_openqasm() assert res == qasm2 @@ -292,7 +293,7 @@ def qnode(): # construct the qnode circuit qnode() - res = qnode.qtape.graph.to_openqasm() + res = qnode.qtape.to_openqasm() expected = dedent( """\ OPENQASM 2.0; @@ -322,7 +323,7 @@ def qnode(): # construct the qnode circuit qnode() - res = qnode.qtape.graph.to_openqasm() + res = qnode.qtape.to_openqasm() expected = dedent( """\ @@ -386,7 +387,7 @@ def qnode(x, y): """ ) - res = qnode.qtape.graph.to_openqasm() + res = qnode.qtape.to_openqasm() assert res == expected # execute the QNode with new parameters, and serialize again @@ -412,7 +413,7 @@ def qnode(x, y): """ ) - res = qnode.qtape.graph.to_openqasm() + res = qnode.qtape.to_openqasm() assert res == expected def test_native_inverse_gates(self): @@ -430,7 +431,7 @@ def qnode(): # construct the qnode circuit qnode() - res = qnode.qtape.graph.to_openqasm() + res = qnode.qtape.to_openqasm() expected = dedent( """\ @@ -460,7 +461,7 @@ def qnode(): # construct the qnode circuit qnode() - res = qnode.qtape.graph.to_openqasm(wires=dev.wires) + res = qnode.qtape.to_openqasm(wires=dev.wires) expected = dedent( """\ @@ -492,7 +493,7 @@ def qnode(): # construct the qnode circuit qnode() - res = qnode.qtape.graph.to_openqasm(wires=dev.wires) + res = qnode.qtape.to_openqasm(wires=dev.wires) expected = dedent( """\ @@ -522,7 +523,7 @@ def qnode(state=None): # construct the qnode circuit qnode(state=np.array([1, -1, -1, 1]) / np.sqrt(4)) - res = qnode.qtape.graph.to_openqasm() + res = qnode.qtape.to_openqasm() expected = dedent( """\ @@ -556,7 +557,7 @@ def qnode(state=None): # construct the qnode circuit qnode(state=np.array([1, 0, 1, 1])) - res = qnode.qtape.graph.to_openqasm() + res = qnode.qtape.to_openqasm() expected = dedent( """\ @@ -593,7 +594,7 @@ def qnode(): with pytest.raises( ValueError, match="QubitUnitary not supported by the QASM serializer" ): - qnode.qtape.graph.to_openqasm() + qnode.qtape.to_openqasm() def test_rotations(self): """Test that observable rotations are correctly applied.""" @@ -610,7 +611,7 @@ def qnode(): ] qnode() - res = qnode.qtape.graph.to_openqasm() + res = qnode.qtape.to_openqasm() expected = dedent( """\ @@ -645,7 +646,7 @@ def qnode(): ] qnode() - res = qnode.qtape.graph.to_openqasm() + res = qnode.qtape.to_openqasm() expected = dedent( """\ @@ -696,7 +697,7 @@ def qnode(x): ] qnode([0.1, 0.2]) - res = qnode.qtape.graph.to_openqasm() + res = qnode.qtape.to_openqasm() # Note: Qiskit hardcodes in pi as a QASM constant. # Here, we replace it with its numerical value. @@ -718,7 +719,7 @@ def qnode(state=None): # construct the qnode circuit qnode(state=np.array([1, 0, 1, 1])) - res = qnode.qtape.graph.to_openqasm(wires=dev.wires) + res = qnode.qtape.to_openqasm(wires=dev.wires) expected = dev._circuit.qasm() assert res == expected @@ -742,7 +743,7 @@ def qnode(x): params = [0.1, 0.2] qnode(params) - qasm = qnode.qtape.graph.to_openqasm() + qasm = qnode.qtape.to_openqasm() qc = self.qiskit.QuantumCircuit.from_qasm_str(qasm) gates = [g for g, _, _ in qc.data] diff --git a/tests/collections/test_collections.py b/tests/collections/test_collections.py index 960215be522..b50e752d5a6 100644 --- a/tests/collections/test_collections.py +++ b/tests/collections/test_collections.py @@ -63,13 +63,15 @@ def test_mapping_over_observables(self): # evaluate collection so that queue is populated qc(1) - assert len(qc[0].qtape.operations) == 2 - assert qc[0].qtape.operations[0].name == "RX" - assert qc[0].qtape.operations[1].name == "PauliX" + queue = qc[0].qtape.operations + qc[0].qtape.observables + assert len(queue) == 2 + assert queue[0].name == "RX" + assert queue[1].name == "PauliX" - assert len(qc[1].qtape.operations) == 2 - assert qc[1].qtape.operations[0].name == "RX" - assert qc[1].qtape.operations[1].name == "PauliY" + queue = qc[1].qtape.operations + qc[1].qtape.observables + assert len(queue) == 2 + assert queue[0].name == "RX" + assert queue[1].name == "PauliY" def test_mapping_over_observables_as_tuples(self): """Test that mapping over a tuple of observables produces @@ -86,13 +88,15 @@ def test_mapping_over_observables_as_tuples(self): # evaluate collection so that queue is populated qc(1) - assert len(qc[0].qtape.operations) == 2 - assert qc[0].qtape.operations[0].name == "RX" - assert qc[0].qtape.operations[1].name == "PauliX" + queue = qc[0].qtape.operations + qc[0].qtape.observables + assert len(queue) == 2 + assert queue[0].name == "RX" + assert queue[1].name == "PauliX" - assert len(qc[1].qtape.operations) == 2 - assert qc[1].qtape.operations[0].name == "RX" - assert qc[1].qtape.operations[1].name == "PauliY" + queue = qc[1].qtape.operations + qc[1].qtape.observables + assert len(queue) == 2 + assert queue[0].name == "RX" + assert queue[1].name == "PauliY" def test_mapping_over_devices(self): """Test that mapping over a list of devices produces @@ -109,13 +113,15 @@ def test_mapping_over_devices(self): # evaluate collection so that queue is populated qc(1) - assert len(qc[0].qtape.operations) == 2 - assert qc[0].qtape.operations[0].name == "RX" - assert qc[0].qtape.operations[1].name == "PauliX" + queue = qc[0].qtape.operations + qc[0].qtape.observables + assert len(queue) == 2 + assert queue[0].name == "RX" + assert queue[1].name == "PauliX" - assert len(qc[1].qtape.operations) == 2 - assert qc[1].qtape.operations[0].name == "RX" - assert qc[1].qtape.operations[1].name == "PauliY" + queue = qc[1].qtape.operations + qc[1].qtape.observables + assert len(queue) == 2 + assert queue[0].name == "RX" + assert queue[1].name == "PauliY" # test that device is not broadcast assert qc[0].device is not qc[1].device @@ -135,15 +141,17 @@ def test_mapping_over_measurements(self): # evaluate collection so that queue is populated qc(1) - assert len(qc[0].qtape.operations) == 2 - assert qc[0].qtape.operations[0].name == "RX" - assert qc[0].qtape.operations[1].name == "PauliX" - assert qc[0].qtape.operations[1].return_type == qml.operation.Expectation - - assert len(qc[1].qtape.operations) == 2 - assert qc[1].qtape.operations[0].name == "RX" - assert qc[1].qtape.operations[1].name == "PauliY" - assert qc[1].qtape.operations[1].return_type == qml.operation.Variance + queue = qc[0].qtape.operations + qc[0].qtape.observables + assert len(queue) == 2 + assert queue[0].name == "RX" + assert queue[1].name == "PauliX" + assert queue[1].return_type == qml.operation.Expectation + + queue = qc[1].qtape.operations + qc[1].qtape.observables + assert len(queue) == 2 + assert queue[0].name == "RX" + assert queue[1].name == "PauliY" + assert queue[1].return_type == qml.operation.Variance def test_invalid_observable(self): """Test that an invalid observable raises an exception""" diff --git a/tests/devices/test_default_qubit_tf.py b/tests/devices/test_default_qubit_tf.py index 31d42006f45..247f367ede1 100644 --- a/tests/devices/test_default_qubit_tf.py +++ b/tests/devices/test_default_qubit_tf.py @@ -419,25 +419,23 @@ class TestExpval: def test_single_wire_expectation(self, gate, obs, expected, theta, phi, varphi, tol): """Test that identity expectation value (i.e. the trace) is 1""" dev = DefaultQubitTF(wires=2) - queue = [gate(theta, wires=0), gate(phi, wires=1), qml.CNOT(wires=[0, 1])] - observables = [obs(wires=[i]) for i in range(2)] - for i in range(len(observables)): - observables[i].return_type = qml.operation.Expectation + with qml.tape.QuantumTape() as tape: + queue = [gate(theta, wires=0), gate(phi, wires=1), qml.CNOT(wires=[0, 1])] + observables = [qml.expval(obs(wires=[i])) for i in range(2)] - res = dev.execute(qml.CircuitGraph(queue, observables, Wires([0, 1, 2]))) + res = dev.execute(tape) assert np.allclose(res, expected(theta, phi), atol=tol, rtol=0) def test_hermitian_expectation(self, theta, phi, varphi, tol): """Test that arbitrary Hermitian expectation values are correct""" dev = DefaultQubitTF(wires=2) - queue = [qml.RY(theta, wires=0), qml.RY(phi, wires=1), qml.CNOT(wires=[0, 1])] - observables = [qml.Hermitian(A, wires=[i]) for i in range(2)] - for i in range(len(observables)): - observables[i].return_type = qml.operation.Expectation + with qml.tape.QuantumTape() as tape: + queue = [qml.RY(theta, wires=0), qml.RY(phi, wires=1), qml.CNOT(wires=[0, 1])] + observables = [qml.expval(qml.Hermitian(A, wires=[i])) for i in range(2)] - res = dev.execute(qml.CircuitGraph(queue, observables, Wires([0, 1]))) + res = dev.execute(tape) a = A[0, 0] re_b = A[0, 1].real @@ -460,13 +458,13 @@ def test_multi_mode_hermitian_expectation(self, theta, phi, varphi, tol): ) dev = DefaultQubitTF(wires=2) - queue = [qml.RY(theta, wires=0), qml.RY(phi, wires=1), qml.CNOT(wires=[0, 1])] - observables = [qml.Hermitian(A, wires=[0, 1])] - for i in range(len(observables)): - observables[i].return_type = qml.operation.Expectation - res = dev.execute(qml.CircuitGraph(queue, observables, Wires([0, 1]))) + with qml.tape.QuantumTape() as tape: + queue = [qml.RY(theta, wires=0), qml.RY(phi, wires=1), qml.CNOT(wires=[0, 1])] + observables = [qml.expval(qml.Hermitian(A, wires=[0, 1]))] + + res = dev.execute(tape) # below is the analytic expectation value for this circuit with arbitrary # Hermitian observable A @@ -700,13 +698,11 @@ def test_var(self, theta, phi, varphi, tol): dev = DefaultQubitTF(wires=1) # test correct variance for of a rotated state - queue = [qml.RX(phi, wires=0), qml.RY(theta, wires=0)] - observables = [qml.PauliZ(wires=[0])] - - for i in range(len(observables)): - observables[i].return_type = qml.operation.Variance + with qml.tape.QuantumTape() as tape: + queue = [qml.RX(phi, wires=0), qml.RY(theta, wires=0)] + observables = [qml.var(qml.PauliZ(wires=[0]))] - res = dev.execute(qml.CircuitGraph(queue, observables, Wires([0]))) + res = dev.execute(tape) expected = 0.25 * (3 - np.cos(2 * theta) - 2 * np.cos(theta) ** 2 * np.cos(2 * phi)) assert np.allclose(res, expected, atol=tol, rtol=0) @@ -716,13 +712,12 @@ def test_var_hermitian(self, theta, phi, varphi, tol): # test correct variance for of a rotated state H = np.array([[4, -1 + 6j], [-1 - 6j, 2]]) - queue = [qml.RX(phi, wires=0), qml.RY(theta, wires=0)] - observables = [qml.Hermitian(H, wires=[0])] - for i in range(len(observables)): - observables[i].return_type = qml.operation.Variance + with qml.tape.QuantumTape() as tape: + queue = [qml.RX(phi, wires=0), qml.RY(theta, wires=0)] + observables = [qml.var(qml.Hermitian(H, wires=[0]))] - res = dev.execute(qml.CircuitGraph(queue, observables, Wires([0]))) + res = dev.execute(tape) expected = 0.5 * ( 2 * np.sin(2 * theta) * np.cos(phi) ** 2 + 24 * np.sin(phi) * np.cos(phi) * (np.sin(theta) - np.cos(theta)) diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index e4993db3580..b3070394c47 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -420,7 +420,7 @@ def f(inputs, weights): with tf.GradientTape() as tape: out = tf.reduce_sum(qlayer(inputs)) - spy = mocker.spy(qml.tape.tapes.QubitParamShiftTape, "jacobian") + spy = mocker.spy(qml.tape.QubitParamShiftTape, "jacobian") grad = tape.gradient(out, qlayer.trainable_weights) assert grad is not None diff --git a/tests/tape/test_qnode.py b/tests/tape/test_qnode.py index 56ceddd300b..623f9f7c754 100644 --- a/tests/tape/test_qnode.py +++ b/tests/tape/test_qnode.py @@ -428,19 +428,6 @@ def circuit(p1, p2, **kwargs): assert result == expected - def test_draw_transform_raises(self): - dev = qml.device("default.qubit", wires=2) - @qml.qnode(dev, interface="autograd") - def circuit(p1, p2, **kwargs): - qml.RX(p1, wires=0) - qml.RY(p2[0] * p2[1], wires=1) - qml.RX(kwargs["p3"], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - with pytest.raises(ValueError, match="only works when tape mode is enabled"): - result = draw(circuit, charset="ascii") - def test_drawing(self): """Test circuit drawing""" from pennylane import numpy as anp @@ -759,8 +746,6 @@ class TestIntegration: def test_correct_number_of_executions_autograd(self): """Test that number of executions are tracked in the autograd interface.""" - qml.enable_tape() - def func(): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index ec3385c7a6c..442992ff4a9 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -857,7 +857,7 @@ def circuit(): qml.T(wires=0) return sample(qml.PauliZ(0)) - qnode = qml.tape.QNode(circuit, dev) + qnode = qml.QNode(circuit, dev) qnode() assert qnode.qtape.is_sampled @@ -1282,7 +1282,7 @@ def test_casting(self): qml.expval(qml.PauliZ(0) @ qml.PauliY(1)) # copy and cast to a JacobianTape - copied_tape = tape.copy(tape_cls=qml.tape.tapes.JacobianTape) + copied_tape = tape.copy(tape_cls=qml.tape.JacobianTape) # check that the copying worked assert copied_tape is not tape @@ -1292,5 +1292,5 @@ def test_casting(self): assert copied_tape.operations[0] is tape.operations[0] # check that the casting worked - assert isinstance(copied_tape, qml.tape.tapes.JacobianTape) - assert not isinstance(tape, qml.tape.tapes.JacobianTape) + assert isinstance(copied_tape, qml.tape.JacobianTape) + assert not isinstance(tape, qml.tape.JacobianTape) diff --git a/tests/templates/test_embeddings.py b/tests/templates/test_embeddings.py index 91fced71c39..fd9f9c5ff1c 100644 --- a/tests/templates/test_embeddings.py +++ b/tests/templates/test_embeddings.py @@ -310,9 +310,6 @@ def circuit(x=None): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") - n_subsystems = 1 dev = qml.device('default.qubit', wires=n_subsystems) @@ -389,8 +386,6 @@ def circuit(x=None): def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" - if not qml.tape_mode_active(): - pytest.skip("This validation is only performed in tape mode") n_subsystems = 2 dev = qml.device('default.qubit', wires=n_subsystems) @@ -491,22 +486,6 @@ def circuit(f=None): with pytest.raises(ValueError, match="Features must be"): circuit(f=features) - def test_exception_incorrect_pattern(self): - """Verifies that an exception is raised if 'pattern' has the wrong shape.""" - if qml.tape_mode_active(): - pytest.skip("Check only done in non-tape mode") - - dev = qml.device('default.qubit', wires=3) - features = [1., 2., 3.] - - @qml.qnode(dev) - def circuit(f=None): - qml.templates.IQPEmbedding(features=f, wires=range(3), pattern=[0., 0.2]) - return [qml.expval(qml.PauliZ(w)) for w in range(3)] - - with pytest.raises(ValueError, match="'pattern' must be a"): - circuit(f=features) - class TestQAOAEmbedding: """ Tests the QAOAEmbedding method.""" diff --git a/tests/test_operation.py b/tests/test_operation.py index 59c3ace98ed..97dad4eb37d 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -267,19 +267,6 @@ class DummyOp(qml.operation.Operator): with pytest.raises(ValueError, match="wrong number of parameters"): DummyOp(0.5, 0.6, wires=0) - def test_incorrect_param_domain(self): - """Test that an exception is raised if an incorrect parameter domain is requested""" - - class DummyOp(qml.operation.Operator): - r"""Dummy custom operator""" - num_wires = 1 - num_params = 1 - par_domain = "J" - grad_method = "A" - - with pytest.raises(ValueError, match="Unknown parameter domain"): - DummyOp(0.5, wires=0) - class TestOperationConstruction: """Test custom operations construction.""" @@ -346,61 +333,6 @@ class DummyOp(qml.operation.Operation): with pytest.raises(AssertionError, match="Gradient recipe is only used by the A method"): DummyOp(0.5, wires=[0, 1]) - def test_list_of_arrays(self): - """Test that an exception is raised if a list of arrays is expected - but a list of mixed types is passed""" - - class DummyOp(qml.operation.Operation): - r"""Dummy custom operation""" - num_wires = 1 - num_params = 1 - par_domain = "L" - - with pytest.raises(TypeError, match="List elements must be Numpy arrays."): - DummyOp([[np.eye(2), "a"]], wires=[0]) - - def test_scalar_instead_of_array(self): - """Test that an exception is raised if an array is expected but a scalar is passed""" - - class DummyOp(qml.operation.Operation): - r"""Dummy custom operation""" - num_wires = 1 - num_params = 1 - par_domain = "A" - grad_method = "F" - - with pytest.raises(TypeError, match="Array parameter expected, got"): - DummyOp(0.5, wires=[0]) - - def test_array_instead_of_real(self): - """Test that an exception is raised if a real number is expected but an array is passed""" - - class DummyOp(qml.operation.Operation): - r"""Dummy custom operation""" - num_wires = 1 - num_params = 1 - par_domain = "R" - grad_method = "F" - - with pytest.raises(TypeError, match="Real scalar parameter expected, got"): - DummyOp(np.array([1.0]), wires=[0]) - - def test_not_natural_param(self): - """Test that an exception is raised if a natural number is expected but not passed""" - - class DummyOp(qml.operation.Operation): - r"""Dummy custom operation""" - num_wires = 1 - num_params = 1 - par_domain = "N" - grad_method = None - - with pytest.raises(TypeError, match="Natural number parameter expected, got"): - DummyOp(0.5, wires=[0]) - - with pytest.raises(TypeError, match="Natural number parameter expected, got"): - DummyOp(-2, wires=[0]) - def test_no_wires_passed(self): """Test exception raised if no wires are passed""" diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py index 779529a63b0..ab0ce23fb50 100644 --- a/tests/test_qubit_device.py +++ b/tests/test_qubit_device.py @@ -160,11 +160,10 @@ def test_op_queue_is_filled_during_execution( ): """Tests that the op_queue is correctly filled when apply is called and that accessing op_queue raises no error""" - queue = [qml.PauliX(wires=0), qml.PauliY(wires=1), qml.PauliZ(wires=2)] - observables = [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(1)), qml.sample(qml.PauliZ(2))] - - circuit_graph = CircuitGraph(queue, observables, Wires([0, 1, 2])) + with qml.tape.QuantumTape() as tape: + queue = [qml.PauliX(wires=0), qml.PauliY(wires=1), qml.PauliZ(wires=2)] + observables = [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(1)), qml.sample(qml.PauliZ(2))] call_history = [] @@ -173,7 +172,7 @@ def test_op_queue_is_filled_during_execution( lambda self, x, **kwargs: call_history.extend(x + kwargs.get('rotations', []))) m.setattr(QubitDevice, "analytic_probability", lambda *args: None) dev = mock_qubit_device_with_paulis_and_methods() - dev.execute(circuit_graph) + dev.execute(tape) assert call_history == queue @@ -189,15 +188,13 @@ def test_op_queue_is_filled_during_execution( def test_unsupported_operations_raise_error(self, mock_qubit_device_with_paulis_and_methods): """Tests that the operations are properly applied and queued""" - queue = [qml.PauliX(wires=0), qml.PauliY(wires=1), qml.Hadamard(wires=2)] - - observables = [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(1)), qml.sample(qml.PauliZ(2))] - - circuit_graph = CircuitGraph(queue, observables, Wires([0, 1, 2])) + with qml.tape.QuantumTape() as tape: + queue = [qml.PauliX(wires=0), qml.PauliY(wires=1), qml.Hadamard(wires=2)] + observables = [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(1)), qml.sample(qml.PauliZ(2))] with pytest.raises(DeviceError, match="Gate Hadamard not supported on device"): dev = mock_qubit_device_with_paulis_and_methods() - dev.execute(circuit_graph) + dev.execute(tape) numeric_queues = [ [ @@ -222,17 +219,19 @@ def test_passing_keyword_arguments_to_execute(self, mock_qubit_device_with_pauli monkeypatch, queue, observables): """Tests that passing keyword arguments to execute propagates those kwargs to the apply() method""" - circuit_graph = CircuitGraph(queue, observables, Wires([0, 1, 2])) + with qml.tape.QuantumTape() as tape: + for op in queue + observables: + op.queue() call_history = {} with monkeypatch.context() as m: m.setattr(QubitDevice, "apply", lambda self, x, **kwargs: call_history.update(kwargs)) dev = mock_qubit_device_with_paulis_rotations_and_methods() - dev.execute(circuit_graph, hash=circuit_graph.hash) + dev.execute(tape, hash=tape.graph.hash) len(call_history.items()) == 1 - call_history["hash"] = circuit_graph.hash + call_history["hash"] = tape.graph.hash class TestObservables: @@ -252,33 +251,27 @@ def test_obs_queue_accessed_outside_execution_context(self, mock_qubit_device): def test_unsupported_observables_raise_error(self, mock_qubit_device_with_paulis_and_methods): """Tests that the operations are properly applied and queued""" - queue = [qml.PauliX(wires=0), qml.PauliY(wires=1), qml.PauliZ(wires=2)] + with qml.tape.QuantumTape() as tape: + queue = [qml.PauliX(wires=0), qml.PauliY(wires=1), qml.PauliZ(wires=2)] - observables = [ - qml.expval(qml.Hadamard(0)), - qml.var(qml.PauliZ(1)), - qml.sample(qml.PauliZ(2)), - ] - - circuit_graph = CircuitGraph(queue, observables, Wires([0, 1, 2])) + observables = [ + qml.expval(qml.Hadamard(0)), + qml.var(qml.PauliZ(1)), + qml.sample(qml.PauliZ(2)), + ] with pytest.raises(DeviceError, match="Observable Hadamard not supported on device"): dev = mock_qubit_device_with_paulis_and_methods() - dev.execute(circuit_graph) + dev.execute(tape) def test_unsupported_observable_return_type_raise_error( self, mock_qubit_device_with_paulis_and_methods, monkeypatch ): """Check that an error is raised if the return type of an observable is unsupported""" - queue = [qml.PauliX(wires=0)] - - # Make a observable without specifying a return operation upon measuring - obs = qml.PauliZ(0) - obs.return_type = "SomeUnsupportedReturnType" - observables = [obs] - - circuit_graph = CircuitGraph(queue, observables, Wires([0])) + with qml.tape.QuantumTape() as tape: + qml.PauliX(wires=0) + qml.measure.MeasurementProcess(return_type="SomeUnsupportedReturnType", obs=qml.PauliZ(0)) with monkeypatch.context() as m: m.setattr(QubitDevice, "apply", lambda self, x, **kwargs: None) @@ -286,7 +279,7 @@ def test_unsupported_observable_return_type_raise_error( QuantumFunctionError, match="Unsupported return type specified for observable" ): dev = mock_qubit_device_with_paulis_and_methods() - dev.execute(circuit_graph) + dev.execute(tape) class TestParameters: From 5074218e30598e83652839a627052cb1a4f92911 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Sat, 30 Jan 2021 02:02:01 +0800 Subject: [PATCH 05/64] more tests --- tests/templates/test_layers.py | 1 + tests/templates/test_subroutines.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/templates/test_layers.py b/tests/templates/test_layers.py index 8c1bf90c3c0..881f4263cbd 100644 --- a/tests/templates/test_layers.py +++ b/tests/templates/test_layers.py @@ -338,6 +338,7 @@ def circuit(weights): second_call = circuit(weights) assert np.allclose(first_call, second_call, atol=tol) + @pytest.mark.xfail(reason="mutable qnodes are deprecated") def test_no_seed(self, tol): """Test that two calls to a qnode with RandomLayers() for 'seed=None' option create the same circuit for immutable qnodes.""" diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py index 1fffe6f4b5e..f0fe089c9aa 100644 --- a/tests/templates/test_subroutines.py +++ b/tests/templates/test_subroutines.py @@ -1259,12 +1259,12 @@ def test_identity_permutation_qnode(self): @qml.qnode(dev) def identity_permutation(): - Permute([0, 1, 2, 3], wires=dev.wires) + ops = Permute([0, 1, 2, 3], wires=dev.wires) return qml.expval(qml.PauliZ(0)) identity_permutation() - assert len(identity_permutation.ops) == 1 + assert len(identity_permutation.qtape.operations) == 0 def test_identity_permutation_tape(self): """ Test that identity permutations have no effect on tapes. """ @@ -1297,8 +1297,8 @@ def two_cycle(): two_cycle() # Ensure all operations are SWAPs, and that the wires are the same - assert all(op.name == "SWAP" for op in two_cycle.qtape.operations[:-1]) - assert [op.wires.labels for op in two_cycle.qtape.operations[:-1]] == expected_wires + assert all(op.name == "SWAP" for op in two_cycle.qtape.operations) + assert [op.wires.labels for op in two_cycle.qtape.operations] == expected_wires @pytest.mark.parametrize( # For tape need to specify the wire labels @@ -1343,8 +1343,8 @@ def cycle(): cycle() # Ensure all operations are SWAPs, and that the wires are the same - assert all(op.name == "SWAP" for op in cycle.qtape.operations[:-1]) - assert [op.wires.labels for op in cycle.qtape.operations[:-1]] == expected_wires + assert all(op.name == "SWAP" for op in cycle.qtape.operations) + assert [op.wires.labels for op in cycle.qtape.operations] == expected_wires @pytest.mark.parametrize( "permutation_order,wire_order,expected_wires", @@ -1385,8 +1385,8 @@ def arbitrary_perm(): arbitrary_perm() # Ensure all operations are SWAPs, and that the wires are the same - assert all(op.name == "SWAP" for op in arbitrary_perm.qtape.operations[:-1]) - assert [op.wires.labels for op in arbitrary_perm.qtape.operations[:-1]] == expected_wires + assert all(op.name == "SWAP" for op in arbitrary_perm.qtape.operations) + assert [op.wires.labels for op in arbitrary_perm.qtape.operations] == expected_wires @pytest.mark.parametrize( "permutation_order,wire_order,expected_wires", @@ -1437,8 +1437,8 @@ def subset_perm(): subset_perm() # Ensure all operations are SWAPs, and that the wires are the same - assert all(op.name == "SWAP" for op in subset_perm.qtape.operations[:-1]) - assert [op.wires.labels for op in subset_perm.qtape.operations[:-1]] == expected_wires + assert all(op.name == "SWAP" for op in subset_perm.qtape.operations) + assert [op.wires.labels for op in subset_perm.qtape.operations] == expected_wires @pytest.mark.parametrize( "wire_labels,permutation_order,wire_subset,expected_wires", From 0f31b36c40ffb3852f5178f0c1b0a7bc448253c1 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Sat, 30 Jan 2021 02:34:52 +0800 Subject: [PATCH 06/64] fixed more tests --- pennylane/operation.py | 2 +- tests/test_measure.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pennylane/operation.py b/pennylane/operation.py index 2ba60282c5b..e0277e4067e 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -459,7 +459,7 @@ def wires(self): @property def parameters(self): """Current parameter values.""" - return self.data + return self.data.copy() def queue(self): """Append the operator to the Operator queue.""" diff --git a/tests/test_measure.py b/tests/test_measure.py index 2cb5a242ad8..eebd5303332 100644 --- a/tests/test_measure.py +++ b/tests/test_measure.py @@ -682,13 +682,13 @@ def func(): @pytest.mark.parametrize( "diff_method", ["best", "finite-diff", "parameter-shift"] ) - def test_devices(self, device, skip_if_no_tf_support): + def test_devices(self, device, diff_method, skip_if_no_tf_support): """Test that the returned state is equal to the expected returned state for all of PennyLane's built in statevector devices""" dev = qml.device(device, wires=4) - @qnode(dev, diff_method=diff_method) + @qml.qnode(dev, diff_method=diff_method) def func(): for i in range(4): qml.Hadamard(i) From e13a1e51f388caac1a8d8aa840c02917ab5927e9 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Sat, 30 Jan 2021 22:59:38 +0800 Subject: [PATCH 07/64] more tests passing --- tests/circuit_drawer/test_circuit_drawer.py | 8 +++++--- tests/circuit_graph/test_circuit_graph_hash.py | 15 ++++++++------- tests/interfaces/test_tape_torch.py | 2 +- tests/templates/test_templ_utils.py | 2 -- tests/test_qubit_device.py | 6 +++--- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/tests/circuit_drawer/test_circuit_drawer.py b/tests/circuit_drawer/test_circuit_drawer.py index fda145e3422..81abc4edda8 100644 --- a/tests/circuit_drawer/test_circuit_drawer.py +++ b/tests/circuit_drawer/test_circuit_drawer.py @@ -623,9 +623,9 @@ def drawn_qubit_circuit_with_probs(): def drawn_qubit_circuit_with_state(): """The rendered circuit representation of the above qubit circuit.""" return ( - " 0: ──X──╭X──┤ State \n" - + " 1: ─────├C──┤ \n" - + " 5: ──X──╰C──┤ \n" + " 0: ──X──╭X──╭┤ State \n" + + " 1: ─────├C──├┤ State \n" + + " 5: ──X──╰C──╰┤ State \n" ) @@ -721,6 +721,8 @@ def test_qubit_circuit_with_state( """Test that a qubit circuit with unused wires renders correctly.""" output = qubit_circuit_with_state.draw() + print(output) + print(drawn_qubit_circuit_with_state) assert output == drawn_qubit_circuit_with_state def test_direct_qnode_integration(self): diff --git a/tests/circuit_graph/test_circuit_graph_hash.py b/tests/circuit_graph/test_circuit_graph_hash.py index 36856c8bd0f..e221a949e00 100644 --- a/tests/circuit_graph/test_circuit_graph_hash.py +++ b/tests/circuit_graph/test_circuit_graph_hash.py @@ -107,7 +107,7 @@ def circuit1(): return qml.expval(qml.PauliZ(0)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([], {}) + node1.construct([], {}) circuit_hash_1 = node1.qtape.graph.hash def circuit2(): @@ -117,7 +117,7 @@ def circuit2(): return qml.expval(qml.PauliZ(0)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([], {}) + node2.construct([], {}) circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 == circuit_hash_2 @@ -192,6 +192,7 @@ def circuit2(x, y): "x,y", zip(np.linspace(-2 * np.pi, 0, 3), np.linspace(-2 * np.pi, 0, 3)), ) + @pytest.mark.xfail(reason="This test will not work in tape mode") def test_evaluate_circuit_hash_symbolic_assigned_arguments_do_not_matter(self, a, b, x, y): """Tests that the circuit hashes of identical circuits where different values are assigned to symbolic parameters are equal""" dev = qml.device("default.qubit", wires=2) @@ -276,7 +277,7 @@ def circuit2(x, y): node2 = qml.QNode(circuit2, dev) node2(x, y) - circuit_hash_2 = dev.circuit_hash + circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 == circuit_hash_2 @@ -364,7 +365,7 @@ def circuit1(): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([], {}) + node1.construct([], {}) circuit_hash_1 = node1.qtape.graph.hash c = 0.6 @@ -376,7 +377,7 @@ def circuit2(): return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([], {}) + node2.construct([], {}) circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 @@ -392,7 +393,7 @@ def circuit1(): return qml.expval(qml.PauliZ(0)) node1 = qml.QNode(circuit1, dev) - node1.evaluate([], {}) + node1.construct([], {}) circuit_hash_1 = node1.qtape.graph.hash def circuit2(): @@ -400,7 +401,7 @@ def circuit2(): return qml.expval(qml.PauliZ(0)) node2 = qml.QNode(circuit2, dev) - node2.evaluate([], {}) + node2.construct([], {}) circuit_hash_2 = node2.qtape.graph.hash assert circuit_hash_1 != circuit_hash_2 diff --git a/tests/interfaces/test_tape_torch.py b/tests/interfaces/test_tape_torch.py index 3867053ad0d..baf2f1b2f11 100644 --- a/tests/interfaces/test_tape_torch.py +++ b/tests/interfaces/test_tape_torch.py @@ -451,5 +451,5 @@ def test_complex_min_version(self, monkeypatch): with monkeypatch.context() as m: m.setattr(qml.interfaces.torch, "COMPLEX_SUPPORT", False) - with pytest.raises(qml.QuantumFunctionError, match="Version 1.6.0 or above of PyTorch"): + with pytest.raises(qml.QuantumFunctionError, match=r"Version 1\.6\.0 or above of PyTorch"): TorchInterface.apply(JacobianTape(), dtype=torch.complex128) diff --git a/tests/templates/test_templ_utils.py b/tests/templates/test_templ_utils.py index eb9429271dc..871b21f78c7 100644 --- a/tests/templates/test_templ_utils.py +++ b/tests/templates/test_templ_utils.py @@ -99,12 +99,10 @@ TYPE_PASS = [(["a"], list, type(None)), (1, int, type(None)), ("a", int, str), - (list, str, int) ] TYPE_FAIL = [("a", list, type(None)), (type(None), int, list), - (1., "a", type(None)) ] diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py index ab0ce23fb50..0fd622c915f 100644 --- a/tests/test_qubit_device.py +++ b/tests/test_qubit_device.py @@ -276,7 +276,7 @@ def test_unsupported_observable_return_type_raise_error( with monkeypatch.context() as m: m.setattr(QubitDevice, "apply", lambda self, x, **kwargs: None) with pytest.raises( - QuantumFunctionError, match="Unsupported return type specified for observable" + qml.QuantumFunctionError, match="Unsupported return type specified for observable" ): dev = mock_qubit_device_with_paulis_and_methods() dev.execute(tape) @@ -323,7 +323,7 @@ def test_results_no_state(self, mock_qubit_device_extract_stats, monkeypatch): with monkeypatch.context(): dev = mock_qubit_device_extract_stats() delattr(dev.__class__, "state") - with pytest.raises(QuantumFunctionError, match="The state is not available in the current"): + with pytest.raises(qml.QuantumFunctionError, match="The state is not available in the current"): dev.statistics([state()]) @pytest.mark.parametrize("returntype", [None]) @@ -358,7 +358,7 @@ class SomeObservable(qml.operation.Observable): obs = SomeObservable(wires=0) - with pytest.raises(QuantumFunctionError, match="Unsupported return type"): + with pytest.raises(qml.QuantumFunctionError, match="Unsupported return type"): dev = mock_qubit_device_extract_stats() dev.statistics([obs]) From f014c89a2c2ff4ecdc82504d40489e027dcd0fc6 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Mon, 8 Feb 2021 20:17:44 +0800 Subject: [PATCH 08/64] update jax test --- tests/devices/test_default_qubit_jax.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/devices/test_default_qubit_jax.py b/tests/devices/test_default_qubit_jax.py index 9d042d3c447..0d98359a0bf 100644 --- a/tests/devices/test_default_qubit_jax.py +++ b/tests/devices/test_default_qubit_jax.py @@ -254,15 +254,14 @@ def test_state_differentiability(self, tol): @qml.qnode(dev, diff_method="backprop", interface="jax") def circuit(a): qml.RY(a, wires=0) - return qml.expval(qml.PauliZ(0)) + return qml.state() a = jnp.array(0.54) def cost(a): """A function of the device quantum state, as a function of ijnput QNode parameters.""" - circuit(a) - res = jnp.abs(dev.state) ** 2 + res = jnp.abs(circuit(a)) ** 2 return res[1] - res[0] grad = jax.grad(cost)(a) @@ -311,7 +310,7 @@ def circuit(a, b): res = circuit(a, b) expected_cost = 0.5 * (jnp.cos(a) * jnp.cos(b) + jnp.cos(a) - jnp.cos(b) + 1) assert jnp.allclose(res, expected_cost, atol=tol, rtol=0) - res = jax.grad(lambda x, y: circuit(x, y).reshape(()), argnums=(0, 1))(a, b) + res = jax.grad(circuit, argnums=(0, 1))(a, b) expected_grad = jnp.array( [-0.5 * jnp.sin(a) * (jnp.cos(b) + 1), 0.5 * jnp.sin(b) * (1 - jnp.cos(a))] ) @@ -394,7 +393,7 @@ def circuit(weights): weights = jnp.array(qml.init.strong_ent_layers_normal(n_wires=2, n_layers=2)) - grad = jax.grad(lambda a: circuit(a).reshape(()))(weights) + grad = jax.grad(circuit)(weights) assert grad.shape == weights.shape def test_qnode_collection_integration(self): From 14ebf350b046b88c0af72c204b146813c7b681c1 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Mon, 22 Feb 2021 19:47:54 +0800 Subject: [PATCH 09/64] tests passing --- pennylane/qnode.py | 2 +- pennylane/tape/interfaces/jax.py | 139 ------------------------ tests/tape/interfaces/test_qnode_jax.py | 3 +- tests/tape/interfaces/test_tape_jax.py | 2 +- tests/test_quantum_gradients.py | 2 +- 5 files changed, 5 insertions(+), 143 deletions(-) delete mode 100644 pennylane/tape/interfaces/jax.py diff --git a/pennylane/qnode.py b/pennylane/qnode.py index 614c79b7877..f8e546df40e 100644 --- a/pennylane/qnode.py +++ b/pennylane/qnode.py @@ -803,7 +803,7 @@ def to_jax(self): """ # pylint: disable=import-outside-toplevel try: - from pennylane.tape.interfaces.jax import JAXInterface + from pennylane.interfaces.jax import JAXInterface if self.interface != "jax" and self.interface is not None: # Since the interface is changing, need to re-validate the tape class. diff --git a/pennylane/tape/interfaces/jax.py b/pennylane/tape/interfaces/jax.py deleted file mode 100644 index 061f381efc3..00000000000 --- a/pennylane/tape/interfaces/jax.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the mixin interface class for creating differentiable quantum tapes with -JAX. -""" -from functools import partial -import jax -import jax.experimental.host_callback as host_callback -import jax.numpy as jnp -from pennylane.tape.queuing import AnnotatedQueue -from pennylane.operation import Variance, Expectation - - -class JAXInterface(AnnotatedQueue): - """Mixin class for applying an JAX interface to a :class:`~.JacobianTape`. - - JAX-compatible quantum tape classes can be created via subclassing: - - .. code-block:: python - - class MyJAXQuantumTape(JAXInterface, JacobianTape): - - Alternatively, the JAX interface can be dynamically applied to existing - quantum tapes via the :meth:`~.apply` class method. This modifies the - tape **in place**. - - Once created, the JAX interface can be used to perform quantum-classical - differentiable programming. - - .. note:: - - If using a device that supports native JAX computation and backpropagation, such as - :class:`~.DefaultQubitJAX`, the JAX interface **does not need to be applied**. It - is only applied to tapes executed on non-JAX compatible devices. - - **Example** - - Once a JAX quantum tape has been created, it can be differentiated using JAX: - - .. code-block:: python - - tape = JAXInterface.apply(JacobianTape()) - - with tape: - qml.Rot(0, 0, 0, wires=0) - expval(qml.PauliX(0)) - - def cost_fn(x, y, z, device): - tape.set_parameters([x, y ** 2, y * np.sin(z)], trainable_only=False) - return tape.execute(device=device) - - >>> x = jnp.array(0.1, requires_grad=False) - >>> y = jnp.array(0.2, requires_grad=True) - >>> z = jnp.array(0.3, requires_grad=True) - >>> dev = qml.device("default.qubit", wires=2) - >>> cost_fn(x, y, z, device=dev) - DeviceArray([ 0.03991951], dtype=float32) - >>> jac_fn = jax.vjp(cost_fn) - >>> jac_fn(x, y, z, device=dev) - DeviceArray([[ 0.39828408, -0.00045133]], dtype=float32) - """ - - # pylint: disable=attribute-defined-outside-init - dtype = jnp.float64 - - @property - def interface(self): # pylint: disable=missing-function-docstring - return "jax" - - def _execute(self, params, device): - # TODO (chase): Add support for more than 1 measured observable. - if len(self.observables) != 1: - raise ValueError( - "The JAX interface currently only supports quantum nodes with a single return type." - ) - return_type = self.observables[0].return_type - if return_type is not Variance and return_type is not Expectation: - raise ValueError( - f"Only Variance and Expectation returns are support for the JAX interface, given {return_type}." - ) - - @jax.custom_vjp - def wrapped_exec(params): - exec_fn = partial(self.execute_device, device=device) - return host_callback.call( - exec_fn, params, result_shape=jax.ShapeDtypeStruct((1,), JAXInterface.dtype) - ) - - def wrapped_exec_fwd(params): - return wrapped_exec(params), params - - def wrapped_exec_bwd(params, g): - def jacobian(params): - tape = self.copy() - tape.set_parameters(params) - return tape.jacobian(device, params=params, **tape.jacobian_options) - - val = g.reshape((-1,)) * host_callback.call( - jacobian, - params, - result_shape=jax.ShapeDtypeStruct((1, len(params)), JAXInterface.dtype), - ) - return (list(val.reshape((-1,))),) # Comma is on purpose. - - wrapped_exec.defvjp(wrapped_exec_fwd, wrapped_exec_bwd) - return wrapped_exec(params) - - @classmethod - def apply(cls, tape): - """Apply the JAX interface to an existing tape in-place. - - Args: - tape (.JacobianTape): a quantum tape to apply the JAX interface to - - **Example** - - >>> with JacobianTape() as tape: - ... qml.RX(0.5, wires=0) - ... expval(qml.PauliZ(0)) - >>> JAXInterface.apply(tape) - >>> tape - , params=1> - """ - tape_class = getattr(tape, "__bare__", tape.__class__) - tape.__bare__ = tape_class - tape.__class__ = type("JAXQuantumTape", (cls, tape_class), {}) - return tape diff --git a/tests/tape/interfaces/test_qnode_jax.py b/tests/tape/interfaces/test_qnode_jax.py index 79069073bbd..8728fa7583f 100644 --- a/tests/tape/interfaces/test_qnode_jax.py +++ b/tests/tape/interfaces/test_qnode_jax.py @@ -17,7 +17,8 @@ jnp = pytest.importorskip("jax.numpy") import numpy as np import pennylane as qml -from pennylane.tape import JacobianTape, qnode, QNode, QubitParamShiftTape +from pennylane import qnode, QNode +from pennylane.tape import JacobianTape, QubitParamShiftTape def test_qnode_intergration(): """Test a simple use of qnode with a JAX interface and non-JAX device""" diff --git a/tests/tape/interfaces/test_tape_jax.py b/tests/tape/interfaces/test_tape_jax.py index 234a0f3c5e8..9e195a2c6ad 100644 --- a/tests/tape/interfaces/test_tape_jax.py +++ b/tests/tape/interfaces/test_tape_jax.py @@ -19,7 +19,7 @@ from functools import partial import pennylane as qml from pennylane.tape import JacobianTape -from pennylane.tape.interfaces.jax import JAXInterface +from pennylane.interfaces.jax import JAXInterface class TestJAXQuantumTape: diff --git a/tests/test_quantum_gradients.py b/tests/test_quantum_gradients.py index 94a7b8db6ac..50eed7ebc40 100644 --- a/tests/test_quantum_gradients.py +++ b/tests/test_quantum_gradients.py @@ -240,7 +240,7 @@ def test_cv_gradients_parameters_inside_array(self, gaussian_dev, tol): def qf(x, y): qml.Displacement(0.5, 0, wires=[0]) qml.Squeezing(x, 0, wires=[0]) - M = np.zeros((5, 5), dtype=object) + M = np.zeros((5, 5)) M[1,1] = y M[1,2] = 1.0 M[2,1] = 1.0 From 643113f47549112701aa54335a0e1924d9abe2f5 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Mon, 22 Feb 2021 20:01:18 +0800 Subject: [PATCH 10/64] fix --- pennylane/circuit_graph.py | 1 - pennylane/interfaces/jax.py | 139 ++++++++++++++++++ pennylane/qnn/keras.py | 2 - pennylane/qnn/torch.py | 3 - pennylane/qnode.py | 2 +- pennylane/tape/operation_recorder.py | 95 ++++++++++++ pennylane/tape/tape.py | 1 - pennylane/templates/broadcast.py | 1 - pennylane/templates/embeddings/amplitude.py | 1 - pennylane/templates/embeddings/angle.py | 4 - pennylane/templates/embeddings/basis.py | 2 - .../templates/embeddings/displacement.py | 5 - pennylane/templates/embeddings/iqp.py | 10 +- pennylane/templates/embeddings/qaoa.py | 5 - pennylane/templates/embeddings/squeezing.py | 5 - pennylane/templates/layers/basic_entangler.py | 5 - pennylane/templates/layers/cv_neural_net.py | 1 - .../layers/particle_conserving_u1.py | 4 - .../layers/particle_conserving_u2.py | 4 - pennylane/templates/layers/random.py | 5 - .../templates/layers/simplified_two_design.py | 5 - .../templates/layers/strongly_entangling.py | 6 - .../arbitrary_state_preparation.py | 1 - .../templates/state_preparations/basis.py | 1 - .../templates/state_preparations/mottonen.py | 3 - .../subroutines/arbitrary_unitary.py | 1 - .../subroutines/double_excitation_unitary.py | 4 - .../templates/subroutines/interferometer.py | 1 - .../subroutines/single_excitation_unitary.py | 4 - pennylane/templates/subroutines/uccsd.py | 4 - pennylane/transforms/classical_jacobian.py | 2 +- 31 files changed, 238 insertions(+), 89 deletions(-) create mode 100644 pennylane/interfaces/jax.py create mode 100644 pennylane/tape/operation_recorder.py diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 65b6f94bac6..b59773853ef 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -193,7 +193,6 @@ def serialize(self): """ serialization_string = "" delimiter = "!" - variable_delimiter = "V" for op in self.operations_in_order: serialization_string += op.name diff --git a/pennylane/interfaces/jax.py b/pennylane/interfaces/jax.py new file mode 100644 index 00000000000..0cb56db370d --- /dev/null +++ b/pennylane/interfaces/jax.py @@ -0,0 +1,139 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains the mixin interface class for creating differentiable quantum tapes with +JAX. +""" +from functools import partial +import jax +import jax.experimental.host_callback as host_callback +import jax.numpy as jnp +from pennylane.queuing import AnnotatedQueue +from pennylane.operation import Variance, Expectation + + +class JAXInterface(AnnotatedQueue): + """Mixin class for applying an JAX interface to a :class:`~.JacobianTape`. + + JAX-compatible quantum tape classes can be created via subclassing: + + .. code-block:: python + + class MyJAXQuantumTape(JAXInterface, JacobianTape): + + Alternatively, the JAX interface can be dynamically applied to existing + quantum tapes via the :meth:`~.apply` class method. This modifies the + tape **in place**. + + Once created, the JAX interface can be used to perform quantum-classical + differentiable programming. + + .. note:: + + If using a device that supports native JAX computation and backpropagation, such as + :class:`~.DefaultQubitJAX`, the JAX interface **does not need to be applied**. It + is only applied to tapes executed on non-JAX compatible devices. + + **Example** + + Once a JAX quantum tape has been created, it can be differentiated using JAX: + + .. code-block:: python + + tape = JAXInterface.apply(JacobianTape()) + + with tape: + qml.Rot(0, 0, 0, wires=0) + expval(qml.PauliX(0)) + + def cost_fn(x, y, z, device): + tape.set_parameters([x, y ** 2, y * np.sin(z)], trainable_only=False) + return tape.execute(device=device) + + >>> x = jnp.array(0.1, requires_grad=False) + >>> y = jnp.array(0.2, requires_grad=True) + >>> z = jnp.array(0.3, requires_grad=True) + >>> dev = qml.device("default.qubit", wires=2) + >>> cost_fn(x, y, z, device=dev) + DeviceArray([ 0.03991951], dtype=float32) + >>> jac_fn = jax.vjp(cost_fn) + >>> jac_fn(x, y, z, device=dev) + DeviceArray([[ 0.39828408, -0.00045133]], dtype=float32) + """ + + # pylint: disable=attribute-defined-outside-init + dtype = jnp.float64 + + @property + def interface(self): # pylint: disable=missing-function-docstring + return "jax" + + def _execute(self, params, device): + # TODO (chase): Add support for more than 1 measured observable. + if len(self.observables) != 1: + raise ValueError( + "The JAX interface currently only supports quantum nodes with a single return type." + ) + return_type = self.observables[0].return_type + if return_type is not Variance and return_type is not Expectation: + raise ValueError( + f"Only Variance and Expectation returns are support for the JAX interface, given {return_type}." + ) + + @jax.custom_vjp + def wrapped_exec(params): + exec_fn = partial(self.execute_device, device=device) + return host_callback.call( + exec_fn, params, result_shape=jax.ShapeDtypeStruct((1,), JAXInterface.dtype) + ) + + def wrapped_exec_fwd(params): + return wrapped_exec(params), params + + def wrapped_exec_bwd(params, g): + def jacobian(params): + tape = self.copy() + tape.set_parameters(params) + return tape.jacobian(device, params=params, **tape.jacobian_options) + + val = g.reshape((-1,)) * host_callback.call( + jacobian, + params, + result_shape=jax.ShapeDtypeStruct((1, len(params)), JAXInterface.dtype), + ) + return (list(val.reshape((-1,))),) # Comma is on purpose. + + wrapped_exec.defvjp(wrapped_exec_fwd, wrapped_exec_bwd) + return wrapped_exec(params) + + @classmethod + def apply(cls, tape): + """Apply the JAX interface to an existing tape in-place. + + Args: + tape (.JacobianTape): a quantum tape to apply the JAX interface to + + **Example** + + >>> with JacobianTape() as tape: + ... qml.RX(0.5, wires=0) + ... expval(qml.PauliZ(0)) + >>> JAXInterface.apply(tape) + >>> tape + , params=1> + """ + tape_class = getattr(tape, "__bare__", tape.__class__) + tape.__bare__ = tape_class + tape.__class__ = type("JAXQuantumTape", (cls, tape_class), {}) + return tape diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index db5372c7ac0..26def5d35af 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -13,11 +13,9 @@ # limitations under the License. """This module contains the classes and functions for integrating QNodes with the Keras Layer API.""" -import functools import inspect from collections.abc import Iterable from typing import Optional -import pennylane as qml try: import tensorflow as tf diff --git a/pennylane/qnn/torch.py b/pennylane/qnn/torch.py index 6760c169896..ba3f43cf0c0 100644 --- a/pennylane/qnn/torch.py +++ b/pennylane/qnn/torch.py @@ -33,9 +33,6 @@ TORCH_IMPORTED = False -import pennylane as qml - - class TorchLayer(Module): r"""Converts a :func:`~.QNode` to a Torch layer. diff --git a/pennylane/qnode.py b/pennylane/qnode.py index f8e546df40e..49b6aacc31f 100644 --- a/pennylane/qnode.py +++ b/pennylane/qnode.py @@ -16,7 +16,7 @@ """ # pylint: disable=import-outside-toplevel from collections.abc import Sequence -from functools import lru_cache, update_wrapper, wraps +from functools import lru_cache, update_wrapper import warnings import inspect diff --git a/pennylane/tape/operation_recorder.py b/pennylane/tape/operation_recorder.py new file mode 100644 index 00000000000..fb52af0ec74 --- /dev/null +++ b/pennylane/tape/operation_recorder.py @@ -0,0 +1,95 @@ +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains the :class:`OperationRecorder`. +""" +from pennylane.queuing import QueuingContext + +from .tape import QuantumTape + + +class OperationRecorder(QuantumTape): + """A template and quantum function inspector, + allowing easy introspection of operators that have been + applied without requiring a QNode. + + **Example**: + + The OperationRecorder is a context manager. Executing templates + or quantum functions stores applied operators in the + recorder, which can then be printed. + + >>> weights = qml.init.strong_ent_layers_normal(n_layers=1, n_wires=2) + >>> + >>> with OperationRecorder() as rec: + >>> qml.templates.layers.StronglyEntanglingLayers(*weights, wires=[0, 1]) + >>> + >>> print(rec) + Operations + ========== + Rot(-0.10832656163640327, 0.14429091013664083, -0.010835826725765343, wires=[0]) + Rot(-0.11254523669444501, 0.0947222564914006, -0.09139600968423377, wires=[1]) + CNOT(wires=[0, 1]) + CNOT(wires=[1, 0]) + + Alternatively, the :attr:`~.OperationRecorder.queue` attribute can be used + to directly access the applied :class:`~.Operation` and :class:`~.Observable` + objects. + + Attributes: + queue (List[Operator]): list of operators applied within + the OperatorRecorder context, includes operations and observables + operations (List[Operation]): list of operations applied within + the OperatorRecorder context + observables (List[Observable]): list of observables applied within + the OperatorRecorder context + """ + + def __init__(self): + super().__init__() + self.ops = None + self.obs = None + + def _process_queue(self): + super()._process_queue() + + for obj, info in self._queue.items(): + QueuingContext.append(obj, **info) + + # remove the operation recorder from the queuing + # context + QueuingContext.remove(self) + + new_tape = self.expand(depth=5, stop_at=lambda obj: not isinstance(obj, QuantumTape)) + self.ops = new_tape.operations + self.obs = new_tape.observables + + def __str__(self): + output = "" + output += "Operations\n" + output += "==========\n" + for op in self.ops: + output += repr(op) + "\n" + + output += "\n" + output += "Observables\n" + output += "==========\n" + for op in self.obs: + output += repr(op) + "\n" + + return output + + @property + def queue(self): + return self.ops + self.obs diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py index 9c0dde0bb9b..9b9028bcaca 100644 --- a/pennylane/tape/tape.py +++ b/pennylane/tape/tape.py @@ -22,7 +22,6 @@ import numpy as np import pennylane as qml -import pennylane.grouping from pennylane.queuing import AnnotatedQueue, QueuingContext from pennylane.operation import Sample diff --git a/pennylane/templates/broadcast.py b/pennylane/templates/broadcast.py index 0a6207d9636..2518e3b90bd 100644 --- a/pennylane/templates/broadcast.py +++ b/pennylane/templates/broadcast.py @@ -22,7 +22,6 @@ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access import pennylane as qml from pennylane.templates.decorator import template -from pennylane.templates.utils import get_shape from pennylane.wires import Wires OPTIONS = {"single", "double", "double_odd", "chain", "ring", "pyramid", "all_to_all", "custom"} diff --git a/pennylane/templates/embeddings/amplitude.py b/pennylane/templates/embeddings/amplitude.py index c369511a255..208b6b4e595 100644 --- a/pennylane/templates/embeddings/amplitude.py +++ b/pennylane/templates/embeddings/amplitude.py @@ -22,7 +22,6 @@ from pennylane.templates.decorator import template from pennylane.ops import QubitStateVector from pennylane.wires import Wires -from pennylane.templates.utils import check_shape, get_shape, check_type # tolerance for normalization TOLERANCE = 1e-10 diff --git a/pennylane/templates/embeddings/angle.py b/pennylane/templates/embeddings/angle.py index cc4fa7345f9..fc84311e2ce 100644 --- a/pennylane/templates/embeddings/angle.py +++ b/pennylane/templates/embeddings/angle.py @@ -18,10 +18,6 @@ import pennylane as qml from pennylane.templates.decorator import template from pennylane.templates import broadcast -from pennylane.templates.utils import ( - check_shape, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/embeddings/basis.py b/pennylane/templates/embeddings/basis.py index 67325cd91c5..600589f38fa 100644 --- a/pennylane/templates/embeddings/basis.py +++ b/pennylane/templates/embeddings/basis.py @@ -15,10 +15,8 @@ Contains the ``BasisEmbedding`` template. """ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access -from collections.abc import Iterable import pennylane as qml from pennylane.templates.decorator import template -from pennylane.templates.utils import check_shape, get_shape, check_type from pennylane.wires import Wires diff --git a/pennylane/templates/embeddings/displacement.py b/pennylane/templates/embeddings/displacement.py index 4f7e0ff1cbf..1fbcad394be 100644 --- a/pennylane/templates/embeddings/displacement.py +++ b/pennylane/templates/embeddings/displacement.py @@ -19,11 +19,6 @@ from pennylane.templates.decorator import template from pennylane.templates import broadcast from pennylane.wires import Wires -from pennylane.templates.utils import ( - check_shape, - check_is_in_options, - get_shape, -) def _preprocess(features, wires, method, c): diff --git a/pennylane/templates/embeddings/iqp.py b/pennylane/templates/embeddings/iqp.py index c04e6324c3c..17838f77487 100644 --- a/pennylane/templates/embeddings/iqp.py +++ b/pennylane/templates/embeddings/iqp.py @@ -22,15 +22,10 @@ from pennylane.templates.decorator import template from pennylane.ops import RZ, MultiRZ, Hadamard from pennylane.templates import broadcast -from pennylane.templates.utils import ( - check_shape, - check_type, - get_shape, -) from pennylane.wires import Wires -def _preprocess(features, wires, pattern, n_repeats): +def _preprocess(features, wires, pattern): """Validate and pre-process inputs as follows: * Check that the features tensor is one-dimensional. @@ -42,7 +37,6 @@ def _preprocess(features, wires, pattern, n_repeats): features (tensor_like): input features to pre-process wires (Wires): wires that template acts on pattern (list[int]): specifies the wires and features of the entanglers - n_repeats (int): number of times the basic embedding is repeated Returns: list[Wires]: preprocessed pattern @@ -228,7 +222,7 @@ def circuit(features=None, pattern=None): """ wires = Wires(wires) - pattern = _preprocess(features, wires, pattern, n_repeats) + pattern = _preprocess(features, wires, pattern) for i in range(n_repeats): diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index 3ac285cb288..0ac1a7d6505 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -19,11 +19,6 @@ from pennylane.templates.decorator import template from pennylane.ops import RX, RY, RZ, MultiRZ, Hadamard from pennylane.templates import broadcast -from pennylane.templates.utils import ( - check_shape, - check_number_of_layers, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/embeddings/squeezing.py b/pennylane/templates/embeddings/squeezing.py index 397fca6d637..d747dcbc6e3 100644 --- a/pennylane/templates/embeddings/squeezing.py +++ b/pennylane/templates/embeddings/squeezing.py @@ -18,11 +18,6 @@ import pennylane as qml from pennylane.templates.decorator import template from pennylane.templates import broadcast -from pennylane.templates.utils import ( - check_shape, - check_is_in_options, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index f697aa7f875..6ebf1f8f8bd 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -19,11 +19,6 @@ from pennylane.templates.decorator import template from pennylane.ops import CNOT, RX from pennylane.templates import broadcast -from pennylane.templates.utils import ( - check_shape, - check_number_of_layers, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/layers/cv_neural_net.py b/pennylane/templates/layers/cv_neural_net.py index 6f580cc20f9..2fef6b8e684 100644 --- a/pennylane/templates/layers/cv_neural_net.py +++ b/pennylane/templates/layers/cv_neural_net.py @@ -20,7 +20,6 @@ from pennylane.ops import Squeezing, Displacement, Kerr from pennylane.templates.subroutines import Interferometer from pennylane.templates import broadcast -from pennylane.templates.utils import check_number_of_layers, check_shapes from pennylane.wires import Wires diff --git a/pennylane/templates/layers/particle_conserving_u1.py b/pennylane/templates/layers/particle_conserving_u1.py index 3eda4f73c50..65defb1cdb7 100644 --- a/pennylane/templates/layers/particle_conserving_u1.py +++ b/pennylane/templates/layers/particle_conserving_u1.py @@ -21,10 +21,6 @@ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access from pennylane.templates.decorator import template from pennylane.ops import CNOT, CRot, PhaseShift, CZ -from pennylane.templates.utils import ( - check_shape, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/layers/particle_conserving_u2.py b/pennylane/templates/layers/particle_conserving_u2.py index 99cf1fbd963..3cf17b7656b 100644 --- a/pennylane/templates/layers/particle_conserving_u2.py +++ b/pennylane/templates/layers/particle_conserving_u2.py @@ -19,10 +19,6 @@ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access from pennylane.templates.decorator import template from pennylane.ops import CNOT, CRX, RZ -from pennylane.templates.utils import ( - check_shape, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/layers/random.py b/pennylane/templates/layers/random.py index 2a523e17ceb..e168386f0bf 100644 --- a/pennylane/templates/layers/random.py +++ b/pennylane/templates/layers/random.py @@ -19,11 +19,6 @@ import pennylane as qml from pennylane.templates.decorator import template from pennylane.ops import CNOT, RX, RY, RZ -from pennylane.templates.utils import ( - check_shape, - check_number_of_layers, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/layers/simplified_two_design.py b/pennylane/templates/layers/simplified_two_design.py index ba73c1be178..559431b63c9 100644 --- a/pennylane/templates/layers/simplified_two_design.py +++ b/pennylane/templates/layers/simplified_two_design.py @@ -19,11 +19,6 @@ from pennylane.templates.decorator import template from pennylane.ops import CZ, RY from pennylane.templates import broadcast -from pennylane.templates.utils import ( - check_shape, - check_number_of_layers, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/layers/strongly_entangling.py b/pennylane/templates/layers/strongly_entangling.py index 6fdf0775757..1c65dab1de6 100644 --- a/pennylane/templates/layers/strongly_entangling.py +++ b/pennylane/templates/layers/strongly_entangling.py @@ -19,12 +19,6 @@ from pennylane.templates.decorator import template from pennylane.ops import CNOT, Rot from pennylane.templates import broadcast -from pennylane.templates.utils import ( - check_shape, - check_type, - check_number_of_layers, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/state_preparations/arbitrary_state_preparation.py b/pennylane/templates/state_preparations/arbitrary_state_preparation.py index 01f223dee2e..0a06b21fac9 100644 --- a/pennylane/templates/state_preparations/arbitrary_state_preparation.py +++ b/pennylane/templates/state_preparations/arbitrary_state_preparation.py @@ -17,7 +17,6 @@ import functools import pennylane as qml from pennylane.templates.decorator import template -from pennylane.templates.utils import check_shape, get_shape from pennylane.wires import Wires diff --git a/pennylane/templates/state_preparations/basis.py b/pennylane/templates/state_preparations/basis.py index 1867c5f1c91..8f03b9b9d2a 100644 --- a/pennylane/templates/state_preparations/basis.py +++ b/pennylane/templates/state_preparations/basis.py @@ -17,7 +17,6 @@ import pennylane as qml from pennylane.templates.decorator import template -from pennylane.templates.utils import check_shape, get_shape from pennylane.wires import Wires diff --git a/pennylane/templates/state_preparations/mottonen.py b/pennylane/templates/state_preparations/mottonen.py index df81cc5b164..d61aee72b52 100644 --- a/pennylane/templates/state_preparations/mottonen.py +++ b/pennylane/templates/state_preparations/mottonen.py @@ -19,7 +19,6 @@ import pennylane as qml from pennylane.templates.decorator import template -from pennylane.templates.utils import check_shape, get_shape from pennylane.wires import Wires @@ -37,8 +36,6 @@ def _preprocess(state_vector, wires): Returns: tensor_like, tensor_like, Wires: amplitudes a, phases omega and preprocessed wires """ - n_wires = len(wires) - shape = qml.math.shape(state_vector) if len(shape) != 1: diff --git a/pennylane/templates/subroutines/arbitrary_unitary.py b/pennylane/templates/subroutines/arbitrary_unitary.py index 0f4f942345d..1b1d63db52f 100644 --- a/pennylane/templates/subroutines/arbitrary_unitary.py +++ b/pennylane/templates/subroutines/arbitrary_unitary.py @@ -16,7 +16,6 @@ """ import pennylane as qml from pennylane.templates.decorator import template -from pennylane.templates.utils import check_shape, get_shape from pennylane.wires import Wires _PAULIS = ["I", "X", "Y", "Z"] diff --git a/pennylane/templates/subroutines/double_excitation_unitary.py b/pennylane/templates/subroutines/double_excitation_unitary.py index dadb27559fb..ce0b44403c9 100644 --- a/pennylane/templates/subroutines/double_excitation_unitary.py +++ b/pennylane/templates/subroutines/double_excitation_unitary.py @@ -20,10 +20,6 @@ import pennylane as qml from pennylane.ops import CNOT, RX, RZ, Hadamard from pennylane.templates.decorator import template -from pennylane.templates.utils import ( - check_shape, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/subroutines/interferometer.py b/pennylane/templates/subroutines/interferometer.py index ea90e566698..d5e27bd8562 100644 --- a/pennylane/templates/subroutines/interferometer.py +++ b/pennylane/templates/subroutines/interferometer.py @@ -19,7 +19,6 @@ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access from pennylane.templates.decorator import template from pennylane.ops import Beamsplitter, Rotation -from pennylane.templates.utils import check_shapes, get_shape from pennylane.wires import Wires diff --git a/pennylane/templates/subroutines/single_excitation_unitary.py b/pennylane/templates/subroutines/single_excitation_unitary.py index 53e5a45fc72..d18ad4ef2cb 100644 --- a/pennylane/templates/subroutines/single_excitation_unitary.py +++ b/pennylane/templates/subroutines/single_excitation_unitary.py @@ -20,10 +20,6 @@ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access from pennylane.ops import CNOT, RX, RZ, Hadamard from pennylane.templates.decorator import template -from pennylane.templates.utils import ( - check_shape, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/templates/subroutines/uccsd.py b/pennylane/templates/subroutines/uccsd.py index f5b81767e68..2be7e1cbdfa 100644 --- a/pennylane/templates/subroutines/uccsd.py +++ b/pennylane/templates/subroutines/uccsd.py @@ -21,10 +21,6 @@ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access from pennylane.templates.decorator import template from pennylane.templates.subroutines import DoubleExcitationUnitary, SingleExcitationUnitary -from pennylane.templates.utils import ( - check_shape, - get_shape, -) from pennylane.wires import Wires diff --git a/pennylane/transforms/classical_jacobian.py b/pennylane/transforms/classical_jacobian.py index 4dddce43b56..20abcb7ea63 100644 --- a/pennylane/transforms/classical_jacobian.py +++ b/pennylane/transforms/classical_jacobian.py @@ -14,7 +14,7 @@ """ Contains the classical Jacobian transform """ -import numpy as np +# pylint: disable=import-outside-toplevel import pennylane as qml From 153401dabf3e6e9082965ccf5ddf4b145cfd924a Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Mon, 22 Feb 2021 20:03:25 +0800 Subject: [PATCH 11/64] fix --- pennylane/interfaces/torch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py index 6ef0e0212d7..84b996fe3a9 100644 --- a/pennylane/interfaces/torch.py +++ b/pennylane/interfaces/torch.py @@ -20,8 +20,7 @@ import semantic_version import torch -from pennylane import QuantumFunctionError - +import pennylane as qml from pennylane.queuing import AnnotatedQueue COMPLEX_SUPPORT = semantic_version.match(">=1.6.0", torch.__version__) @@ -219,7 +218,7 @@ def apply(cls, tape, dtype=torch.float64): , params=1> """ if (dtype is torch.complex64 or dtype is torch.complex128) and not COMPLEX_SUPPORT: - raise QuantumFunctionError( + raise qml.QuantumFunctionError( "Version 1.6.0 or above of PyTorch must be installed for complex support, " "which is required for quantum functions that return the state." ) From 951b9249685466b9a64f035f155db3fed94c6aa3 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Mon, 22 Feb 2021 20:14:01 +0800 Subject: [PATCH 12/64] linting --- pennylane/__init__.py | 58 ++++++++----------- pennylane/_qubit_device.py | 2 +- .../circuit_drawer/representation_resolver.py | 3 +- pennylane/circuit_graph.py | 2 +- pennylane/operation.py | 1 - pennylane/templates/embeddings/iqp.py | 1 - pennylane/transforms/measurement_grouping.py | 18 +++++- pennylane/transforms/metric_tensor.py | 2 +- pennylane/utils.py | 1 - tests/tape/interfaces/test_qnode_jax.py | 4 +- 10 files changed, 48 insertions(+), 44 deletions(-) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 2601ea47d4b..f72a54005b3 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -16,45 +16,37 @@ PennyLane can be directly imported. """ from importlib import reload -import pkg_resources import numpy as _np - -from semantic_version import Version, Spec - -# QueuingContext needs to be imported before all other pennylane imports -from .queuing import QueuingContext # pylint: disable=wrong-import-order -import pennylane.operation - -import pennylane.templates - - -from .circuit_graph import CircuitGraph -from .configuration import Configuration -from ._device import Device, DeviceError -from ._qubit_device import QubitDevice -from .measure import expval, var, sample, state, density_matrix, probs -from .ops import * -from .optimize import * -from .qnode import qnode, QNode -from .utils import inv -from ._version import __version__ -from .io import * -from ._grad import jacobian, grad - -import pennylane.math -import pennylane.tape +import pkg_resources +from semantic_version import Spec, Version import pennylane.init -import pennylane.qnn +import pennylane.math +import pennylane.operation import pennylane.qaoa as qaoa - -from pennylane.collections import apply, map, sum, dot, QNodeCollection -from pennylane.templates import template, broadcast, layer +import pennylane.qnn +import pennylane.templates +from pennylane._device import Device, DeviceError +from pennylane._grad import grad, jacobian +from pennylane._qubit_device import QubitDevice +from pennylane._version import __version__ from pennylane.about import about -from pennylane.vqe import Hamiltonian, ExpvalCost, VQECost -from pennylane.transforms import draw, metric_tensor, measurement_grouping - +from pennylane.circuit_graph import CircuitGraph +from pennylane.configuration import Configuration +from pennylane.io import * +from pennylane.measure import density_matrix, expval, probs, sample, state, var +from pennylane.ops import * +from pennylane.optimize import * +from pennylane.qnode import QNode, qnode +from pennylane.templates import broadcast, layer, template +from pennylane.transforms import draw, measurement_grouping, metric_tensor +from pennylane.utils import inv +from pennylane.vqe import ExpvalCost, Hamiltonian, VQECost + +# QueuingContext and collections needs to be imported after all other pennylane imports +from .collections import QNodeCollection, apply, dot, map, sum +from .queuing import QueuingContext # Look for an existing configuration file default_config = Configuration("config.toml") diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index 24b5b503e84..03182ae250b 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -186,7 +186,7 @@ def execute(self, circuit, **kwargs): # consider merging with caching case try: self._circuit_hash = circuit.graph.hash - except AttributeError as e: + except AttributeError: self._circuit_hash = circuit.hash if self._cache: diff --git a/pennylane/circuit_drawer/representation_resolver.py b/pennylane/circuit_drawer/representation_resolver.py index 2c8e6a5a318..0cc2c4641ed 100644 --- a/pennylane/circuit_drawer/representation_resolver.py +++ b/pennylane/circuit_drawer/representation_resolver.py @@ -102,7 +102,8 @@ def index_of_array_or_append(target_element, target_list): return len(target_list) - 1 - def single_parameter_representation(self, par): + @staticmethod + def single_parameter_representation(par): """Resolve the representation of an Operator's parameter. Args: diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index b59773853ef..176ea7e6fcb 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -15,7 +15,7 @@ This module contains the CircuitGraph class which is used to generate a DAG (directed acyclic graph) representation of a quantum circuit from an Operator queue. """ -# pylint: disable=too-many-branches +# pylint: disable=too-many-branches,too-many-arguments,too-many-instance-attributes from collections import Counter, OrderedDict, namedtuple import networkx as nx diff --git a/pennylane/operation.py b/pennylane/operation.py index e0277e4067e..a7f9ffe047d 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -118,7 +118,6 @@ import copy import itertools import functools -import numbers from enum import Enum, IntEnum import numpy as np diff --git a/pennylane/templates/embeddings/iqp.py b/pennylane/templates/embeddings/iqp.py index 17838f77487..11a335d18c6 100644 --- a/pennylane/templates/embeddings/iqp.py +++ b/pennylane/templates/embeddings/iqp.py @@ -15,7 +15,6 @@ Contains the ``IQPEmbedding`` template. """ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access -from collections.abc import Iterable from itertools import combinations import pennylane as qml diff --git a/pennylane/transforms/measurement_grouping.py b/pennylane/transforms/measurement_grouping.py index 1f52d03752b..8c08fbd644a 100644 --- a/pennylane/transforms/measurement_grouping.py +++ b/pennylane/transforms/measurement_grouping.py @@ -14,11 +14,27 @@ """ Contains the measurement grouping transform """ -import numpy as np import pennylane as qml def measurement_grouping(tape, obs_list, coeffs_list): + """Returns a list of measurement optimized tapes, and a classical processing function, for + evaluating the expectation value of a provided Hamiltonian. + + Args: + tape (.QuantumTape): input tape + obs_list (Sequence[.Observable]): The list of observables to measure + the expectation values of after executing the tape. + coeffs_list (Sequence[float]): Coefficients of the Hamiltonian expression. + Must be of the same length as ``obs_list``. + + Returns: + tuple[list[.QuantumTape], func]: Returns a tuple containing a list of + quantum tapes to be evaluated, and a function to be applied to these + tape results to compute the Hamiltonian expectation value. + + **Example** + """ obs_groupings, coeffs_groupings = qml.grouping.group_observables(obs_list, coeffs_list) tapes = [] diff --git a/pennylane/transforms/metric_tensor.py b/pennylane/transforms/metric_tensor.py index 4a0c1f25c6b..fea6f523c7b 100644 --- a/pennylane/transforms/metric_tensor.py +++ b/pennylane/transforms/metric_tensor.py @@ -32,7 +32,7 @@ def _stopping_critera(obj): def metric_tensor_tape(tape, diag_approx=False, wrt=None): """Returns a list of tapes, and a classical processing function, for computing the block - diagronal metric tensor approximation of an input tape on hardware. + diagonal metric tensor approximation of an input tape on hardware. Args: tape (.QuantumTape): the tape to compute the metric tensor of diff --git a/pennylane/utils.py b/pennylane/utils.py index 5cb0cfef5bc..57a7a2f0779 100644 --- a/pennylane/utils.py +++ b/pennylane/utils.py @@ -17,7 +17,6 @@ """ # pylint: disable=protected-access,too-many-branches from collections.abc import Iterable -import copy import functools import inspect import itertools diff --git a/tests/tape/interfaces/test_qnode_jax.py b/tests/tape/interfaces/test_qnode_jax.py index 8728fa7583f..cce7fdebc0c 100644 --- a/tests/tape/interfaces/test_qnode_jax.py +++ b/tests/tape/interfaces/test_qnode_jax.py @@ -162,10 +162,8 @@ def construct(self, args, kwargs): "dev_name,diff_method", [("default.mixed", "finite-diff"), ("default.qubit.autograd", "parameter-shift")], ) -def test_transform(dev_name, diff_method, monkeypatch, tol): +def test_transform(dev_name, diff_method, tol): """Test an example transform""" - monkeypatch.setattr(qml.operation.Operation, "do_check_domain", False) - dev = qml.device(dev_name, wires=1) @qnode(dev, interface="jax", diff_method=diff_method) From 0fdf4b45ae3ce49ea914383a007eafd4a54733cf Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Mon, 22 Feb 2021 20:28:17 +0800 Subject: [PATCH 13/64] fix docs --- doc/code/qml_operation.rst | 2 +- doc/code/qml_qnodes.rst | 16 --------- doc/code/qml_tape.rst | 8 +++++ doc/code/qml_utils.rst | 2 +- doc/code/qml_variable.rst | 14 -------- doc/development/guide/architecture.rst | 47 ++++++++++---------------- doc/index.rst | 2 -- 7 files changed, 28 insertions(+), 63 deletions(-) delete mode 100644 doc/code/qml_qnodes.rst delete mode 100644 doc/code/qml_variable.rst diff --git a/doc/code/qml_operation.rst b/doc/code/qml_operation.rst index 885b136f02b..f94112c6512 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: Enum, IntEnum, Variable, ClassPropertyDescriptor, multi_dot, pauli_eigs, Wires + :skip: Enum, IntEnum, ClassPropertyDescriptor, multi_dot, pauli_eigs, Wires diff --git a/doc/code/qml_qnodes.rst b/doc/code/qml_qnodes.rst deleted file mode 100644 index 3c26d96101a..00000000000 --- a/doc/code/qml_qnodes.rst +++ /dev/null @@ -1,16 +0,0 @@ -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:`~pennylane.qnode` decorator - and :func:`~pennylane.QNode` constructor. - -.. automodapi:: pennylane.qnodes - :no-heading: - :include-all-objects: - :skip: qnode, QNode, QuantumFunctionError diff --git a/doc/code/qml_tape.rst b/doc/code/qml_tape.rst index 68ca8039b97..a95a47e5715 100644 --- a/doc/code/qml_tape.rst +++ b/doc/code/qml_tape.rst @@ -11,6 +11,14 @@ additional gradient methods: Finally, quantum tapes are fully compatible with autodifferentiating via NumPy/Autograd, TensorFlow, and PyTorch. +.. 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:`~pennylane.qnode` decorator + and :func:`~pennylane.QNode` constructor. .. automodapi:: pennylane.tape :no-main-docstr: diff --git a/doc/code/qml_utils.rst b/doc/code/qml_utils.rst index 278939771b1..c5441e5953d 100644 --- a/doc/code/qml_utils.rst +++ b/doc/code/qml_utils.rst @@ -12,4 +12,4 @@ qml.utils :no-heading: :include-all-objects: :no-inheritance-diagram: - :skip: Iterable, Variable, matmul + :skip: Iterable, matmul diff --git a/doc/code/qml_variable.rst b/doc/code/qml_variable.rst deleted file mode 100644 index 1dd74b03792..00000000000 --- a/doc/code/qml_variable.rst +++ /dev/null @@ -1,14 +0,0 @@ -qml.variable -============ - -.. currentmodule:: pennylane.variable - -.. warning:: - - Unless you are a PennyLane or plugin developer, you likely do not need - to use the ``Variable`` class. - -.. automodapi:: pennylane.variable - :no-heading: - :include-all-objects: - :no-inheritance-diagram: diff --git a/doc/development/guide/architecture.rst b/doc/development/guide/architecture.rst index 971c5a5e2b8..81811eb5f59 100644 --- a/doc/development/guide/architecture.rst +++ b/doc/development/guide/architecture.rst @@ -76,28 +76,29 @@ A quantum node or QNode (represented by a subclass of :math:`f(x;\theta)=R^m\rightarrow R^n` that is executed using quantum information processing on a quantum device. -Apart from encapsulating quantum functions, QNodes also provide custom quantum -differentiation rules. Examples include the :doc:`parameter-shift rule -`, where the derivative of a -quantum function can be expressed by a linear combination of other quantum -functions. As these rules allow quantum gradients to be obtained from QNodes, -hybrid computations may include QNodes as part of training deep learning -models. - Users don't typically instantiate QNodes directly---instead, the :func:`~pennylane.qnode` decorator or :func:`~pennylane.QNode` constructor function automates the process of creating a QNode from a provided -quantum function and device. The constructor attempts to determine the ``"best"`` QNode -subclass/differentiation method for the provided device and interface. For more fine-grained control, +quantum function and device. The constructor attempts to determine the ``"best"`` +differentiation method for the provided device and interface. For more fine-grained control, the differentiation method can be specified directly via the ``diff_method`` option. -A common representation of quantum circuits is a `Directed -Acyclic Graph (DAG) -`__ -where quantum operations are nodes within the graph. Each ``QNode`` builds such -a DAG using a :class:`~.CircuitGraph` instance. +Tapes +***** + +Internally, QNodes store the details of the quantum processing using a datastructure called the +*tape*. Apart from encapsulating quantum processing, tapes also provide custom quantum +differentiation rules. Examples include the :doc:`parameter-shift rule `, +where the derivative of a tape can be expressed by a linear combination of other tapes plus +classical post-processing. As these rules allow quantum gradients to be obtained from tapes, hybrid +computations may include QNodes as part of training deep learning models. -For further details on QNodes, and a full list of QNodes with their custom -differentiation rules, refer to the :doc:`/code/qml_qnodes` module. +A common representation of quantum circuits is a `Directed Acyclic Graph (DAG) +`__ where quantum +operations are nodes within the graph. Each tape builds such a DAG using a :class:`~.CircuitGraph` +instance. + +For further details on tapes, and a full list of tapes with their custom +differentiation rules, refer to the :doc:`/code/qml_tape` module. Interfaces ********** @@ -179,15 +180,3 @@ the surrounding :class:`~.pennylane.QueuingContext`. Measurement functions such as :func:`~.pennylane.expval` are responsible for queuing observables. For further details, refer to the description in :class:`~.pennylane.QueuingContext`. - -Variables -********* - -Circuit parameters in PennyLane are tracked and updated using -:class:`~.Variable`. They play a key role in the evaluation of ``QNode`` gradients, as -the symbolic parameters are substituted with numeric values. The ``Variable`` class plays -an important role in book-keeping, allowing PennyLane to keep track of which parameters are -used in which operations, and automatically perform the product and chain rule where required. - -We refer to the :doc:`/code/qml_variable` page for a more in-depth description of how -``Variables`` are used during execution. diff --git a/doc/index.rst b/doc/index.rst index 76f8402d391..ab128073752 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -211,10 +211,8 @@ PennyLane is **free** and **open source**, released under the Apache License, Ve code/qml_qaoa code/qml_qchem code/qml_qnn - code/qml_qnodes code/qml_tape code/qml_templates code/qml_utils - code/qml_variable code/qml_wires code/qml_beta From 4443b5777b81cf5c89821403def109be9eb27bf4 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Fri, 5 Mar 2021 00:41:41 +0800 Subject: [PATCH 14/64] fix --- pennylane/qnn/keras.py | 2 +- tests/qnn/conftest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index a16d6dca282..2c944fa36df 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -290,7 +290,7 @@ def call(self, inputs): return self._evaluate_qnode(inputs) def _evaluate_qnode(self, x): - """Evaluates a tape-mode QNode for a single input datapoint. + """Evaluates a QNode for a single input datapoint. Args: x (tensor): the datapoint diff --git a/tests/qnn/conftest.py b/tests/qnn/conftest.py index 12e4159e3b2..7f95bdb2f30 100644 --- a/tests/qnn/conftest.py +++ b/tests/qnn/conftest.py @@ -54,7 +54,7 @@ def circuit(inputs, w1, w2, w3, w4, w5, w6, w7): @pytest.fixture -def get_circuit_dm(n_qubits, output_dim, interface, tape_mode): +def get_circuit_dm(n_qubits, output_dim, interface): """Fixture for getting a sample quantum circuit with a controllable qubit number and output dimension for density matrix return type. Returns both the circuit and the shape of the weights.""" From 672d9637f843eb424ba7127af8c5c61046a62c47 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Fri, 5 Mar 2021 01:15:33 +0800 Subject: [PATCH 15/64] fix --- tests/qnn/test_keras.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 3450c847803..b5ab2293858 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -560,10 +560,6 @@ def test_train_model_dm(self, model_dm, batch_size, n_qubits, output_dim): """Test if a model can train using the KerasLayer when QNode returns a density_matrix(). The model is composed of two KerasLayers sandwiched between Dense neural network layers, and the dataset is simply input and output vectors of zeros.""" - - if not qml.tape_mode_active(): - pytest.skip() - x = np.zeros((batch_size, n_qubits)) y = np.zeros((batch_size, output_dim[0] * output_dim[1])) @@ -575,10 +571,6 @@ def test_train_model_dm(self, model_dm, batch_size, n_qubits, output_dim): def test_model_gradients_dm(self, model_dm, output_dim, n_qubits): """Test if a gradient can be calculated with respect to all of the trainable variables in the model.""" - - if not qml.tape_mode_active(): - pytest.skip() - x = tf.zeros((2, n_qubits)) y = tf.zeros((2, output_dim[0] * output_dim[1])) @@ -594,9 +586,6 @@ def test_model_save_weights_dm(self, model_dm, n_qubits, tmpdir): """Test if the model_dm can be successfully saved and reloaded using the get_weights() method""" - if not qml.tape_mode_active(): - pytest.skip() - prediction = model_dm.predict(np.ones(n_qubits)) weights = model_dm.get_weights() file = str(tmpdir) + "/model" From c4f609a3fdb5cc07364431ebed6bb3524e1248a7 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Wed, 10 Mar 2021 21:01:38 +0800 Subject: [PATCH 16/64] fix --- tests/tape/test_tape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index 2b148180d42..fb87177192c 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -858,7 +858,7 @@ def circuit(): return sample(qml.PauliZ(0)) # Choosing parameter-shift not to swap the device under the hood - qnode = qml.tape.QNode(circuit, dev, diff_method="parameter-shift") + qnode = qml.QNode(circuit, dev, diff_method="parameter-shift") qnode() # Double-checking that the T gate is not supported From f8a27479070a90191a6a8535b155586ab2391f36 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Thu, 11 Mar 2021 00:17:39 +0800 Subject: [PATCH 17/64] Update pennylane/tape/operation_recorder.py Co-authored-by: Chase Roberts --- pennylane/tape/operation_recorder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/tape/operation_recorder.py b/pennylane/tape/operation_recorder.py index fb52af0ec74..480ddc85271 100644 --- a/pennylane/tape/operation_recorder.py +++ b/pennylane/tape/operation_recorder.py @@ -84,7 +84,7 @@ def __str__(self): output += "\n" output += "Observables\n" - output += "==========\n" + output += "===========\n" for op in self.obs: output += repr(op) + "\n" From 5e1df17a9959cc10f8d290fea04fc53ee4477322 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Thu, 11 Mar 2021 00:40:23 +0800 Subject: [PATCH 18/64] fix tests after changing observable underline --- tests/test_queuing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_queuing.py b/tests/test_queuing.py index c1499dc54ec..9c7e6775ac6 100644 --- a/tests/test_queuing.py +++ b/tests/test_queuing.py @@ -381,7 +381,7 @@ def test_circuit_integration(self): + "CNOT(wires=[0, 1])\n" + "\n" + "Observables\n" - + "==========\n" + + "===========\n" ) dev = qml.device("default.qubit", wires=2) @@ -420,7 +420,7 @@ def test_template_integration(self): + "RZ(12, wires=[0])\n" + "\n" + "Observables\n" - + "==========\n" + + "===========\n" ) def template(x): @@ -445,7 +445,7 @@ def test_template_with_return_integration(self): + "RZ(12, wires=[0])\n" + "\n" + "Observables\n" - + "==========\n" + + "===========\n" + "var(PauliZ(wires=[0]))\n" + "sample(PauliX(wires=[1]))\n" ) From 27d11b3d90c384625845566977b3cd0a8be53f95 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Thu, 11 Mar 2021 12:49:31 +0200 Subject: [PATCH 19/64] backup --- pennylane/templates/layers/basic_entangler.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 5980ed61fb6..5d09625a0f2 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -65,11 +65,8 @@ def _preprocess(weights, wires): msg=f"Weights tensor must have second dimension of length {len(wires)}; got {get_shape(weights)[1]}", ) - return repeat - -@template -def BasicEntanglerLayers(weights, wires, rotation=None): +class BasicEntanglerLayers: r"""Layers consisting of one-parameter single-qubit rotations on each qubit, followed by a closed chain or *ring* of CNOT gates. @@ -176,14 +173,20 @@ def circuit(weights): ``ValueError: Wrong number of parameters``. """ - if rotation is None: - rotation = RX + def __init__(self, weights, wires, rotation=None, do_queue=True): + + self.rotation = rotation or RX + _preprocess(weights, wires) + + super().__init__(weights, wires=wires, do_queue=do_queue) + + @staticmethod + def decomposition(weights, wires): - wires = Wires(wires) - repeat = _preprocess(weights, wires) + #wires = Wires(wires) - for layer in range(repeat): + for layer in range(repeat): - broadcast(unitary=rotation, pattern="single", wires=wires, parameters=weights[layer]) - broadcast(unitary=CNOT, pattern="ring", wires=wires) + broadcast(unitary=rotation, pattern="single", wires=wires, parameters=weights[layer]) + broadcast(unitary=CNOT, pattern="ring", wires=wires) From 9407fe7a35abbf7772ee2192d25f59f0f127863e Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Thu, 11 Mar 2021 15:01:00 +0200 Subject: [PATCH 20/64] prototype - all tests pass --- pennylane/templates/layers/basic_entangler.py | 25 +++++++++------ tests/templates/test_layer.py | 3 +- tests/templates/test_layers.py | 32 ++++++++++++------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 855fc90a70e..7e69d7d1192 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -16,7 +16,7 @@ """ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access import pennylane as qml -from pennylane.templates.decorator import template +from pennylane.operation import Operation, AnyWires from pennylane.ops import CNOT, RX from pennylane.templates import broadcast from pennylane.wires import Wires @@ -36,7 +36,6 @@ def _preprocess(weights, wires): int: number of times that the ansatz is repeated """ shape = qml.math.shape(weights) - repeat = shape[0] if len(shape) != 2: raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") @@ -47,7 +46,7 @@ def _preprocess(weights, wires): ) -class BasicEntanglerLayers: +class BasicEntanglerLayers(Operation): r"""Layers consisting of one-parameter single-qubit rotations on each qubit, followed by a closed chain or *ring* of CNOT gates. @@ -154,20 +153,26 @@ def circuit(weights): ``ValueError: Wrong number of parameters``. """ - def __init__(self, weights, wires, rotation=None, do_queue=True): + num_params = 1 + num_wires = AnyWires + par_domain = "A" + + def __init__(self, weights, wires=None, rotation=None, do_queue=True): - self.rotation = rotation or RX _preprocess(weights, wires) - super().__init__(weights, wires=wires, do_queue=do_queue) + self.rotation = rotation or RX - @staticmethod - def decomposition(weights, wires): + super().__init__(weights, wires=wires, do_queue=do_queue) + def decomposition(self, *params, wires): - #wires = Wires(wires) + weights = params[0] + # first dimension of the weights tensor determines + # the number of layers + repeat = qml.math.shape(weights)[0] for layer in range(repeat): - broadcast(unitary=rotation, pattern="single", wires=wires, parameters=weights[layer]) + broadcast(unitary=self.rotation, pattern="single", wires=wires, parameters=weights[layer]) broadcast(unitary=CNOT, pattern="ring", wires=wires) diff --git a/tests/templates/test_layer.py b/tests/templates/test_layer.py index 0ad41abdeeb..05d9ca19210 100644 --- a/tests/templates/test_layer.py +++ b/tests/templates/test_layer.py @@ -63,7 +63,8 @@ def MultiCircuit(parameters1, parameters2, var1, wires, var2): qml.RY(parameters1[i], wires=w) if var1 == True: - qml.templates.BasicEntanglerLayers([parameters2], wires=wires) + op = qml.templates.BasicEntanglerLayers([parameters2], wires=wires, do_queue=False) + op.decomposition([parameters2], wires=wires) UNITARIES = [ ConstantCircuit, diff --git a/tests/templates/test_layers.py b/tests/templates/test_layers.py index 881f4263cbd..a98838d9e26 100644 --- a/tests/templates/test_layers.py +++ b/tests/templates/test_layers.py @@ -33,6 +33,7 @@ from pennylane.wires import Wires from pennylane.numpy import tensor from pennylane.init import particle_conserving_u1_normal +from pennylane.tape import QuantumTape TOLERANCE = 1e-8 @@ -620,16 +621,19 @@ def test_circuit_queue(self, n_wires, n_cnots): weights = np.random.randn(n_layers, n_wires) - with qml.tape.OperationRecorder() as rec: - BasicEntanglerLayers(weights, wires=range(n_wires)) + op = BasicEntanglerLayers(weights, wires=range(n_wires)) + + with QuantumTape() as tape: + op.decomposition(weights, wires=range(n_wires)) # Test that gates appear in the right order exp_gates = [qml.RX] * n_wires + [qml.CNOT] * n_cnots exp_gates *= n_layers - res_gates = rec.queue + + res_gates = [type(op) for op in tape.operations] for op1, op2 in zip(res_gates, exp_gates): - assert isinstance(op1, op2) + assert op1 == op2 @pytest.mark.parametrize("n_wires, n_cnots", [(1, 0), (2, 1), (3, 3), (4, 4)]) def test_circuit_parameters(self, n_wires, n_cnots): @@ -639,13 +643,15 @@ def test_circuit_parameters(self, n_wires, n_cnots): weights = np.random.randn(n_layers, n_wires) - with qml.tape.OperationRecorder() as rec: - BasicEntanglerLayers(weights, wires=range(n_wires)) + op = BasicEntanglerLayers(weights, wires=range(n_wires)) + + with QuantumTape() as tape: + op.decomposition(weights, wires=range(n_wires)) # test the device parameters for l in range(n_layers): # only select the rotation gates - layer_ops = rec.queue[l * (n_wires + n_cnots) : l * (n_wires + n_cnots) + n_wires] + layer_ops = tape.operations[l * (n_wires + n_cnots) : l * (n_wires + n_cnots) + n_wires] # check each rotation gate parameter for n in range(n_wires): @@ -660,14 +666,16 @@ def test_custom_rotation(self, rotation): n_wires = 4 weights = np.ones(shape=(n_layers, n_wires)) - with qml.tape.OperationRecorder() as rec: - BasicEntanglerLayers(weights, wires=range(n_wires), rotation=rotation) + op = BasicEntanglerLayers(weights, wires=range(n_wires), rotation=rotation) + + with QuantumTape() as tape: + op.decomposition(weights, wires=range(n_wires)) # assert queue contains the custom rotations and CNOTs only - gates = rec.queue + gates = tape.operations for op in gates: - if not isinstance(op, CNOT): - assert isinstance(op, rotation) + if not type(op) == CNOT: + assert type(op) == rotation @pytest.mark.parametrize( "weights, n_wires, target", From a4cd15351dced3db22dc96ba3697eff24f8b170b Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Thu, 11 Mar 2021 16:10:22 +0200 Subject: [PATCH 21/64] change two more templates --- pennylane/templates/embeddings/qaoa.py | 55 +++++++++----- pennylane/templates/layers/basic_entangler.py | 8 +- .../subroutines/double_excitation_unitary.py | 74 ++++++++++--------- tests/templates/test_embeddings.py | 15 +++- tests/templates/test_subroutines.py | 42 ++++++++--- 5 files changed, 119 insertions(+), 75 deletions(-) diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index 0ac1a7d6505..ba0066f8d79 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -16,7 +16,7 @@ """ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access import pennylane as qml -from pennylane.templates.decorator import template +from pennylane.operation import Operation, AnyWires from pennylane.ops import RX, RY, RZ, MultiRZ, Hadamard from pennylane.templates import broadcast from pennylane.wires import Wires @@ -119,8 +119,7 @@ def qaoa_ising_hamiltonian(weights, wires, local_fields): broadcast(unitary=local_fields, pattern="single", wires=wires, parameters=weights_fields) -@template -def QAOAEmbedding(features, weights, wires, local_field="Y"): +class QAOAEmbedding(Operation): r""" Encodes :math:`N` features into :math:`n>N` qubits, using a layered, trainable quantum circuit that is inspired by the QAOA ansatz. @@ -260,22 +259,38 @@ def circuit(weights, f=None): 1-dimensional Ising model. """ - wires = Wires(wires) - repeat = _preprocess(features, wires, weights) - - if local_field == "Z": - local_fields = RZ - elif local_field == "X": - local_fields = RX - elif local_field == "Y": - local_fields = RY - else: - raise ValueError(f"did not recognize local field {local_field}") - for l in range(repeat): - # apply alternating Hamiltonians - qaoa_feature_encoding_hamiltonian(features, wires) - qaoa_ising_hamiltonian(weights[l], wires, local_fields) + num_params = 2 + num_wires = AnyWires + par_domain = "A" + + def __init__(self, features, weights, wires, local_field="Y", do_queue=True): + + wires = Wires(wires) + + _preprocess(features, wires, weights) + + if local_field == "Z": + self.local_fields = RZ + elif local_field == "X": + self.local_fields = RX + elif local_field == "Y": + self.local_fields = RY + else: + raise ValueError(f"did not recognize local field {local_field}") - # repeat the feature encoding once more at the end - qaoa_feature_encoding_hamiltonian(features, wires) + super().__init__(features, weights, wires=wires, do_queue=do_queue) + + def decomposition(self, features, weights, wires): + + # first dimension of the weights tensor determines + # the number of layers + repeat = qml.math.shape(weights)[0] + + for l in range(repeat): + # apply alternating Hamiltonians + qaoa_feature_encoding_hamiltonian(features, wires) + qaoa_ising_hamiltonian(weights[l], wires, self.local_fields) + + # repeat the feature encoding once more at the end + qaoa_feature_encoding_hamiltonian(features, wires) diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 7e69d7d1192..8b4320aabaf 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -158,16 +158,12 @@ def circuit(weights): par_domain = "A" def __init__(self, weights, wires=None, rotation=None, do_queue=True): - + wires = Wires(wires) _preprocess(weights, wires) - self.rotation = rotation or RX - super().__init__(weights, wires=wires, do_queue=do_queue) - def decomposition(self, *params, wires): - - weights = params[0] + def decomposition(self, weights, wires): # first dimension of the weights tensor determines # the number of layers diff --git a/pennylane/templates/subroutines/double_excitation_unitary.py b/pennylane/templates/subroutines/double_excitation_unitary.py index ce0b44403c9..ff5818ab249 100644 --- a/pennylane/templates/subroutines/double_excitation_unitary.py +++ b/pennylane/templates/subroutines/double_excitation_unitary.py @@ -19,7 +19,7 @@ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access import pennylane as qml from pennylane.ops import CNOT, RX, RZ, Hadamard -from pennylane.templates.decorator import template +from pennylane.operation import Operation, AnyWires from pennylane.wires import Wires @@ -395,8 +395,7 @@ def _layer8(weight, s, r, q, p, set_cnot_wires): RX(np.pi / 2, wires=p) -@template -def DoubleExcitationUnitary(weight, wires1=None, wires2=None): +class DoubleExcitationUnitary(Operation): r"""Circuit to exponentiate the tensor product of Pauli matrices representing the double-excitation operator entering the Unitary Coupled-Cluster Singles and Doubles (UCCSD) ansatz. UCCSD is a VQE ansatz commonly used to run quantum @@ -508,45 +507,54 @@ def circuit(weight, wires1=None, wires2=None): """ - ############## - # Input checks + num_params = 1 + num_wires = AnyWires + par_domain = "A" - wires1 = Wires(wires1) - wires2 = Wires(wires2) + def __init__(self, weight, wires1=None, wires2=None, do_queue=True): + self.wires1 = Wires(wires1) + self.wires2 = Wires(wires2) + _preprocess(weight, wires1, wires2) + # remember so we can split wires in decomposition method + self.len_wires1 = len(wires1) + wires = self.wires1 + self.wires2 + super().__init__(weight, wires=wires, do_queue=do_queue) - _preprocess(weight, wires1, wires2) + def decomposition(self, weight, wires): + # split wires back into two sets + wires1 = wires[:self.len_wires1] + wires2 = wires[self.len_wires1:] + s = wires1[0] + r = wires1[-1] + q = wires2[0] + p = wires2[-1] - s = wires1[0] - r = wires1[-1] - q = wires2[0] - p = wires2[-1] + # Sequence of the wires entering the CNOTs + cnots_occ = [wires1.subset([l, l + 1]) for l in range(len(wires1) - 1)] + cnots_unocc = [wires2.subset([l, l + 1]) for l in range(len(wires2) - 1)] - # Sequence of the wires entering the CNOTs - cnots_occ = [wires1.subset([l, l + 1]) for l in range(len(wires1) - 1)] - cnots_unocc = [wires2.subset([l, l + 1]) for l in range(len(wires2) - 1)] + set_cnot_wires = cnots_occ + [Wires([r, q])] + cnots_unocc - set_cnot_wires = cnots_occ + [Wires([r, q])] + cnots_unocc + # Apply the first layer + _layer1(weight, s, r, q, p, set_cnot_wires) - # Apply the first layer - _layer1(weight, s, r, q, p, set_cnot_wires) + # Apply the second layer + _layer2(weight, s, r, q, p, set_cnot_wires) - # Apply the second layer - _layer2(weight, s, r, q, p, set_cnot_wires) + # Apply the third layer + _layer3(weight, s, r, q, p, set_cnot_wires) - # Apply the third layer - _layer3(weight, s, r, q, p, set_cnot_wires) + # Apply the fourth layer + _layer4(weight, s, r, q, p, set_cnot_wires) - # Apply the fourth layer - _layer4(weight, s, r, q, p, set_cnot_wires) + # Apply the fifth layer + _layer5(weight, s, r, q, p, set_cnot_wires) - # Apply the fifth layer - _layer5(weight, s, r, q, p, set_cnot_wires) + # Apply the sixth layer + _layer6(weight, s, r, q, p, set_cnot_wires) - # Apply the sixth layer - _layer6(weight, s, r, q, p, set_cnot_wires) + # Apply the seventh layer + _layer7(weight, s, r, q, p, set_cnot_wires) - # Apply the seventh layer - _layer7(weight, s, r, q, p, set_cnot_wires) - - # Apply the eighth layer - _layer8(weight, s, r, q, p, set_cnot_wires) + # Apply the eighth layer + _layer8(weight, s, r, q, p, set_cnot_wires) diff --git a/tests/templates/test_embeddings.py b/tests/templates/test_embeddings.py index 2ac1203df24..329c0d2b70b 100644 --- a/tests/templates/test_embeddings.py +++ b/tests/templates/test_embeddings.py @@ -28,6 +28,7 @@ SqueezingEmbedding) from pennylane import Beamsplitter from pennylane.wires import Wires +from pennylane.tape import QuantumTape class TestAmplitudeEmbedding: @@ -503,11 +504,17 @@ class TestQAOAEmbedding: def test_queue(self, n_wires, weight_shape, expected_queue): """Checks the queue for the default settings.""" - with qml.tape.OperationRecorder() as rec: - QAOAEmbedding(features=list(range(n_wires)), weights=np.zeros(shape=weight_shape), wires=range(n_wires)) + features = list(range(n_wires)) + weights = np.zeros(shape=weight_shape) + op = QAOAEmbedding(features, weights, wires=range(n_wires)) - for gate, expected_gate in zip(rec.queue, expected_queue): - assert isinstance(gate, expected_gate) + with QuantumTape() as tape: + op.decomposition(features, weights, wires=range(n_wires)) + + res_gates = [type(op) for op in tape.operations] + + for gate, expected_gate in zip(res_gates, expected_queue): + assert gate == expected_gate def test_state_zero_weights(self, qubit_device, n_subsystems, tol): """Checks the state produced by QAOAEmbedding() is correct if the weights are zero.""" diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py index 693ba96c60c..33bb0e35d6e 100644 --- a/tests/templates/test_subroutines.py +++ b/tests/templates/test_subroutines.py @@ -21,6 +21,7 @@ import pennylane as qml from pennylane import numpy as np from pennylane.wires import Wires +from pennylane.tape import QuantumTape from pennylane.templates.subroutines import ( Interferometer, @@ -744,24 +745,29 @@ def test_double_ex_unitary_operations(self, wires1, wires2, ref_gates): sqg = 72 cnots = 16 * (len(wires1) - 1 + len(wires2) - 1 + 1) weight = np.pi / 3 - with qml.tape.OperationRecorder() as rec: - DoubleExcitationUnitary(weight, wires1=wires1, wires2=wires2) - assert len(rec.queue) == sqg + cnots + op = DoubleExcitationUnitary(weight, wires1=wires1, wires2=wires2) + + with QuantumTape() as tape: + op.decomposition(weight, wires=Wires(wires1+wires2)) + + queue = tape.operations + + assert len(queue) == sqg + cnots for gate in ref_gates: idx = gate[0] exp_gate = gate[1] - res_gate = rec.queue[idx] - assert isinstance(res_gate, exp_gate) + res_gate = queue[idx] + assert type(res_gate) == exp_gate exp_wires = gate[2] - res_wires = rec.queue[idx]._wires + res_wires = queue[idx]._wires assert res_wires == Wires(exp_wires) exp_weight = gate[3] - res_weight = rec.queue[idx].parameters + res_weight = queue[idx].parameters assert res_weight == exp_weight @pytest.mark.parametrize( @@ -906,24 +912,36 @@ def test_uccsd_operations(self, s_wires, d_wires, weights, ref_gates): ref_state = np.array([1, 1, 0, 0, 0, 0]) - with qml.tape.OperationRecorder() as rec: + with QuantumTape() as tape: UCCSD(weights, wires, s_wires=s_wires, d_wires=d_wires, init_state=ref_state) - assert len(rec.queue) == sqg + cnots + 1 + mixed_queue = tape.operations + + # hack: replace DoubleExcitationUnitary with list of gates + queue = [] + for g in mixed_queue: + if type(g) == DoubleExcitationUnitary: + with QuantumTape() as tape2: + g.decomposition(*g.parameters, g.wires) + queue.extend(tape2.operations) + else: + queue.append(g) + + assert len(queue) == sqg + cnots + 1 for gate in ref_gates: idx = gate[0] exp_gate = gate[1] - res_gate = rec.queue[idx] + res_gate = queue[idx] assert isinstance(res_gate, exp_gate) exp_wires = gate[2] - res_wires = rec.queue[idx]._wires + res_wires = queue[idx]._wires assert res_wires == Wires(exp_wires) exp_weight = gate[3] - res_weight = rec.queue[idx].parameters + res_weight = queue[idx].parameters if exp_gate != qml.BasisState: assert res_weight == exp_weight else: From bf7428766a7970d2620500c0960aa9b6fb1179a9 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Thu, 11 Mar 2021 16:56:28 +0200 Subject: [PATCH 22/64] black --- pennylane/templates/layers/basic_entangler.py | 4 +++- pennylane/templates/subroutines/double_excitation_unitary.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 8b4320aabaf..8d65c821a94 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -170,5 +170,7 @@ def decomposition(self, weights, wires): repeat = qml.math.shape(weights)[0] for layer in range(repeat): - broadcast(unitary=self.rotation, pattern="single", wires=wires, parameters=weights[layer]) + broadcast( + unitary=self.rotation, pattern="single", wires=wires, parameters=weights[layer] + ) broadcast(unitary=CNOT, pattern="ring", wires=wires) diff --git a/pennylane/templates/subroutines/double_excitation_unitary.py b/pennylane/templates/subroutines/double_excitation_unitary.py index ff5818ab249..7d0bcf0355d 100644 --- a/pennylane/templates/subroutines/double_excitation_unitary.py +++ b/pennylane/templates/subroutines/double_excitation_unitary.py @@ -522,8 +522,8 @@ def __init__(self, weight, wires1=None, wires2=None, do_queue=True): def decomposition(self, weight, wires): # split wires back into two sets - wires1 = wires[:self.len_wires1] - wires2 = wires[self.len_wires1:] + wires1 = wires[: self.len_wires1] + wires2 = wires[self.len_wires1 :] s = wires1[0] r = wires1[-1] q = wires2[0] From be672e339c2b6fc4e40947cc161208aa777ab969 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Fri, 12 Mar 2021 14:27:55 +0800 Subject: [PATCH 23/64] Update tests/test_queuing.py Co-authored-by: antalszava --- tests/test_queuing.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/test_queuing.py b/tests/test_queuing.py index 9c7e6775ac6..172e569871e 100644 --- a/tests/test_queuing.py +++ b/tests/test_queuing.py @@ -6,22 +6,6 @@ # 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:`QueuingContext` class. -""" -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. From 50b074ae756d7b961212efea81bcab4c14fd7cbf Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Fri, 12 Mar 2021 14:36:48 +0800 Subject: [PATCH 24/64] Update pennylane/circuit_graph.py Co-authored-by: antalszava --- pennylane/circuit_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 176ea7e6fcb..85118bc45c7 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -99,7 +99,7 @@ class CircuitGraph: Args: ops (Iterable[.Operator]): quantum operators constituting the circuit, in temporal order obs (Iterable[.MeasurementProcess]): terminal measurements, in temporal order - wires (.Wires): The addressable wire register of the device that will be executing this graph + wires (.Wires): The addressable wire registers of the device that will be executing this graph par_info (dict[int, dict[str, .Operation or int]]): Parameter information. Keys are parameter indices (in the order they appear on the tape), and values are a dictionary containing the corresponding operation and operation parameter index. From 30c3bb11b01a2648389f120995a32a7dc3bb86fe Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Fri, 12 Mar 2021 15:03:24 +0800 Subject: [PATCH 25/64] Update pennylane/tape/tape.py --- pennylane/tape/tape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py index 7e3c4295f8b..d8d3fa88619 100644 --- a/pennylane/tape/tape.py +++ b/pennylane/tape/tape.py @@ -76,7 +76,7 @@ - ``CX`` (equivalent to :class:`~.CNOT`) All other gates are defined in the file qelib1.inc: -https://github.com/Qiskit/openqasm/blob/master/examples/generic/qelib1.inc +https://github.com/Qiskit/openqasm/blob/master/examples/stdgates.inc """ From 55e2206c8928e9f37d5286ddce054b2d87295147 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Fri, 12 Mar 2021 16:21:58 +0800 Subject: [PATCH 26/64] merge master --- pennylane/_qubit_device.py | 7 +------ pennylane/qnode.py | 3 +-- pennylane/transforms/draw.py | 15 +++++++++++++++ tests/transforms/test_metric_tensor.py | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index 5c8001fbc0f..d87523c989c 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -183,12 +183,7 @@ def execute(self, circuit, **kwargs): Returns: array[float]: measured value(s) """ - # TODO: Remove try/except when circuit is always QuantumTape and - # consider merging with caching case - try: - self._circuit_hash = circuit.graph.hash - except AttributeError: - self._circuit_hash = circuit.hash + self._circuit_hash = circuit.graph.hash if self._cache: circuit_hash = circuit.graph.hash diff --git a/pennylane/qnode.py b/pennylane/qnode.py index 6ff0885b343..8f14a9dd62d 100644 --- a/pennylane/qnode.py +++ b/pennylane/qnode.py @@ -596,7 +596,7 @@ def metric_tensor(self, *args, diag_approx=False, only_construct=False, **kwargs ) def draw( - self, charset="unicode", wire_order=None, show_all_wires=False, **kwargs + self, charset="unicode", wire_order=None, show_all_wires=False ): # pylint: disable=unused-argument """Draw the quantum tape as a circuit diagram. @@ -666,7 +666,6 @@ def circuit(): a: ──╰C──RX(0.2)──┤ -1: ───H───────────┤ """ - # TODO: remove 'kwargs' when tape mode is default. # Currently it only exists to match the signature of non-tape mode draw. if self.qtape is None: raise qml.QuantumFunctionError( diff --git a/pennylane/transforms/draw.py b/pennylane/transforms/draw.py index c929c34c7c8..277f5436e80 100644 --- a/pennylane/transforms/draw.py +++ b/pennylane/transforms/draw.py @@ -89,6 +89,21 @@ def wrapper(*args, **kwargs): qnode.construct(args, kwargs) _wire_order = wire_order or qnode.device.wires _wire_order = qml.wires.Wires(_wire_order) + + if show_all_wires and len(_wire_order) < qnode.device.num_wires: + raise ValueError( + "When show_all_wires is enabled, the provided wire order must contain all wires on the device." + ) + + if not qnode.device.wires.contains_wires(_wire_order): + raise ValueError( + f"Provided wire order {_wire_order.labels} contains wires not contained on the device: {qnode.device.wires}." + ) + + return qnode.qtape.draw( + charset=charset, wire_order=_wire_order, show_all_wires=show_all_wires + ) + return qnode.qtape.draw(charset, wire_order=_wire_order, show_all_wires=show_all_wires) return wrapper diff --git a/tests/transforms/test_metric_tensor.py b/tests/transforms/test_metric_tensor.py index 7fd09c865f1..43aac567c74 100644 --- a/tests/transforms/test_metric_tensor.py +++ b/tests/transforms/test_metric_tensor.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Unit tests for the :mod:`pennylane` :class:`qml.QNode` metric tensor methods. +Unit tests for the metric tensor transform. """ import pytest import numpy as np From 5d92768d67e6a2466b2652179350004016b2cc5b Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 15 Mar 2021 08:31:50 +0200 Subject: [PATCH 27/64] Update pennylane/templates/embeddings/qaoa.py Co-authored-by: Nathan Killoran --- pennylane/templates/embeddings/qaoa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index ba0066f8d79..fbafa3f6278 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -277,7 +277,7 @@ def __init__(self, features, weights, wires, local_field="Y", do_queue=True): elif local_field == "Y": self.local_fields = RY else: - raise ValueError(f"did not recognize local field {local_field}") + raise ValueError(f"Did not recognize local field {local_field}") super().__init__(features, weights, wires=wires, do_queue=do_queue) From c1d2874a868218e1ca9751c579da81a9c656b41b Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Mon, 15 Mar 2021 15:33:19 +0800 Subject: [PATCH 28/64] Update pennylane/measure.py Co-authored-by: Maria Schuld --- pennylane/measure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/measure.py b/pennylane/measure.py index c7bd27c9724..0dd0ceb0e9b 100644 --- a/pennylane/measure.py +++ b/pennylane/measure.py @@ -65,7 +65,7 @@ def __init__(self, return_type, obs=None, wires=None, eigvals=None): # measurement processes rather than specific observables. # The following lines are only applicable for measurement processes - # that do no have corresponding observables (e.g., Probability). We use + # that do not have corresponding observables (e.g., Probability). We use # them to 'trick' the device into thinking it has recieved an observable. # Below, we imitate an identity observable, so that the From 39e77686bdeb93bf7493db2f914fcd5c68c178b1 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Mon, 15 Mar 2021 15:34:49 +0800 Subject: [PATCH 29/64] Update doc/code/qml_tape.rst --- doc/code/qml_tape.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/code/qml_tape.rst b/doc/code/qml_tape.rst index a95a47e5715..468542378e5 100644 --- a/doc/code/qml_tape.rst +++ b/doc/code/qml_tape.rst @@ -1,8 +1,7 @@ qml.tape ======== -Quantum tapes are responsible for recording quantum operations, executing devices, or computing -gradients. +Quantum tapes are a datastructure that can represent quantum circuits and measurement statistics in PennyLane. They are queuing contexts that can record quantum operations, execute devices, and compute gradients. In addition to being created internally by QNodes, quantum tapes can also be created, nested, expanded (via :meth:`~.QuantumTape.expand`), and executed manually. Tape subclasses also provide @@ -23,4 +22,3 @@ TensorFlow, and PyTorch. .. automodapi:: pennylane.tape :no-main-docstr: :include-all-objects: - From 899911bc11a0c92e9f1da0a2c0ad6a9b24bb4b5f Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Mon, 15 Mar 2021 15:40:55 +0800 Subject: [PATCH 30/64] Update doc/code/qml_tape.rst --- doc/code/qml_tape.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/code/qml_tape.rst b/doc/code/qml_tape.rst index 468542378e5..38ff7e4bbb7 100644 --- a/doc/code/qml_tape.rst +++ b/doc/code/qml_tape.rst @@ -7,6 +7,13 @@ In addition to being created internally by QNodes, quantum tapes can also be cre nested, expanded (via :meth:`~.QuantumTape.expand`), and executed manually. Tape subclasses also provide additional gradient methods: +.. autosummary:: + + ~pennylane.tape.QuantumTape + ~pennylane.tape.QubitParamShiftTape + ~pennylane.tape.CVParamShiftTape + ~pennylane.tape.ReversibleTape + Finally, quantum tapes are fully compatible with autodifferentiating via NumPy/Autograd, TensorFlow, and PyTorch. From 3647cc726fca69eb5bdeaebf4ebc927e01f9ddbb Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 15 Mar 2021 11:38:25 +0200 Subject: [PATCH 31/64] finish qaoa embedding --- pennylane/templates/embeddings/qaoa.py | 112 ++++--- tests/templates/test_embeddings.py | 178 ----------- tests/templates/test_embeddings/test_qaoa.py | 318 +++++++++++++++++++ tests/templates/test_integration.py | 18 -- 4 files changed, 373 insertions(+), 253 deletions(-) create mode 100644 tests/templates/test_embeddings/test_qaoa.py diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index ba0066f8d79..348daea81fe 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -22,52 +22,6 @@ from pennylane.wires import Wires -def _preprocess(features, wires, weights): - """Validate and pre-process inputs as follows: - - * Check that the features tensor is one-dimensional. - * Check that the first dimension of the features tensor - has length :math:`n` or less, where :math:`n` is the number of qubits. - * Check that the shape of the weights tensor is correct for the number of qubits. - - Args: - features (tensor_like): input features to pre-process - wires (Wires): wires that template acts on - weights (tensor_like): weights of the embedding - - Returns: - int: number of times that embedding is repeated - """ - shape = qml.math.shape(features) - - if len(shape) != 1: - raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") - - n_features = shape[0] - if n_features > len(wires): - raise ValueError( - f"Features must be of length {len(wires)} or less; got length {n_features}." - ) - - shape = qml.math.shape(weights) - repeat = shape[0] - - if len(wires) == 1: - if shape != (repeat, 1): - raise ValueError(f"Weights tensor must be of shape {(repeat, 1)}; got {shape}") - - elif len(wires) == 2: - if shape != (repeat, 3): - raise ValueError(f"Weights tensor must be of shape {(repeat, 3)}; got {shape}") - else: - if shape != (repeat, 2 * len(wires)): - raise ValueError( - f"Weights tensor must be of shape {(repeat, 2*len(wires))}; got {shape}" - ) - - return repeat - - def qaoa_feature_encoding_hamiltonian(features, wires): """Implements the encoding Hamiltonian of the QAOA embedding. @@ -266,10 +220,6 @@ def circuit(weights, f=None): def __init__(self, features, weights, wires, local_field="Y", do_queue=True): - wires = Wires(wires) - - _preprocess(features, wires, weights) - if local_field == "Z": self.local_fields = RZ elif local_field == "X": @@ -281,16 +231,64 @@ def __init__(self, features, weights, wires, local_field="Y", do_queue=True): super().__init__(features, weights, wires=wires, do_queue=do_queue) - def decomposition(self, features, weights, wires): + self._preprocess() + + def expand(self): + + features = self.data[0] + weights = self.data[1] # first dimension of the weights tensor determines # the number of layers repeat = qml.math.shape(weights)[0] - for l in range(repeat): - # apply alternating Hamiltonians - qaoa_feature_encoding_hamiltonian(features, wires) - qaoa_ising_hamiltonian(weights[l], wires, self.local_fields) + with qml.tape.QuantumTape() as tape: + + for l in range(repeat): + # apply alternating Hamiltonians + qaoa_feature_encoding_hamiltonian(features, self.wires) + qaoa_ising_hamiltonian(weights[l], self.wires, self.local_fields) + + # repeat the feature encoding once more at the end + qaoa_feature_encoding_hamiltonian(features, self.wires) + + return tape + + def _preprocess(self): + """Validate and pre-process inputs as follows: + + * Check that the features tensor is one-dimensional. + * Check that the first dimension of the features tensor + has length :math:`n` or less, where :math:`n` is the number of qubits. + * Check that the shape of the weights tensor is correct for the number of qubits. + """ - # repeat the feature encoding once more at the end - qaoa_feature_encoding_hamiltonian(features, wires) + features = self.data[0] + weights = self.data[1] + + shape = qml.math.shape(features) + + if len(shape) != 1: + raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") + + n_features = shape[0] + if n_features > len(self.wires): + raise ValueError( + f"Features must be of length {len(self.wires)} or less; got length {n_features}." + ) + + shape = qml.math.shape(weights) + repeat = shape[0] + + if len(self.wires) == 1: + if shape != (repeat, 1): + raise ValueError(f"Weights tensor must be of shape {(repeat, 1)}; got {shape}") + + elif len(self.wires) == 2: + if shape != (repeat, 3): + raise ValueError(f"Weights tensor must be of shape {(repeat, 3)}; got {shape}") + else: + if shape != (repeat, 2 * len(self.wires)): + raise ValueError( + f"Weights tensor must be of shape {(repeat, 2*len(self.wires))}; got {shape}" + ) diff --git a/tests/templates/test_embeddings.py b/tests/templates/test_embeddings.py index 329c0d2b70b..563c8cf0202 100644 --- a/tests/templates/test_embeddings.py +++ b/tests/templates/test_embeddings.py @@ -492,185 +492,7 @@ def circuit(f=None): circuit(f=features) -class TestQAOAEmbedding: - """ Tests the QAOAEmbedding method.""" - QUEUES = [(1, (1, 1), [qml.RX, qml.RY, qml.RX]), - (2, (1, 3), [qml.RX, qml.RX, qml.MultiRZ, qml.RY, qml.RY, qml.RX, qml.RX]), - (3, (1, 6), [qml.RX, qml.RX, qml.RX, qml.MultiRZ, qml.MultiRZ, qml.MultiRZ, - qml.RY, qml.RY, qml.RY, qml.RX, qml.RX, qml.RX])] - - @pytest.mark.parametrize('n_wires, weight_shape, expected_queue', QUEUES) - def test_queue(self, n_wires, weight_shape, expected_queue): - """Checks the queue for the default settings.""" - - features = list(range(n_wires)) - weights = np.zeros(shape=weight_shape) - op = QAOAEmbedding(features, weights, wires=range(n_wires)) - - with QuantumTape() as tape: - op.decomposition(features, weights, wires=range(n_wires)) - - res_gates = [type(op) for op in tape.operations] - - for gate, expected_gate in zip(res_gates, expected_queue): - assert gate == expected_gate - - def test_state_zero_weights(self, qubit_device, n_subsystems, tol): - """Checks the state produced by QAOAEmbedding() is correct if the weights are zero.""" - - features = [pi, pi / 2, pi / 4, 0] - if n_subsystems == 1: - shp = (1, 1) - elif n_subsystems == 2: - shp = (1, 3) - else: - shp = (1, 2 * n_subsystems) - - weights = np.zeros(shape=shp) - - @qml.qnode(qubit_device) - def circuit(x=None): - QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems)) - return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] - - res = circuit(x=features[:n_subsystems]) - target = [1, -1, 0, 1, 1] - assert np.allclose(res, target[:n_subsystems], atol=tol, rtol=0) - - @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[pi / 2]], [0]), - (2, [[1, pi / 2, pi / 4]], [0, 1 / np.sqrt(2)]), - (3, [[0, 0, 0, pi, pi / 2, pi / 4]], - [-1, 0, 1 / np.sqrt(2)])]) - def test_output_local_field_ry(self, n_subsystems, weights, target, tol): - """Checks the output if the features are zero. Uses RY local fields.""" - - features = np.zeros(shape=(n_subsystems,)) - dev = qml.device('default.qubit', wires=n_subsystems) - - @qml.qnode(dev) - def circuit(x=None): - QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='Y') - return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] - - res = circuit(x=features[:n_subsystems]) - assert np.allclose(res, target, atol=tol, rtol=0) - - @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[pi / 2]], [0]), - (2, [[1, pi / 2, pi / 4]], [0, 1 / np.sqrt(2)]), - (3, [[0, 0, 0, pi, pi / 2, pi / 4]], - [-1, 0, 1 / np.sqrt(2)])]) - def test_output_local_field_rx(self, n_subsystems, weights, target, tol): - """Checks the output if the features are zero. Uses RX local fields.""" - - features = np.zeros(shape=(n_subsystems,)) - dev = qml.device('default.qubit', wires=n_subsystems) - - @qml.qnode(dev) - def circuit(x=None): - QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='X') - return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] - - res = circuit(x=features[:n_subsystems]) - assert np.allclose(res, target, atol=tol, rtol=0) - - @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[pi / 2]], [1]), - (2, [[1, pi / 2, pi / 4]], [1, 1]), - (3, [[0, 0, 0, pi, pi / 2, pi / 4]], [1, 1, 1])]) - def test_output_local_field_rz(self, n_subsystems, weights, target, tol): - """Checks the output if the features are zero. Uses RZ local fields.""" - - features = np.zeros(shape=(n_subsystems,)) - dev = qml.device('default.qubit', wires=n_subsystems) - - @qml.qnode(dev) - def circuit(x=None): - QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='Z') - return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] - - res = circuit(x=features[:n_subsystems]) - assert np.allclose(res, target, atol=tol, rtol=0) - - @pytest.mark.parametrize('weights, target', [([[np.pi, 0, 0]], [1, 1]), - ([[np.pi / 2, 0, 0]], [0, 0]), - ([[0, 0, 0]], [-1, -1])]) - def test_output_zz(self, weights, target, tol): - """Checks the output if the features and entangler weights are nonzero.""" - - dev = qml.device('default.qubit', wires=2) - - @qml.qnode(dev) - def circuit(x=None): - QAOAEmbedding(features=x, weights=weights, wires=range(2)) - return [qml.expval(qml.PauliZ(i)) for i in range(2)] - - res = circuit(x=[np.pi/2, np.pi/2]) - - assert np.allclose(res, target, atol=tol, rtol=0) - - @pytest.mark.parametrize('n_wires, features, weights, target', [(2, [0], [[0, 0, np.pi / 2]], [1, 0]), - (3, [0, 0], [[0, 0, 0, 0, 0, np.pi / 2]], - [1, 1, 0])]) - def test_state_more_qubits_than_features(self, n_wires, features, weights, target, tol): - """Checks the state is correct if there are more qubits than features.""" - - dev = qml.device('default.qubit', wires=n_wires) - - @qml.qnode(dev) - def circuit(x=None): - QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='Z') - return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] - - res = circuit(x=features) - assert np.allclose(res, target, atol=tol, rtol=0) - - def test_exception_fewer_wires_than_features(self, ): - """Verifies that exception raised if there are fewer - wires than features.""" - - features = [0, 0, 0, 0] - n_wires = 1 - weights = np.zeros(shape=(1, 2 * n_wires)) - dev = qml.device('default.qubit', wires=n_wires) - - @qml.qnode(dev) - def circuit(x=None): - QAOAEmbedding(features=x, weights=weights, wires=range(n_wires)) - return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] - - with pytest.raises(ValueError, match="Features must be of "): - circuit(x=features) - - def test_exception_wrongrot(self): - """Verifies exception raised if the - rotation strategy is unknown.""" - - n_wires = 1 - weights = np.zeros(shape=(1, 1)) - dev = qml.device('default.qubit', wires=n_wires) - - @qml.qnode(dev) - def circuit(x=None): - QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='A') - return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] - - with pytest.raises(ValueError, match="did not recognize"): - circuit(x=[1]) - - def test_exception_wrong_dim(self): - """Verifies that exception is raised if the - number of dimensions of features is incorrect.""" - n_wires = 1 - weights = np.zeros(shape=(1, 1)) - dev = qml.device('default.qubit', wires=n_wires) - - @qml.qnode(dev) - def circuit(x=None): - QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='A') - return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] - - with pytest.raises(ValueError, match="Features must be a one-dimensional"): - circuit(x=[[1], [0]]) class TestDisplacementEmbedding: diff --git a/tests/templates/test_embeddings/test_qaoa.py b/tests/templates/test_embeddings/test_qaoa.py new file mode 100644 index 00000000000..b7e4b88b3a8 --- /dev/null +++ b/tests/templates/test_embeddings/test_qaoa.py @@ -0,0 +1,318 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests for QAOAEmbedding template. +""" +import pytest + +import numpy as np +import pennylane as qml + + +class TestDecomposition: + """Tests that the template defines the correct decomposition.""" + + QUEUES = [(1, (1, 1), [qml.RX, qml.RY, qml.RX]), + (2, (1, 3), [qml.RX, qml.RX, qml.MultiRZ, qml.RY, qml.RY, qml.RX, qml.RX]), + (2, (2, 3), [qml.RX, qml.RX, qml.MultiRZ, qml.RY, qml.RY, qml.RX, qml.RX, + qml.MultiRZ, qml.RY, qml.RY, qml.RX, qml.RX]), + (3, (1, 6), [qml.RX, qml.RX, qml.RX, qml.MultiRZ, qml.MultiRZ, qml.MultiRZ, + qml.RY, qml.RY, qml.RY, qml.RX, qml.RX, qml.RX])] + + @pytest.mark.parametrize('n_wires, weight_shape, expected_types', QUEUES) + def test_queue(self, n_wires, weight_shape, expected_types): + """Checks the queue for the default settings.""" + + features = list(range(n_wires)) + weights = np.zeros(shape=weight_shape) + + op = qml.templates.QAOAEmbedding(features, weights, wires=range(n_wires)) + tape = op.expand() + + for i, gate in enumerate(tape.operations): + assert type(gate) == expected_types[i] + + def test_state_zero_weights(self, qubit_device, n_subsystems, tol): + """Checks the state is correct if the weights are zero.""" + + features = [np.pi, np.pi / 2, np.pi / 4, 0] + if n_subsystems == 1: + shp = (1, 1) + elif n_subsystems == 2: + shp = (1, 3) + else: + shp = (1, 2 * n_subsystems) + + weights = np.zeros(shape=shp) + + @qml.qnode(qubit_device) + def circuit(x=None): + qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems)) + return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] + + res = circuit(x=features[:n_subsystems]) + target = [1, -1, 0, 1, 1] + assert np.allclose(res, target[:n_subsystems], atol=tol, rtol=0) + + @pytest.mark.parametrize('weights, target', [([[np.pi, 0, 0]], [1, 1]), + ([[np.pi / 2, 0, 0]], [0, 0]), + ([[0, 0, 0]], [-1, -1])]) + def test_output_zz(self, weights, target, tol): + """Checks the output if the features and entangler weights are nonzero, + which makes the circuit only depend on the ZZ gate.""" + + dev = qml.device('default.qubit', wires=2) + + @qml.qnode(dev) + def circuit(x=None): + qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(2)) + return [qml.expval(qml.PauliZ(i)) for i in range(2)] + + res = circuit(x=[np.pi/2, np.pi/2]) + + assert np.allclose(res, target, atol=tol, rtol=0) + + +class TestHyperparameters: + """Tests that the template's hyperparameters have the desired effect.""" + + @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[np.pi / 2]], [0]), + (2, [[1, np.pi / 2, np.pi / 4]], [0, 1 / np.sqrt(2)]), + (3, [[0, 0, 0, np.pi, np.pi / 2, np.pi / 4]], + [-1, 0, 1 / np.sqrt(2)])]) + def test_output_local_field_ry(self, n_subsystems, weights, target, tol): + """Checks the output if the features are zero for RY local fields.""" + + features = np.zeros(shape=(n_subsystems,)) + dev = qml.device('default.qubit', wires=n_subsystems) + + @qml.qnode(dev) + def circuit(x=None): + qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='Y') + return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] + + res = circuit(x=features[:n_subsystems]) + assert np.allclose(res, target, atol=tol, rtol=0) + + @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[np.pi / 2]], [0]), + (2, [[1, np.pi / 2, np.pi / 4]], [0, 1 / np.sqrt(2)]), + (3, [[0, 0, 0, np.pi, np.pi / 2, np.pi / 4]], + [-1, 0, 1 / np.sqrt(2)])]) + def test_output_local_field_rx(self, n_subsystems, weights, target, tol): + """Checks the output if the features are zero for RX local fields.""" + + features = np.zeros(shape=(n_subsystems,)) + dev = qml.device('default.qubit', wires=n_subsystems) + + @qml.qnode(dev) + def circuit(x=None): + qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='X') + return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] + + res = circuit(x=features[:n_subsystems]) + assert np.allclose(res, target, atol=tol, rtol=0) + + @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[np.pi / 2]], [1]), + (2, [[1, np.pi / 2, np.pi / 4]], [1, 1]), + (3, [[0, 0, 0, np.pi, np.pi / 2, np.pi / 4]], [1, 1, 1])]) + def test_output_local_field_rz(self, n_subsystems, weights, target, tol): + """Checks the output if the features are zero for RZ local fields.""" + + features = np.zeros(shape=(n_subsystems,)) + dev = qml.device('default.qubit', wires=n_subsystems) + + @qml.qnode(dev) + def circuit(x=None): + qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='Z') + return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] + + res = circuit(x=features[:n_subsystems]) + assert np.allclose(res, target, atol=tol, rtol=0) + + @pytest.mark.parametrize('n_wires, features, weights, target', [(2, [0], [[0, 0, np.pi / 2]], [1, 0]), + (3, [0, 0], [[0, 0, 0, 0, 0, np.pi / 2]], + [1, 1, 0])]) + def test_state_more_qubits_than_features(self, n_wires, features, weights, target, tol): + """Checks the state is correct if there are more qubits than features.""" + + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(x=None): + qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='Z') + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + res = circuit(x=features) + assert np.allclose(res, target, atol=tol, rtol=0) + + def test_exception_fewer_wires_than_features(self, ): + """Verifies that exception raised if there are fewer + wires than features.""" + + features = [0, 0, 0, 0] + n_wires = 1 + weights = np.zeros(shape=(1, 2 * n_wires)) + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(x=None): + qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires)) + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + with pytest.raises(ValueError, match="Features must be of "): + circuit(x=features) + + def test_exception_wrongrot(self): + """Verifies exception raised if the + rotation strategy is unknown.""" + + n_wires = 1 + weights = np.zeros(shape=(1, 1)) + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(x=None): + qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='A') + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + with pytest.raises(ValueError, match="did not recognize"): + circuit(x=[1]) + + def test_exception_wrong_dim(self): + """Verifies that exception is raised if the + number of dimensions of features is incorrect.""" + n_wires = 1 + weights = np.zeros(shape=(1, 1)) + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(x=None): + qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires)) + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + with pytest.raises(ValueError, match="Features must be a one-dimensional"): + circuit(x=[[1], [0]]) + + +class TestGradients: + """Tests that the gradient is computed correctly in all three interfaces.""" + + def circuit(self, features, weights): + qml.templates.QAOAEmbedding(features, weights, range(2)) + return qml.expval(qml.PauliZ(0)) + + def circuit_decomposed(self, features, weights): + qml.RX(features[0], wires=0) + qml.RX(features[1], wires=1) + qml.MultiRZ(weights[0, 0], wires=[0, 1]) + qml.RY(weights[0, 1], wires=0) + qml.RY(weights[0, 2], wires=1) + qml.RX(features[0], wires=0) + qml.RX(features[1], wires=1) + return qml.expval(qml.PauliZ(0)) + + def test_autograd(self, tol): + """Tests that gradients of template and decomposed circuit + are the same in the autograd interface.""" + + features = np.random.random(size=(2, )) + weights = np.random.random(size=(1, 3)) + + dev = qml.device('default.qubit', wires=2) + + circuit = qml.QNode(self.circuit, dev) + circuit2 = qml.QNode(self.circuit_decomposed, dev) + + grad_fn = qml.grad(circuit) + grads = grad_fn(features, weights) + + grad_fn2 = qml.grad(circuit2) + grads2 = grad_fn2(features, weights) + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) + assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) + + def test_jax(self, tol, skip_if_no_jax_support): + """Tests that gradients of template and decomposed circuit + are the same in the jax interface.""" + + import jax + import jax.numpy as jnp + + features = jnp.array(np.random.random(size=(2, ))) + weights = jnp.array(np.random.random(size=(1, 3))) + + dev = qml.device('default.qubit', wires=2) + + circuit = qml.QNode(self.circuit, dev, interface='jax') + circuit2 = qml.QNode(self.circuit_decomposed, dev, interface='jax') + + grad_fn = jax.grad(circuit) + grads = grad_fn(features, weights) + + grad_fn2 = jax.grad(circuit2) + grads2 = grad_fn2(features, weights) + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) + assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) + + def test_tf(self, tol, skip_if_no_tf_support): + """Tests that gradients of template and decomposed circuit + are the same in the tf interface.""" + + import tensorflow as tf + + features = tf.Variable(np.random.random(size=(2, ))) + weights = tf.Variable(np.random.random(size=(1, 3))) + + dev = qml.device('default.qubit', wires=2) + + circuit = qml.QNode(self.circuit, dev, interface='tf') + circuit2 = qml.QNode(self.circuit_decomposed, dev, interface='tf') + + with tf.GradientTape() as tape: + res = circuit(features, weights) + grads = tape.gradient(res, [features, weights]) + + with tf.GradientTape() as tape2: + res2 = circuit2(features, weights) + grads2 = tape2.gradient(res2, [features, weights]) + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) + assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) + + def test_torch(self, tol, skip_if_no_torch_support): + """Tests that gradients of template and decomposed circuit + are the same in the torch interface.""" + + import torch + + features = torch.tensor(np.random.random(size=(2, )), requires_grad=True) + weights = torch.tensor(np.random.random(size=(1, 3)), requires_grad=True) + + dev = qml.device('default.qubit', wires=2) + + circuit = qml.QNode(self.circuit, dev, interface='torch') + circuit2 = qml.QNode(self.circuit_decomposed, dev, interface='torch') + + res = circuit(features, weights) + res.backward() + grads = [features.grad, weights.grad] + + res2 = circuit2(features, weights) + res2.backward() + grads2 = [features.grad, weights.grad] + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) + assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) diff --git a/tests/templates/test_integration.py b/tests/templates/test_integration.py index 3bb341b708c..edf1c69f8d7 100644 --- a/tests/templates/test_integration.py +++ b/tests/templates/test_integration.py @@ -225,24 +225,6 @@ {'weights': qml.init.random_layers_normal(n_layers=2, n_rots=2, n_wires=1), 'wires': range(1)}), (qml.templates.RandomLayers, {'weights': qml.init.random_layers_normal(n_layers=2, n_rots=2, n_wires=2), 'wires': range(2)}), - (qml.templates.QAOAEmbedding, - {'features': [1.], 'weights': qml.init.qaoa_embedding_uniform(n_layers=3, n_wires=1), - 'wires': range(1)}), - (qml.templates.QAOAEmbedding, - {'features': [1., 2.], 'weights': qml.init.qaoa_embedding_uniform(n_layers=3, n_wires=2), - 'wires': range(2)}), - (qml.templates.QAOAEmbedding, - {'features': [1., 2., 3.], 'weights': qml.init.qaoa_embedding_uniform(n_layers=2, n_wires=3), - 'wires': range(3)}), - (qml.templates.QAOAEmbedding, - {'features': [1.], 'weights': qml.init.qaoa_embedding_normal(n_layers=3, n_wires=1), - 'wires': range(1)}), - (qml.templates.QAOAEmbedding, - {'features': [1., 2.], 'weights': qml.init.qaoa_embedding_normal(n_layers=3, n_wires=2), - 'wires': range(2)}), - (qml.templates.QAOAEmbedding, - {'features': [1., 2., 3.], 'weights': qml.init.qaoa_embedding_normal(n_layers=3, n_wires=3), - 'wires': range(3)}), (qml.templates.SimplifiedTwoDesign, {'initial_layer_weights': qml.init.simplified_two_design_initial_layer_uniform(n_wires=4), 'weights': qml.init.simplified_two_design_weights_uniform(n_layers=3, n_wires=4), From e3b73e38cab14751f94b2703ae7288607a8b30f2 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 15 Mar 2021 11:56:33 +0200 Subject: [PATCH 32/64] polish tests for qaoa embedding --- tests/templates/test_embeddings/test_qaoa.py | 130 +++++++------------ 1 file changed, 45 insertions(+), 85 deletions(-) diff --git a/tests/templates/test_embeddings/test_qaoa.py b/tests/templates/test_embeddings/test_qaoa.py index b7e4b88b3a8..cdf5b36bbec 100644 --- a/tests/templates/test_embeddings/test_qaoa.py +++ b/tests/templates/test_embeddings/test_qaoa.py @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Unit tests for QAOAEmbedding template. +Unit tests for the QAOAEmbedding template. """ import pytest - import numpy as np import pennylane as qml @@ -23,15 +22,15 @@ class TestDecomposition: """Tests that the template defines the correct decomposition.""" - QUEUES = [(1, (1, 1), [qml.RX, qml.RY, qml.RX]), - (2, (1, 3), [qml.RX, qml.RX, qml.MultiRZ, qml.RY, qml.RY, qml.RX, qml.RX]), - (2, (2, 3), [qml.RX, qml.RX, qml.MultiRZ, qml.RY, qml.RY, qml.RX, qml.RX, - qml.MultiRZ, qml.RY, qml.RY, qml.RX, qml.RX]), - (3, (1, 6), [qml.RX, qml.RX, qml.RX, qml.MultiRZ, qml.MultiRZ, qml.MultiRZ, - qml.RY, qml.RY, qml.RY, qml.RX, qml.RX, qml.RX])] + QUEUES = [(1, (1, 1), ['RX', 'RY', 'RX']), + (2, (1, 3), ['RX', 'RX', 'MultiRZ', 'RY', 'RY', 'RX', 'RX']), + (2, (2, 3), ['RX', 'RX', 'MultiRZ', 'RY', 'RY', 'RX', 'RX', + 'MultiRZ', 'RY', 'RY', 'RX', 'RX']), + (3, (1, 6), ['RX', 'RX', 'RX', 'MultiRZ', 'MultiRZ', 'MultiRZ', + 'RY', 'RY', 'RY', 'RX', 'RX', 'RX'])] - @pytest.mark.parametrize('n_wires, weight_shape, expected_types', QUEUES) - def test_queue(self, n_wires, weight_shape, expected_types): + @pytest.mark.parametrize('n_wires, weight_shape, expected_names', QUEUES) + def test_expansion(self, n_wires, weight_shape, expected_names): """Checks the queue for the default settings.""" features = list(range(n_wires)) @@ -41,7 +40,41 @@ def test_queue(self, n_wires, weight_shape, expected_types): tape = op.expand() for i, gate in enumerate(tape.operations): - assert type(gate) == expected_types[i] + assert gate.name == expected_names[i] + + @pytest.mark.parametrize('local_field', ['X', 'Y', 'Z']) + def test_local_field(self, local_field): + """Checks that custom local field is used.""" + + get_name = {'X': 'RX', + 'Y': 'RY', + 'Z': 'RZ'} + + features = list(range(2)) + weights = np.zeros(shape=(1, 3)) + + op = qml.templates.QAOAEmbedding(features, weights, wires=range(2), local_field=local_field) + tape = op.expand() + gate_names = [gate.name for gate in tape.operations] + + assert gate_names[3] == get_name[local_field] + assert gate_names[4] == get_name[local_field] + + def test_exception_wrongrot(self): + """Verifies exception raised if the + rotation strategy is unknown.""" + + n_wires = 1 + weights = np.zeros(shape=(1, 1)) + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(x=None): + qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='A') + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + with pytest.raises(ValueError, match="did not recognize"): + circuit(x=[1]) def test_state_zero_weights(self, qubit_device, n_subsystems, tol): """Checks the state is correct if the weights are zero.""" @@ -83,63 +116,6 @@ def circuit(x=None): assert np.allclose(res, target, atol=tol, rtol=0) - -class TestHyperparameters: - """Tests that the template's hyperparameters have the desired effect.""" - - @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[np.pi / 2]], [0]), - (2, [[1, np.pi / 2, np.pi / 4]], [0, 1 / np.sqrt(2)]), - (3, [[0, 0, 0, np.pi, np.pi / 2, np.pi / 4]], - [-1, 0, 1 / np.sqrt(2)])]) - def test_output_local_field_ry(self, n_subsystems, weights, target, tol): - """Checks the output if the features are zero for RY local fields.""" - - features = np.zeros(shape=(n_subsystems,)) - dev = qml.device('default.qubit', wires=n_subsystems) - - @qml.qnode(dev) - def circuit(x=None): - qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='Y') - return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] - - res = circuit(x=features[:n_subsystems]) - assert np.allclose(res, target, atol=tol, rtol=0) - - @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[np.pi / 2]], [0]), - (2, [[1, np.pi / 2, np.pi / 4]], [0, 1 / np.sqrt(2)]), - (3, [[0, 0, 0, np.pi, np.pi / 2, np.pi / 4]], - [-1, 0, 1 / np.sqrt(2)])]) - def test_output_local_field_rx(self, n_subsystems, weights, target, tol): - """Checks the output if the features are zero for RX local fields.""" - - features = np.zeros(shape=(n_subsystems,)) - dev = qml.device('default.qubit', wires=n_subsystems) - - @qml.qnode(dev) - def circuit(x=None): - qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='X') - return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] - - res = circuit(x=features[:n_subsystems]) - assert np.allclose(res, target, atol=tol, rtol=0) - - @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[np.pi / 2]], [1]), - (2, [[1, np.pi / 2, np.pi / 4]], [1, 1]), - (3, [[0, 0, 0, np.pi, np.pi / 2, np.pi / 4]], [1, 1, 1])]) - def test_output_local_field_rz(self, n_subsystems, weights, target, tol): - """Checks the output if the features are zero for RZ local fields.""" - - features = np.zeros(shape=(n_subsystems,)) - dev = qml.device('default.qubit', wires=n_subsystems) - - @qml.qnode(dev) - def circuit(x=None): - qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='Z') - return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] - - res = circuit(x=features[:n_subsystems]) - assert np.allclose(res, target, atol=tol, rtol=0) - @pytest.mark.parametrize('n_wires, features, weights, target', [(2, [0], [[0, 0, np.pi / 2]], [1, 0]), (3, [0, 0], [[0, 0, 0, 0, 0, np.pi / 2]], [1, 1, 0])]) @@ -156,7 +132,7 @@ def circuit(x=None): res = circuit(x=features) assert np.allclose(res, target, atol=tol, rtol=0) - def test_exception_fewer_wires_than_features(self, ): + def test_exception_fewer_qubits_than_features(self, ): """Verifies that exception raised if there are fewer wires than features.""" @@ -173,22 +149,6 @@ def circuit(x=None): with pytest.raises(ValueError, match="Features must be of "): circuit(x=features) - def test_exception_wrongrot(self): - """Verifies exception raised if the - rotation strategy is unknown.""" - - n_wires = 1 - weights = np.zeros(shape=(1, 1)) - dev = qml.device('default.qubit', wires=n_wires) - - @qml.qnode(dev) - def circuit(x=None): - qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='A') - return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] - - with pytest.raises(ValueError, match="did not recognize"): - circuit(x=[1]) - def test_exception_wrong_dim(self): """Verifies that exception is raised if the number of dimensions of features is incorrect.""" From e5cefff85d732aec949d372a974268f5b98b07eb Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 15 Mar 2021 14:01:59 +0200 Subject: [PATCH 33/64] backup --- pennylane/templates/embeddings/qaoa.py | 49 ++++++++++---------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index 98ca68c6051..b83edf25d03 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -17,8 +17,6 @@ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access import pennylane as qml from pennylane.operation import Operation, AnyWires -from pennylane.ops import RX, RY, RZ, MultiRZ, Hadamard -from pennylane.templates import broadcast from pennylane.wires import Wires @@ -30,18 +28,12 @@ def qaoa_feature_encoding_hamiltonian(features, wires): wires (Wires): wires that the template acts on """ - try: - # works for tensors - n_features = features.shape[0] - except AttributeError: - # works for lists and tuples - n_features = len(features) + n_features = qml.math.shape(features)[0] - feature_encoding_wires = wires[:n_features] - remaining_wires = wires[n_features:] - - broadcast(unitary=RX, pattern="single", wires=feature_encoding_wires, parameters=features) - broadcast(unitary=Hadamard, pattern="single", wires=remaining_wires) + for i in range(n_features): + qml.RX(features[i], wires=wires[i]) + for i in range(n_features, len(wires)): + qml.Hadamard(wires=wires[i]) def qaoa_ising_hamiltonian(weights, wires, local_fields): @@ -54,23 +46,20 @@ def qaoa_ising_hamiltonian(weights, wires, local_fields): """ if len(wires) == 1: - weights_zz = [] - weights_fields = weights + local_fields(weights[0], wires=wires) elif len(wires) == 2: - # for 2 wires the periodic boundary condition is dropped in broadcast's "ring" pattern - # only feed in 1 parameter - weights_zz = weights[:1] - weights_fields = weights[1:] + # deviation for 2 wires: we do not connect last to first qubit + # with the entangling gates + qml.MultiRZ(weights[0], wires=wires.subset([0, 1])) + local_fields(weights[1], wires=wires[0:1]) + local_fields(weights[1], wires=wires[1:2]) else: - weights_zz = weights[: len(wires)] - weights_fields = weights[len(wires) :] - - # zz couplings - broadcast(unitary=MultiRZ, pattern="ring", wires=wires, parameters=weights_zz) - # local fields - broadcast(unitary=local_fields, pattern="single", wires=wires, parameters=weights_fields) + for i in range(len(wires)): + qml.MultiRZ(weights[i], wires=wires.subset([i, i + 1], periodic_boundary=True)) + for i in range(len(wires)): + local_fields(weights[len(wires)+i], wires=wires[i]) class QAOAEmbedding(Operation): @@ -221,13 +210,13 @@ def circuit(weights, f=None): def __init__(self, features, weights, wires, local_field="Y", do_queue=True): if local_field == "Z": - self.local_fields = RZ + self.local_fields = qml.RZ elif local_field == "X": - self.local_fields = RX + self.local_fields = qml.RX elif local_field == "Y": - self.local_fields = RY + self.local_fields = qml.RY else: - raise ValueError(f"Did not recognize local field {local_field}") + raise ValueError(f"did not recognize local field {local_field}") super().__init__(features, weights, wires=wires, do_queue=do_queue) From d49d65bf108c3051a320b79dfdf483472fbc8c21 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 15 Mar 2021 14:30:53 +0200 Subject: [PATCH 34/64] finish qaoa --- pennylane/templates/embeddings/qaoa.py | 67 ++++++++++++++++++-- tests/templates/test_embeddings/test_qaoa.py | 50 ++++++++++++--- tests/test_init.py | 18 ------ 3 files changed, 102 insertions(+), 33 deletions(-) diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index b83edf25d03..bc08adb85a9 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -15,6 +15,7 @@ Contains the ``QAOAEmbedding`` template. """ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access +import numpy as np import pennylane as qml from pennylane.operation import Operation, AnyWires from pennylane.wires import Wires @@ -27,7 +28,6 @@ def qaoa_feature_encoding_hamiltonian(features, wires): features (tensor_like): array of features to encode wires (Wires): wires that the template acts on """ - n_features = qml.math.shape(features)[0] for i in range(n_features): @@ -53,7 +53,7 @@ def qaoa_ising_hamiltonian(weights, wires, local_fields): # with the entangling gates qml.MultiRZ(weights[0], wires=wires.subset([0, 1])) local_fields(weights[1], wires=wires[0:1]) - local_fields(weights[1], wires=wires[1:2]) + local_fields(weights[2], wires=wires[1:2]) else: for i in range(len(wires)): @@ -139,14 +139,12 @@ def circuit(weights, f=None): **Using parameter initialization functions** - The initial weight parameters can alternatively be generated by utility functions from the - ``pennylane.init`` module, for example using the function :func:`~.qaoa_embedding_normal`: + The initial weight parameters can alternatively be generated using the static methods + `QAOAEmbedding.weights_normal` and `QAOAEmbedding.weights_uniform`. .. code-block:: python - from pennylane.init import qaoa_embedding_normal - weights = qaoa_embedding_normal(n_layers=2, n_wires=2, mean=0, std=0.2) - + weights = QAOAEmbedding.weights_normal(n_layers=2, n_wires=2, mean=0, std=0.2) **Training the embedding** @@ -281,3 +279,58 @@ def _preprocess(self): raise ValueError( f"Weights tensor must be of shape {(repeat, 2*len(self.wires))}; got {shape}" ) + + @staticmethod + def weights_uniform(n_layers, n_wires, low=0, high=2 * np.pi, seed=None): + r"""Creates a standard numpy weights array whose entries are drawn from a uniform + distribution. + + Args: + n_layers (int): number of layers + n_wires (int): number of qubits + low (float): minimum value of uniform distribution + high (float): maximum value of uniform distribution + seed (int): seed used in sampling the parameters, makes function call deterministic + + Returns: + array: numpy array + """ + if seed is not None: + np.random.seed(seed) + + if n_wires == 1: + shp = (n_layers, 1) + elif n_wires == 2: + shp = (n_layers, 3) + else: + shp = (n_layers, 2 * n_wires) + + params = np.random.uniform(low=low, high=high, size=shp) + return params + + @staticmethod + def weights_normal(n_layers, n_wires, mean=0, std=0.1, seed=None): + r"""Creates a standard numpy weights array whose entries are drawn from a normal distribution. + + Args: + n_layers (int): number of layers + n_wires (int): number of qubits + mean (float): mean of parameters + std (float): standard deviation of parameters + seed (int): seed used in sampling the parameters, makes function call deterministic + + Returns: + array: numpy array + """ + if seed is not None: + np.random.seed(seed) + + if n_wires == 1: + shp = (n_layers, 1) + elif n_wires == 2: + shp = (n_layers, 3) + else: + shp = (n_layers, 2 * n_wires) + + params = np.random.normal(loc=mean, scale=std, size=shp) + return params diff --git a/tests/templates/test_embeddings/test_qaoa.py b/tests/templates/test_embeddings/test_qaoa.py index cdf5b36bbec..d3dd0f95a6f 100644 --- a/tests/templates/test_embeddings/test_qaoa.py +++ b/tests/templates/test_embeddings/test_qaoa.py @@ -132,6 +132,9 @@ def circuit(x=None): res = circuit(x=features) assert np.allclose(res, target, atol=tol, rtol=0) + +class TestParameters: + def test_exception_fewer_qubits_than_features(self, ): """Verifies that exception raised if there are fewer wires than features.""" @@ -149,30 +152,60 @@ def circuit(x=None): with pytest.raises(ValueError, match="Features must be of "): circuit(x=features) - def test_exception_wrong_dim(self): - """Verifies that exception is raised if the - number of dimensions of features is incorrect.""" + def test_exception_wrong_feauture_shape(self): + """Verifies that exception is raised if the shape of features is incorrect.""" n_wires = 1 weights = np.zeros(shape=(1, 1)) + features = np.zeros(shape=(2, 1)) dev = qml.device('default.qubit', wires=n_wires) @qml.qnode(dev) - def circuit(x=None): - qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires)) + def circuit(): + qml.templates.QAOAEmbedding(features, weights, wires=range(n_wires)) return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] with pytest.raises(ValueError, match="Features must be a one-dimensional"): - circuit(x=[[1], [0]]) + circuit() + + def test_exception_wrong_weight_shape(self): + """Verifies that exception is raised if the shape of weights is incorrect.""" + n_wires = 2 + weights = np.zeros(shape=(1, 4)) + features = np.zeros(shape=(2, )) + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(): + qml.templates.QAOAEmbedding(features, weights, wires=range(n_wires)) + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + with pytest.raises(ValueError, match="Weights tensor must be of shape"): + circuit() + + @pytest.mark.parametrize('n_layers, n_wires, expected_shape', [ + (2, 3, (2, 6)), + (2, 1, (2, 1)), + (2, 2, (2, 3)), + ]) + def test_shape_random_weights(self, n_layers, n_wires, expected_shape): + + weights1 = qml.templates.QAOAEmbedding.weights_uniform(n_layers, n_wires) + weights2 = qml.templates.QAOAEmbedding.weights_normal(n_layers, n_wires) + + assert weights1.shape == expected_shape + assert weights2.shape == expected_shape class TestGradients: """Tests that the gradient is computed correctly in all three interfaces.""" - def circuit(self, features, weights): + @staticmethod + def circuit(features, weights): qml.templates.QAOAEmbedding(features, weights, range(2)) return qml.expval(qml.PauliZ(0)) - def circuit_decomposed(self, features, weights): + @staticmethod + def circuit_decomposed(features, weights): qml.RX(features[0], wires=0) qml.RX(features[1], wires=1) qml.MultiRZ(weights[0, 0], wires=[0, 1]) @@ -276,3 +309,4 @@ def test_torch(self, tol, skip_if_no_torch_support): assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) + diff --git a/tests/test_init.py b/tests/test_init.py index 935329af6d0..12a8de668d3 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -190,24 +190,6 @@ (qml.init.interferometer_varphi_uniform, {'n_wires': 1, 'low': 0, 'high': 1}, (1,)), - (qml.init.qaoa_embedding_normal, - {'n_layers': 2, 'n_wires': 3, 'mean': 0, 'std': 1}, - (2, 2*3)), - (qml.init.qaoa_embedding_uniform, - {'n_layers': 2, 'n_wires': 3, 'low': 0, 'high': 1}, - (2, 2*3)), - (qml.init.qaoa_embedding_uniform, - {'n_layers': 2, 'n_wires': 1, 'low': 0, 'high': 1}, - (2, 1)), - (qml.init.qaoa_embedding_uniform, - {'n_layers': 2, 'n_wires': 2, 'low': 0, 'high': 1}, - (2, 3)), - (qml.init.qaoa_embedding_normal, - {'n_layers': 2, 'n_wires': 1, 'mean': 0, 'std': 1}, - (2, 1)), - (qml.init.qaoa_embedding_normal, - {'n_layers': 2, 'n_wires': 2, 'mean': 0, 'std': 1}, - (2, 3)), (qml.init.simplified_two_design_initial_layer_uniform, {'n_wires': 1, 'low': 0, 'high': 1}, (1,)), From d7d5225848dff1903dc43be3c46084b9849d4037 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 15 Mar 2021 16:07:57 +0200 Subject: [PATCH 35/64] finish basic entangler --- pennylane/templates/embeddings/qaoa.py | 19 +- pennylane/templates/layers/basic_entangler.py | 128 ++++++---- .../test_ansaetze/test_basic_entangler.py | 237 ++++++++++++++++++ tests/templates/test_embeddings/test_qaoa.py | 71 ++++-- tests/templates/test_layers.py | 109 -------- 5 files changed, 377 insertions(+), 187 deletions(-) create mode 100644 tests/templates/test_ansaetze/test_basic_entangler.py diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index bc08adb85a9..6af3b9f9f90 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -1,4 +1,4 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. r""" -Contains the ``QAOAEmbedding`` template. +Contains the QAOAEmbedding template. """ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access import numpy as np @@ -139,7 +139,7 @@ def circuit(weights, f=None): **Using parameter initialization functions** - The initial weight parameters can alternatively be generated using the static methods + A random numpy weights array can be generated using the static methods `QAOAEmbedding.weights_normal` and `QAOAEmbedding.weights_uniform`. .. code-block:: python @@ -208,16 +208,15 @@ def circuit(weights, f=None): def __init__(self, features, weights, wires, local_field="Y", do_queue=True): if local_field == "Z": - self.local_fields = qml.RZ + self.local_field = qml.RZ elif local_field == "X": - self.local_fields = qml.RX + self.local_field = qml.RX elif local_field == "Y": - self.local_fields = qml.RY + self.local_field = qml.RY else: raise ValueError(f"did not recognize local field {local_field}") super().__init__(features, weights, wires=wires, do_queue=do_queue) - self._preprocess() def expand(self): @@ -234,7 +233,7 @@ def expand(self): for l in range(repeat): # apply alternating Hamiltonians qaoa_feature_encoding_hamiltonian(features, self.wires) - qaoa_ising_hamiltonian(weights[l], self.wires, self.local_fields) + qaoa_ising_hamiltonian(weights[l], self.wires, self.local_field) # repeat the feature encoding once more at the end qaoa_feature_encoding_hamiltonian(features, self.wires) @@ -250,8 +249,8 @@ def _preprocess(self): * Check that the shape of the weights tensor is correct for the number of qubits. """ - features = self.data[0] - weights = self.data[1] + features = self.parameters[0] + weights = self.parameters[1] shape = qml.math.shape(features) diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 8d65c821a94..1a3b15876c4 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -1,4 +1,4 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,38 +14,13 @@ r""" Contains the ``BasicEntanglerLayers`` template. """ -# pylint: disable-msg=too-many-branches,too-many-arguments,protected-access +import numpy as np import pennylane as qml from pennylane.operation import Operation, AnyWires -from pennylane.ops import CNOT, RX from pennylane.templates import broadcast from pennylane.wires import Wires -def _preprocess(weights, wires): - """Validate and pre-process inputs as follows: - - * Check the shape of the weights tensor, making sure that the second dimension - has length :math:`n`, where :math:`n` is the number of qubits. - - Args: - weights (tensor_like): trainable parameters of the template - wires (Wires): wires that template acts on - - Returns: - int: number of times that the ansatz is repeated - """ - shape = qml.math.shape(weights) - - if len(shape) != 2: - raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") - - if shape[1] != len(wires): - raise ValueError( - f"Weights tensor must have second dimension of length {len(wires)}; got {shape[1]}" - ) - - class BasicEntanglerLayers(Operation): r"""Layers consisting of one-parameter single-qubit rotations on each qubit, followed by a closed chain or *ring* of CNOT gates. @@ -106,18 +81,12 @@ def circuit(weights): **Parameter initialization function** - The :mod:`~pennylane.init` module has two parameter initialization functions, ``basic_entangler_layers_normal`` - and ``basic_entangler_layers_uniform``. + A random numpy weights array can be generated using the static methods + `BasicEntanglerLayers.weights_normal` and `BasicEntanglerLayers.weights_uniform`. .. code-block:: python - from pennylane.init import basic_entangler_layers_normal - - n_layers = 4 - weights = basic_entangler_layers_normal(n_layers=n_layers, n_wires=n_wires) - - circuit(weights) - + weights = BasicEntanglerLayers.weights_normal(n_layers=2, n_wires=2, mean=0, std=0.2) **No periodic boundary for two wires** @@ -158,19 +127,90 @@ def circuit(weights): par_domain = "A" def __init__(self, weights, wires=None, rotation=None, do_queue=True): - wires = Wires(wires) - _preprocess(weights, wires) - self.rotation = rotation or RX + + self.rotation = rotation or qml.RX + super().__init__(weights, wires=wires, do_queue=do_queue) + self._preprocess() + + def expand(self): - def decomposition(self, weights, wires): + weights = self.data[0] # first dimension of the weights tensor determines # the number of layers repeat = qml.math.shape(weights)[0] - for layer in range(repeat): - broadcast( - unitary=self.rotation, pattern="single", wires=wires, parameters=weights[layer] + with qml.tape.QuantumTape() as tape: + + for layer in range(repeat): + for i in range(len(self.wires)): + self.rotation(weights[layer, i], wires=self.wires[i:i+1]) + + broadcast(unitary=qml.CNOT, pattern="ring", wires=self.wires) + + return tape + + def _preprocess(self): + """Validate and pre-process inputs as follows: + + * Check the shape of the weights tensor, making sure that the second dimension + has length :math:`n`, where :math:`n` is the number of qubits. + + Args: + weights (tensor_like): trainable parameters of the template + wires (Wires): wires that template acts on + """ + shape = qml.math.shape(self.parameters[0]) + + if len(shape) != 2: + raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") + + if shape[1] != len(self.wires): + raise ValueError( + f"Weights tensor must have second dimension of length {len(self.wires)}; got {shape[1]}" ) - broadcast(unitary=CNOT, pattern="ring", wires=wires) + + @staticmethod + def weights_normal(n_layers, n_wires, mean=0, std=0.1, seed=None): + r"""Creates a standard numpy weights array whose entries are drawn from a normal + distribution. + + Args: + n_layers (int): number of layers + n_wires (int): number of qubits + mean (float): mean of parameters + std (float): standard deviation of parameters + seed (int): seed used in sampling the parameters, makes function call deterministic + + Returns: + array: weights array + """ + if seed is not None: + np.random.seed(seed) + + params = np.random.normal(loc=mean, scale=std, size=(n_layers, n_wires)) + + return params + + @staticmethod + def weights_uniform(n_layers, n_wires, low=0, high=2 * np.pi, seed=None): + r"""Creates a standard numpy weights array whose entries are drawn from a uniform + distribution. + + Args: + n_layers (int): number of layers + n_wires (int): number of qubits + low (float): minimum value of uniform distribution + high (float): maximum value of uniform distribution + seed (int): seed used in sampling the parameters, makes function call deterministic + + Returns: + array: weights array + """ + if seed is not None: + np.random.seed(seed) + + params = np.random.uniform(low=low, high=high, size=(n_layers, n_wires)) + + return params diff --git a/tests/templates/test_ansaetze/test_basic_entangler.py b/tests/templates/test_ansaetze/test_basic_entangler.py new file mode 100644 index 00000000000..0febf4df543 --- /dev/null +++ b/tests/templates/test_ansaetze/test_basic_entangler.py @@ -0,0 +1,237 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests for the BasicEntanglerLayers template. +""" +import pytest +import numpy as np +import pennylane as qml + + +def circuit_template(weights): + qml.templates.BasicEntanglerLayers(weights, range(3)) + return qml.expval(qml.PauliZ(0)) + + +def circuit_decomposed(weights): + qml.RX(weights[0, 0], wires=0) + qml.RX(weights[0, 1], wires=1) + qml.RX(weights[0, 2], wires=2) + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=[1, 2]) + qml.CNOT(wires=[2, 0]) + return qml.expval(qml.PauliZ(0)) + + +class TestDecomposition: + """Tests that the template defines the correct decomposition.""" + + QUEUES = [(1, (1, 1), ['RX']), + (2, (1, 2), ['RX', 'RX', 'CNOT', 'RY', 'RY', 'RX', 'RX']), + (2, (2, 2), ['RX', 'RX', 'CNOT', 'RX', 'RX', 'CNOT']), + (3, (1, 3), ['RX', 'RX', 'RX', 'CNOT', 'CNOT', 'CNOT'])] + + @pytest.mark.parametrize('n_wires, weight_shape, expected_names', QUEUES) + def test_expansion(self, n_wires, weight_shape, expected_names): + """Checks the queue for the default settings.""" + + weights = np.random.random(size=weight_shape) + + op = qml.templates.BasicEntanglerLayers(weights, wires=range(n_wires)) + tape = op.expand() + + for i, gate in enumerate(tape.operations): + assert gate.name == expected_names[i] + + @pytest.mark.parametrize('rotation', [qml.RY, qml.RZ]) + def test_rotation(self, rotation): + """Checks that custom rotation gate is used.""" + + weights = np.zeros(shape=(1, 2)) + + op = qml.templates.BasicEntanglerLayers(weights, wires=range(2), rotation=rotation) + tape = op.expand() + + assert type(tape.operations[0]) == rotation + assert type(tape.operations[1]) == rotation + + @pytest.mark.parametrize( + "weights, n_wires, target", + [ + ([[np.pi]], 1, [-1]), + ([[np.pi] * 2], 2, [-1, 1]), + ([[np.pi] * 3], 3, [1, 1, -1]), + ([[np.pi] * 4], 4, [-1, 1, -1, 1]), + ], + ) + def test_simple_target_outputs(self, weights, n_wires, target): + """Tests the result of the template for simple cases.""" + + dev = qml.device("default.qubit", wires=n_wires) + + @qml.qnode(dev) + def circuit(weights): + qml.templates.BasicEntanglerLayers(weights, wires=range(n_wires)) + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + expectations = circuit(weights) + for exp, target_exp in zip(expectations, target): + assert exp == target_exp + + def test_custom_wire_labels(self, tol): + """Test that template can deal with non-numeric, nonconsecutive wire labels.""" + weights = np.random.random(size=(1, 3)) + + dev = qml.device('default.qubit', wires=3) + dev2 = qml.device('default.qubit', wires=['z', 'a', 'k']) + + @qml.qnode(dev) + def circuit(): + qml.templates.BasicEntanglerLayers(weights, wires=range(3)) + return qml.expval(qml.Identity(0)) + + @qml.qnode(dev2) + def circuit2(): + qml.templates.BasicEntanglerLayers(weights, wires=['z', 'a', 'k']) + return qml.expval(qml.Identity('z')) + + circuit() + circuit2() + + assert np.allclose(dev.state, dev2.state, atol=tol, rtol=0) + + +class TestParameters: + + def test_exception_wrong_dim(self): + """Verifies that exception is raised if the weights shape is incorrect.""" + + n_wires = 1 + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(weights): + qml.templates.BasicEntanglerLayers(weights=weights, wires=range(n_wires)) + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + with pytest.raises(ValueError, match="Weights tensor must be 2-dimensional"): + circuit([1, 0]) + + with pytest.raises(ValueError, match="Weights tensor must have second dimension of length"): + circuit([[1, 0], [1, 0]]) + + @pytest.mark.parametrize('n_layers, n_wires, expected_shape', [ + (2, 3, (2, 3)), + (2, 1, (2, 1)), + (2, 2, (2, 2)), + ]) + def test_shape_random_weights(self, n_layers, n_wires, expected_shape): + + weights1 = qml.templates.BasicEntanglerLayers.weights_uniform(n_layers, n_wires) + weights2 = qml.templates.BasicEntanglerLayers.weights_normal(n_layers, n_wires) + + assert weights1.shape == expected_shape + assert weights2.shape == expected_shape + + +class TestGradients: + """Tests that the gradient is computed correctly in all three interfaces.""" + + def test_autograd(self, tol): + """Tests that gradients of template and decomposed circuit + are the same in the autograd interface.""" + + weights = np.random.random(size=(1, 3)) + + dev = qml.device('default.qubit', wires=3) + + circuit = qml.QNode(circuit_template, dev) + circuit2 = qml.QNode(circuit_decomposed, dev) + + grad_fn = qml.grad(circuit) + grads = grad_fn(weights) + + grad_fn2 = qml.grad(circuit2) + grads2 = grad_fn2(weights) + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) + + def test_jax(self, tol, skip_if_no_jax_support): + """Tests that gradients of template and decomposed circuit + are the same in the jax interface.""" + + import jax + import jax.numpy as jnp + + weights = jnp.array(np.random.random(size=(1, 3))) + + dev = qml.device('default.qubit', wires=3) + + circuit = qml.QNode(circuit_template, dev, interface='jax') + circuit2 = qml.QNode(circuit_decomposed, dev, interface='jax') + + grad_fn = jax.grad(circuit) + grads = grad_fn(weights) + + grad_fn2 = jax.grad(circuit2) + grads2 = grad_fn2(weights) + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) + + def test_tf(self, tol, skip_if_no_tf_support): + """Tests that gradients of template and decomposed circuit + are the same in the tf interface.""" + + import tensorflow as tf + + weights = tf.Variable(np.random.random(size=(1, 3))) + + dev = qml.device('default.qubit', wires=3) + + circuit = qml.QNode(circuit_template, dev, interface='tf') + circuit2 = qml.QNode(circuit_decomposed, dev, interface='tf') + + with tf.GradientTape() as tape: + res = circuit(weights) + grads = tape.gradient(res, [weights]) + + with tf.GradientTape() as tape2: + res2 = circuit2(weights) + grads2 = tape2.gradient(res2, [weights]) + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) + + def test_torch(self, tol, skip_if_no_torch_support): + """Tests that gradients of template and decomposed circuit + are the same in the torch interface.""" + + import torch + + weights = torch.tensor(np.random.random(size=(1, 3)), requires_grad=True) + + dev = qml.device('default.qubit', wires=3) + + circuit = qml.QNode(circuit_template, dev, interface='torch') + circuit2 = qml.QNode(circuit_decomposed, dev, interface='torch') + + res = circuit(weights) + res.backward() + grads = [weights.grad] + + res2 = circuit2(weights) + res2.backward() + grads2 = [weights.grad] + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) + diff --git a/tests/templates/test_embeddings/test_qaoa.py b/tests/templates/test_embeddings/test_qaoa.py index d3dd0f95a6f..812a8260ae9 100644 --- a/tests/templates/test_embeddings/test_qaoa.py +++ b/tests/templates/test_embeddings/test_qaoa.py @@ -19,6 +19,22 @@ import pennylane as qml +def circuit_template(features, weights): + qml.templates.QAOAEmbedding(features, weights, range(2)) + return qml.expval(qml.PauliZ(0)) + + +def circuit_decomposed(features, weights): + qml.RX(features[0], wires=0) + qml.RX(features[1], wires=1) + qml.MultiRZ(weights[0, 0], wires=[0, 1]) + qml.RY(weights[0, 1], wires=0) + qml.RY(weights[0, 2], wires=1) + qml.RX(features[0], wires=0) + qml.RX(features[1], wires=1) + return qml.expval(qml.PauliZ(0)) + + class TestDecomposition: """Tests that the template defines the correct decomposition.""" @@ -132,6 +148,29 @@ def circuit(x=None): res = circuit(x=features) assert np.allclose(res, target, atol=tol, rtol=0) + def test_custom_wire_labels(self, tol): + """Test that template can deal with non-numeric, nonconsecutive wire labels.""" + weights = np.random.random(size=(1, 6)) + features = np.random.random(size=(3, )) + + dev = qml.device('default.qubit', wires=3) + dev2 = qml.device('default.qubit', wires=['z', 'a', 'k']) + + @qml.qnode(dev) + def circuit(): + qml.templates.QAOAEmbedding(features, weights, wires=range(3)) + return qml.expval(qml.Identity(0)) + + @qml.qnode(dev2) + def circuit2(): + qml.templates.QAOAEmbedding(features, weights, wires=['z', 'a', 'k']) + return qml.expval(qml.Identity('z')) + + circuit() + circuit2() + + assert np.allclose(dev.state, dev2.state, atol=tol, rtol=0) + class TestParameters: @@ -199,22 +238,6 @@ def test_shape_random_weights(self, n_layers, n_wires, expected_shape): class TestGradients: """Tests that the gradient is computed correctly in all three interfaces.""" - @staticmethod - def circuit(features, weights): - qml.templates.QAOAEmbedding(features, weights, range(2)) - return qml.expval(qml.PauliZ(0)) - - @staticmethod - def circuit_decomposed(features, weights): - qml.RX(features[0], wires=0) - qml.RX(features[1], wires=1) - qml.MultiRZ(weights[0, 0], wires=[0, 1]) - qml.RY(weights[0, 1], wires=0) - qml.RY(weights[0, 2], wires=1) - qml.RX(features[0], wires=0) - qml.RX(features[1], wires=1) - return qml.expval(qml.PauliZ(0)) - def test_autograd(self, tol): """Tests that gradients of template and decomposed circuit are the same in the autograd interface.""" @@ -224,8 +247,8 @@ def test_autograd(self, tol): dev = qml.device('default.qubit', wires=2) - circuit = qml.QNode(self.circuit, dev) - circuit2 = qml.QNode(self.circuit_decomposed, dev) + circuit = qml.QNode(circuit_template, dev) + circuit2 = qml.QNode(circuit_decomposed, dev) grad_fn = qml.grad(circuit) grads = grad_fn(features, weights) @@ -248,8 +271,8 @@ def test_jax(self, tol, skip_if_no_jax_support): dev = qml.device('default.qubit', wires=2) - circuit = qml.QNode(self.circuit, dev, interface='jax') - circuit2 = qml.QNode(self.circuit_decomposed, dev, interface='jax') + circuit = qml.QNode(circuit_template, dev, interface='jax') + circuit2 = qml.QNode(circuit_decomposed, dev, interface='jax') grad_fn = jax.grad(circuit) grads = grad_fn(features, weights) @@ -271,8 +294,8 @@ def test_tf(self, tol, skip_if_no_tf_support): dev = qml.device('default.qubit', wires=2) - circuit = qml.QNode(self.circuit, dev, interface='tf') - circuit2 = qml.QNode(self.circuit_decomposed, dev, interface='tf') + circuit = qml.QNode(circuit_template, dev, interface='tf') + circuit2 = qml.QNode(circuit_decomposed, dev, interface='tf') with tf.GradientTape() as tape: res = circuit(features, weights) @@ -296,8 +319,8 @@ def test_torch(self, tol, skip_if_no_torch_support): dev = qml.device('default.qubit', wires=2) - circuit = qml.QNode(self.circuit, dev, interface='torch') - circuit2 = qml.QNode(self.circuit_decomposed, dev, interface='torch') + circuit = qml.QNode(circuit_template, dev, interface='torch') + circuit2 = qml.QNode(circuit_decomposed, dev, interface='torch') res = circuit(features, weights) res.backward() diff --git a/tests/templates/test_layers.py b/tests/templates/test_layers.py index a98838d9e26..6a2f766c75f 100644 --- a/tests/templates/test_layers.py +++ b/tests/templates/test_layers.py @@ -610,115 +610,6 @@ def circuit(initial_layer, weights): circuit(initial_layer, weights) -class TestBasicEntangler: - """Tests for the BasicEntanglerLayers method from the pennylane.templates.layers module.""" - - @pytest.mark.parametrize("n_wires, n_cnots", [(1, 0), (2, 1), (3, 3), (4, 4)]) - def test_circuit_queue(self, n_wires, n_cnots): - """Tests the gate types in the circuit.""" - np.random.seed(42) - n_layers = 2 - - weights = np.random.randn(n_layers, n_wires) - - op = BasicEntanglerLayers(weights, wires=range(n_wires)) - - with QuantumTape() as tape: - op.decomposition(weights, wires=range(n_wires)) - - # Test that gates appear in the right order - exp_gates = [qml.RX] * n_wires + [qml.CNOT] * n_cnots - exp_gates *= n_layers - - res_gates = [type(op) for op in tape.operations] - - for op1, op2 in zip(res_gates, exp_gates): - assert op1 == op2 - - @pytest.mark.parametrize("n_wires, n_cnots", [(1, 0), (2, 1), (3, 3), (4, 4)]) - def test_circuit_parameters(self, n_wires, n_cnots): - """Tests the parameter values in the circuit.""" - np.random.seed(42) - n_layers = 2 - - weights = np.random.randn(n_layers, n_wires) - - op = BasicEntanglerLayers(weights, wires=range(n_wires)) - - with QuantumTape() as tape: - op.decomposition(weights, wires=range(n_wires)) - - # test the device parameters - for l in range(n_layers): - # only select the rotation gates - layer_ops = tape.operations[l * (n_wires + n_cnots) : l * (n_wires + n_cnots) + n_wires] - - # check each rotation gate parameter - for n in range(n_wires): - res_param = layer_ops[n].parameters[0] - exp_param = weights[l, n] - assert res_param == exp_param - - @pytest.mark.parametrize("rotation", [RX, RY, RZ]) - def test_custom_rotation(self, rotation): - """Tests that non-default rotation gates are used correctly.""" - n_layers = 2 - n_wires = 4 - weights = np.ones(shape=(n_layers, n_wires)) - - op = BasicEntanglerLayers(weights, wires=range(n_wires), rotation=rotation) - - with QuantumTape() as tape: - op.decomposition(weights, wires=range(n_wires)) - - # assert queue contains the custom rotations and CNOTs only - gates = tape.operations - for op in gates: - if not type(op) == CNOT: - assert type(op) == rotation - - @pytest.mark.parametrize( - "weights, n_wires, target", - [ - ([[np.pi]], 1, [-1]), - ([[np.pi] * 2], 2, [-1, 1]), - ([[np.pi] * 3], 3, [1, 1, -1]), - ([[np.pi] * 4], 4, [-1, 1, -1, 1]), - ], - ) - def test_simple_target_outputs(self, weights, n_wires, target): - """Tests the result of the template for simple cases.""" - - dev = qml.device("default.qubit", wires=n_wires) - - @qml.qnode(dev) - def circuit(weights): - BasicEntanglerLayers(weights=weights, wires=range(n_wires), rotation=RX) - return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_wires)] - - expectations = circuit(weights) - for exp, target_exp in zip(expectations, target): - assert exp == target_exp - - def test_exception_wrong_dim(self): - """Verifies that exception is raised if the - number of dimensions of features is incorrect.""" - - n_wires = 1 - dev = qml.device('default.qubit', wires=n_wires) - - @qml.qnode(dev) - def circuit(weights): - BasicEntanglerLayers(weights=weights, wires=range(n_wires)) - return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] - - with pytest.raises(ValueError, match="Weights tensor must be 2-dimensional"): - circuit([1, 0]) - - with pytest.raises(ValueError, match="Weights tensor must have second dimension of length"): - circuit([[1, 0], [1, 0]]) - - class TestParticleConservingU2: """Tests for the ParticleConservingU2 template from the pennylane.templates.layers module.""" From 988d3fc7d111f7d5f02d5fb140238c33f613e702 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Tue, 16 Mar 2021 09:14:48 +0200 Subject: [PATCH 36/64] update double-excitation --- pennylane/templates/layers/basic_entangler.py | 4 - .../subroutines/double_excitation_unitary.py | 111 ++--- tests/templates/test_embeddings/test_qaoa.py | 1 + .../test_qchem/test_double_excitation.py | 443 ++++++++++++++++++ tests/templates/test_subroutines.py | 214 --------- 5 files changed, 492 insertions(+), 281 deletions(-) create mode 100644 tests/templates/test_qchem/test_double_excitation.py diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 1a3b15876c4..9631dd47742 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -156,10 +156,6 @@ def _preprocess(self): * Check the shape of the weights tensor, making sure that the second dimension has length :math:`n`, where :math:`n` is the number of qubits. - - Args: - weights (tensor_like): trainable parameters of the template - wires (Wires): wires that template acts on """ shape = qml.math.shape(self.parameters[0]) diff --git a/pennylane/templates/subroutines/double_excitation_unitary.py b/pennylane/templates/subroutines/double_excitation_unitary.py index 7d0bcf0355d..551c4faede5 100644 --- a/pennylane/templates/subroutines/double_excitation_unitary.py +++ b/pennylane/templates/subroutines/double_excitation_unitary.py @@ -23,32 +23,6 @@ from pennylane.wires import Wires -def _preprocess(weight, wires1, wires2): - """Validate and pre-process inputs as follows: - - * Check the shape of the weights tensor. - * Check that both wire sets have at least 2 wires. - - Args: - weight (tensor_like): trainable parameters of the template - wires1 (Wires): first set of wires - wires2 (Wires): second set of wires - """ - - if len(wires1) < 2: - raise ValueError( - "expected at least two wires representing the occupied orbitals; " - "got {}".format(len(wires1)) - ) - if len(wires2) < 2: - raise ValueError( - "expected at least two wires representing the unoccupied orbitals; " - "got {}".format(len(wires2)) - ) - - shape = qml.math.shape(weight) - if shape != (): - raise ValueError(f"Weight must be a scalar; got shape {shape}.") def _layer1(weight, s, r, q, p, set_cnot_wires): @@ -514,47 +488,58 @@ def circuit(weight, wires1=None, wires2=None): def __init__(self, weight, wires1=None, wires2=None, do_queue=True): self.wires1 = Wires(wires1) self.wires2 = Wires(wires2) - _preprocess(weight, wires1, wires2) - # remember so we can split wires in decomposition method - self.len_wires1 = len(wires1) - wires = self.wires1 + self.wires2 - super().__init__(weight, wires=wires, do_queue=do_queue) - - def decomposition(self, weight, wires): - # split wires back into two sets - wires1 = wires[: self.len_wires1] - wires2 = wires[self.len_wires1 :] - s = wires1[0] - r = wires1[-1] - q = wires2[0] - p = wires2[-1] + wires = wires1 + wires2 - # Sequence of the wires entering the CNOTs - cnots_occ = [wires1.subset([l, l + 1]) for l in range(len(wires1) - 1)] - cnots_unocc = [wires2.subset([l, l + 1]) for l in range(len(wires2) - 1)] - - set_cnot_wires = cnots_occ + [Wires([r, q])] + cnots_unocc - - # Apply the first layer - _layer1(weight, s, r, q, p, set_cnot_wires) - - # Apply the second layer - _layer2(weight, s, r, q, p, set_cnot_wires) + super().__init__(weight, wires=wires, do_queue=do_queue) + self._preprocess() - # Apply the third layer - _layer3(weight, s, r, q, p, set_cnot_wires) + def expand(self): - # Apply the fourth layer - _layer4(weight, s, r, q, p, set_cnot_wires) + weight = self.parameters[0] - # Apply the fifth layer - _layer5(weight, s, r, q, p, set_cnot_wires) + s = self.wires1[0] + r = self.wires1[-1] + q = self.wires2[0] + p = self.wires2[-1] - # Apply the sixth layer - _layer6(weight, s, r, q, p, set_cnot_wires) + # Sequence of the wires entering the CNOTs + cnots_occ = [self.wires1.subset([l, l + 1]) for l in range(len(self.wires1) - 1)] + cnots_unocc = [self.wires2.subset([l, l + 1]) for l in range(len(self.wires2) - 1)] - # Apply the seventh layer - _layer7(weight, s, r, q, p, set_cnot_wires) + set_cnot_wires = cnots_occ + [Wires([r, q])] + cnots_unocc - # Apply the eighth layer - _layer8(weight, s, r, q, p, set_cnot_wires) + with qml.tape.QuantumTape() as tape: + + _layer1(weight, s, r, q, p, set_cnot_wires) + _layer2(weight, s, r, q, p, set_cnot_wires) + _layer3(weight, s, r, q, p, set_cnot_wires) + _layer4(weight, s, r, q, p, set_cnot_wires) + _layer5(weight, s, r, q, p, set_cnot_wires) + _layer6(weight, s, r, q, p, set_cnot_wires) + _layer7(weight, s, r, q, p, set_cnot_wires) + _layer8(weight, s, r, q, p, set_cnot_wires) + + return tape + + def _preprocess(self): + """Validate and pre-process inputs as follows: + + * Check the shape of the weights tensor. + * Check that both wire sets have at least 2 wires. + """ + weight = self.parameters[0] + + if len(self.wires1) < 2: + raise ValueError( + "expected at least two wires representing the occupied orbitals; " + "got {}".format(len(self.wires1)) + ) + if len(self.wires2) < 2: + raise ValueError( + "expected at least two wires representing the unoccupied orbitals; " + "got {}".format(len(self.wires2)) + ) + + shape = qml.math.shape(weight) + if shape != (): + raise ValueError(f"Weight must be a scalar; got shape {shape}.") diff --git a/tests/templates/test_embeddings/test_qaoa.py b/tests/templates/test_embeddings/test_qaoa.py index 812a8260ae9..6d51e056af5 100644 --- a/tests/templates/test_embeddings/test_qaoa.py +++ b/tests/templates/test_embeddings/test_qaoa.py @@ -173,6 +173,7 @@ def circuit2(): class TestParameters: + """Test inputs and pre-processing.""" def test_exception_fewer_qubits_than_features(self, ): """Verifies that exception raised if there are fewer diff --git a/tests/templates/test_qchem/test_double_excitation.py b/tests/templates/test_qchem/test_double_excitation.py new file mode 100644 index 00000000000..28f5e624e32 --- /dev/null +++ b/tests/templates/test_qchem/test_double_excitation.py @@ -0,0 +1,443 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests for the QAOAEmbedding template. +""" +import pytest +import numpy as np +import pennylane as qml + + +def circuit_template(features, weights): + qml.templates.QAOAEmbedding(features, weights, range(2)) + return qml.expval(qml.PauliZ(0)) + + +def circuit_decomposed(features, weights): + qml.RX(features[0], wires=0) + qml.RX(features[1], wires=1) + qml.MultiRZ(weights[0, 0], wires=[0, 1]) + qml.RY(weights[0, 1], wires=0) + qml.RY(weights[0, 2], wires=1) + qml.RX(features[0], wires=0) + qml.RX(features[1], wires=1) + return qml.expval(qml.PauliZ(0)) + + +class TestDecomposition: + """Tests that the template defines the correct decomposition.""" + + @pytest.mark.parametrize( + ("wires1", "wires2", "ref_gates"), + [ + ( + [0, 1, 2], + [4, 5, 6], + [ + [0, qml.Hadamard, [0], []], + [1, qml.Hadamard, [2], []], + [2, qml.RX, [4], [-np.pi / 2]], + [3, qml.Hadamard, [6], []], + [9, qml.RZ, [6], [np.pi / 24]], + [15, qml.Hadamard, [0], []], + [16, qml.Hadamard, [2], []], + [17, qml.RX, [4], [np.pi / 2]], + [18, qml.Hadamard, [6], []], + ], + ), + ( + [0, 1], + [4, 5], + [ + [15, qml.RX, [0], [-np.pi / 2]], + [16, qml.Hadamard, [1], []], + [17, qml.RX, [4], [-np.pi / 2]], + [18, qml.RX, [5], [-np.pi / 2]], + [22, qml.RZ, [5], [np.pi / 24]], + [26, qml.RX, [0], [np.pi / 2]], + [27, qml.Hadamard, [1], []], + [28, qml.RX, [4], [np.pi / 2]], + [29, qml.RX, [5], [np.pi / 2]], + ], + ), + ( + [1, 2, 3], + [7, 8, 9, 10, 11], + [ + [46, qml.Hadamard, [1], []], + [47, qml.RX, [3], [-np.pi / 2]], + [48, qml.RX, [7], [-np.pi / 2]], + [49, qml.RX, [11], [-np.pi / 2]], + [57, qml.RZ, [11], [np.pi / 24]], + [65, qml.Hadamard, [1], []], + [66, qml.RX, [3], [np.pi / 2]], + [67, qml.RX, [7], [np.pi / 2]], + [68, qml.RX, [11], [np.pi / 2]], + ], + ), + ( + [2, 3, 4], + [8, 9, 10], + [ + [57, qml.Hadamard, [2], []], + [58, qml.Hadamard, [4], []], + [59, qml.Hadamard, [8], []], + [60, qml.RX, [10], [-np.pi / 2]], + [66, qml.RZ, [10], [np.pi / 24]], + [72, qml.Hadamard, [2], []], + [73, qml.Hadamard, [4], []], + [74, qml.Hadamard, [8], []], + [75, qml.RX, [10], [np.pi / 2]], + ], + ), + ( + [3, 4, 5], + [11, 12, 13, 14, 15], + [ + [92, qml.RX, [3], [-np.pi / 2]], + [93, qml.Hadamard, [5], []], + [94, qml.Hadamard, [11], []], + [95, qml.Hadamard, [15], []], + [103, qml.RZ, [15], [-np.pi / 24]], + [111, qml.RX, [3], [np.pi / 2]], + [112, qml.Hadamard, [5], []], + [113, qml.Hadamard, [11], []], + [114, qml.Hadamard, [15], []], + ], + ), + ( + [4, 5, 6, 7], + [9, 10], + [ + [95, qml.Hadamard, [4], []], + [96, qml.RX, [7], [-np.pi / 2]], + [97, qml.Hadamard, [9], []], + [98, qml.Hadamard, [10], []], + [104, qml.RZ, [10], [-np.pi / 24]], + [110, qml.Hadamard, [4], []], + [111, qml.RX, [7], [np.pi / 2]], + [112, qml.Hadamard, [9], []], + [113, qml.Hadamard, [10], []], + ], + ), + ( + [5, 6], + [10, 11, 12], + [ + [102, qml.RX, [5], [-np.pi / 2]], + [103, qml.RX, [6], [-np.pi / 2]], + [104, qml.RX, [10], [-np.pi / 2]], + [105, qml.Hadamard, [12], []], + [110, qml.RZ, [12], [-np.pi / 24]], + [115, qml.RX, [5], [np.pi / 2]], + [116, qml.RX, [6], [np.pi / 2]], + [117, qml.RX, [10], [np.pi / 2]], + [118, qml.Hadamard, [12], []], + ], + ), + ( + [3, 4, 5, 6], + [17, 18, 19], + [ + [147, qml.RX, [3], [-np.pi / 2]], + [148, qml.RX, [6], [-np.pi / 2]], + [149, qml.Hadamard, [17], []], + [150, qml.RX, [19], [-np.pi / 2]], + [157, qml.RZ, [19], [-np.pi / 24]], + [164, qml.RX, [3], [np.pi / 2]], + [165, qml.RX, [6], [np.pi / 2]], + [166, qml.Hadamard, [17], []], + [167, qml.RX, [19], [np.pi / 2]], + ], + ), + ( + [6, 7], + [8, 9], + [ + [4, qml.CNOT, [6, 7], []], + [5, qml.CNOT, [7, 8], []], + [6, qml.CNOT, [8, 9], []], + [8, qml.CNOT, [8, 9], []], + [9, qml.CNOT, [7, 8], []], + [10, qml.CNOT, [6, 7], []], + ], + ), + ( + [4, 5, 6, 7], + [8, 9, 10, 11, 12, 13], + [ + [58, qml.CNOT, [4, 5], []], + [59, qml.CNOT, [5, 6], []], + [60, qml.CNOT, [6, 7], []], + [61, qml.CNOT, [7, 8], []], + [62, qml.CNOT, [8, 9], []], + [63, qml.CNOT, [9, 10], []], + [64, qml.CNOT, [10, 11], []], + [65, qml.CNOT, [11, 12], []], + [66, qml.CNOT, [12, 13], []], + [122, qml.CNOT, [12, 13], []], + [123, qml.CNOT, [11, 12], []], + [124, qml.CNOT, [10, 11], []], + [125, qml.CNOT, [9, 10], []], + [126, qml.CNOT, [8, 9], []], + [127, qml.CNOT, [7, 8], []], + [128, qml.CNOT, [6, 7], []], + [129, qml.CNOT, [5, 6], []], + [130, qml.CNOT, [4, 5], []], + ], + ), + ], + ) + def test_double_ex_unitary_operations(self, wires1, wires2, ref_gates): + """Test the correctness of the DoubleExcitationUnitary template including the gate count + and order, the wires each operation acts on and the correct use of parameters + in the circuit.""" + + sqg = 72 + cnots = 16 * (len(wires1) - 1 + len(wires2) - 1 + 1) + weight = np.pi / 3 + + op = qml.templates.DoubleExcitationUnitary(weight, wires1=wires1, wires2=wires2) + tape = op.expand() + queue = tape.operations + + assert len(queue) == sqg + cnots + + for gate in ref_gates: + idx = gate[0] + + exp_gate = gate[1] + res_gate = queue[idx] + assert type(res_gate) == exp_gate + + exp_wires = gate[2] + res_wires = queue[idx]._wires + assert res_wires == qml.wires.Wires(exp_wires) + + exp_weight = gate[3] + res_weight = queue[idx].parameters + assert res_weight == exp_weight + + +class TestParameters: + """Test inputs and pre-processing.""" + + @pytest.mark.parametrize( + ("weight", "wires1", "wires2", "msg_match"), + [ + (0.2, [0], [1, 2], "expected at least two wires representing the occupied"), + (0.2, [0, 1], [2], "expected at least two wires representing the unoccupied"), + (0.2, [0], [1], "expected at least two wires representing the occupied"), + ([0.2, 1.1], [0, 2], [4, 6], "Weight must be a scalar"), + ], + ) + def test_double_excitation_unitary_exceptions(self, weight, wires1, wires2, msg_match): + """Test that DoubleExcitationUnitary throws an exception if ``weight`` or + ``pphh`` parameter has illegal shapes, types or values.""" + dev = qml.device("default.qubit", wires=10) + + def circuit(weight=weight): + qml.templates.DoubleExcitationUnitary(weight=weight, wires1=wires1, wires2=wires2) + return qml.expval(qml.PauliZ(0)) + + qnode = qml.QNode(circuit, dev) + + with pytest.raises(ValueError, match=msg_match): + qnode(weight=weight) + + def test_custom_wire_labels(self, tol): + """Test that template can deal with non-numeric, nonconsecutive wire labels.""" + weight = np.random.random() + + dev = qml.device('default.qubit', wires=4) + dev2 = qml.device('default.qubit', wires=['z', 'a', 'k', 'b']) + + @qml.qnode(dev) + def circuit(): + qml.templates.DoubleExcitationUnitary(weight, wires1=[0, 3], wires2=[2, 1]) + return qml.expval(qml.Identity(0)) + + @qml.qnode(dev2) + def circuit2(): + qml.templates.QAOAEmbedding(weight, wires1=['z', 'b'], wires2=['k', 'a']) + return qml.expval(qml.Identity('z')) + + circuit() + circuit2() + + assert np.allclose(dev.state, dev2.state, atol=tol, rtol=0) + + +# class TestParameters: + +# +# def test_exception_fewer_qubits_than_features(self, ): +# """Verifies that exception raised if there are fewer +# wires than features.""" +# +# features = [0, 0, 0, 0] +# n_wires = 1 +# weights = np.zeros(shape=(1, 2 * n_wires)) +# dev = qml.device('default.qubit', wires=n_wires) +# +# @qml.qnode(dev) +# def circuit(x=None): +# qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires)) +# return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] +# +# with pytest.raises(ValueError, match="Features must be of "): +# circuit(x=features) +# +# def test_exception_wrong_feauture_shape(self): +# """Verifies that exception is raised if the shape of features is incorrect.""" +# n_wires = 1 +# weights = np.zeros(shape=(1, 1)) +# features = np.zeros(shape=(2, 1)) +# dev = qml.device('default.qubit', wires=n_wires) +# +# @qml.qnode(dev) +# def circuit(): +# qml.templates.QAOAEmbedding(features, weights, wires=range(n_wires)) +# return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] +# +# with pytest.raises(ValueError, match="Features must be a one-dimensional"): +# circuit() +# +# def test_exception_wrong_weight_shape(self): +# """Verifies that exception is raised if the shape of weights is incorrect.""" +# n_wires = 2 +# weights = np.zeros(shape=(1, 4)) +# features = np.zeros(shape=(2, )) +# dev = qml.device('default.qubit', wires=n_wires) +# +# @qml.qnode(dev) +# def circuit(): +# qml.templates.QAOAEmbedding(features, weights, wires=range(n_wires)) +# return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] +# +# with pytest.raises(ValueError, match="Weights tensor must be of shape"): +# circuit() +# +# @pytest.mark.parametrize('n_layers, n_wires, expected_shape', [ +# (2, 3, (2, 6)), +# (2, 1, (2, 1)), +# (2, 2, (2, 3)), +# ]) +# def test_shape_random_weights(self, n_layers, n_wires, expected_shape): +# +# weights1 = qml.templates.QAOAEmbedding.weights_uniform(n_layers, n_wires) +# weights2 = qml.templates.QAOAEmbedding.weights_normal(n_layers, n_wires) +# +# assert weights1.shape == expected_shape +# assert weights2.shape == expected_shape +# +# +# class TestGradients: +# """Tests that the gradient is computed correctly in all three interfaces.""" +# +# def test_autograd(self, tol): +# """Tests that gradients of template and decomposed circuit +# are the same in the autograd interface.""" +# +# features = np.random.random(size=(2, )) +# weights = np.random.random(size=(1, 3)) +# +# dev = qml.device('default.qubit', wires=2) +# +# circuit = qml.QNode(circuit_template, dev) +# circuit2 = qml.QNode(circuit_decomposed, dev) +# +# grad_fn = qml.grad(circuit) +# grads = grad_fn(features, weights) +# +# grad_fn2 = qml.grad(circuit2) +# grads2 = grad_fn2(features, weights) +# +# assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) +# assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) +# +# def test_jax(self, tol, skip_if_no_jax_support): +# """Tests that gradients of template and decomposed circuit +# are the same in the jax interface.""" +# +# import jax +# import jax.numpy as jnp +# +# features = jnp.array(np.random.random(size=(2, ))) +# weights = jnp.array(np.random.random(size=(1, 3))) +# +# dev = qml.device('default.qubit', wires=2) +# +# circuit = qml.QNode(circuit_template, dev, interface='jax') +# circuit2 = qml.QNode(circuit_decomposed, dev, interface='jax') +# +# grad_fn = jax.grad(circuit) +# grads = grad_fn(features, weights) +# +# grad_fn2 = jax.grad(circuit2) +# grads2 = grad_fn2(features, weights) +# +# assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) +# assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) +# +# def test_tf(self, tol, skip_if_no_tf_support): +# """Tests that gradients of template and decomposed circuit +# are the same in the tf interface.""" +# +# import tensorflow as tf +# +# features = tf.Variable(np.random.random(size=(2, ))) +# weights = tf.Variable(np.random.random(size=(1, 3))) +# +# dev = qml.device('default.qubit', wires=2) +# +# circuit = qml.QNode(circuit_template, dev, interface='tf') +# circuit2 = qml.QNode(circuit_decomposed, dev, interface='tf') +# +# with tf.GradientTape() as tape: +# res = circuit(features, weights) +# grads = tape.gradient(res, [features, weights]) +# +# with tf.GradientTape() as tape2: +# res2 = circuit2(features, weights) +# grads2 = tape2.gradient(res2, [features, weights]) +# +# assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) +# assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) +# +# def test_torch(self, tol, skip_if_no_torch_support): +# """Tests that gradients of template and decomposed circuit +# are the same in the torch interface.""" +# +# import torch +# +# features = torch.tensor(np.random.random(size=(2, )), requires_grad=True) +# weights = torch.tensor(np.random.random(size=(1, 3)), requires_grad=True) +# +# dev = qml.device('default.qubit', wires=2) +# +# circuit = qml.QNode(circuit_template, dev, interface='torch') +# circuit2 = qml.QNode(circuit_decomposed, dev, interface='torch') +# +# res = circuit(features, weights) +# res.backward() +# grads = [features.grad, weights.grad] +# +# res2 = circuit2(features, weights) +# res2.backward() +# grads2 = [features.grad, weights.grad] +# +# assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) +# assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) + diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py index 33bb0e35d6e..8d807bdb802 100644 --- a/tests/templates/test_subroutines.py +++ b/tests/templates/test_subroutines.py @@ -576,222 +576,8 @@ class TestDoubleExcitationUnitary: """Tests for the DoubleExcitationUnitary template from the pennylane.templates.subroutine module.""" - @pytest.mark.parametrize( - ("wires1", "wires2", "ref_gates"), - [ - ( - [0, 1, 2], - [4, 5, 6], - [ - [0, qml.Hadamard, [0], []], - [1, qml.Hadamard, [2], []], - [2, qml.RX, [4], [-np.pi / 2]], - [3, qml.Hadamard, [6], []], - [9, qml.RZ, [6], [np.pi / 24]], - [15, qml.Hadamard, [0], []], - [16, qml.Hadamard, [2], []], - [17, qml.RX, [4], [np.pi / 2]], - [18, qml.Hadamard, [6], []], - ], - ), - ( - [0, 1], - [4, 5], - [ - [15, qml.RX, [0], [-np.pi / 2]], - [16, qml.Hadamard, [1], []], - [17, qml.RX, [4], [-np.pi / 2]], - [18, qml.RX, [5], [-np.pi / 2]], - [22, qml.RZ, [5], [np.pi / 24]], - [26, qml.RX, [0], [np.pi / 2]], - [27, qml.Hadamard, [1], []], - [28, qml.RX, [4], [np.pi / 2]], - [29, qml.RX, [5], [np.pi / 2]], - ], - ), - ( - [1, 2, 3], - [7, 8, 9, 10, 11], - [ - [46, qml.Hadamard, [1], []], - [47, qml.RX, [3], [-np.pi / 2]], - [48, qml.RX, [7], [-np.pi / 2]], - [49, qml.RX, [11], [-np.pi / 2]], - [57, qml.RZ, [11], [np.pi / 24]], - [65, qml.Hadamard, [1], []], - [66, qml.RX, [3], [np.pi / 2]], - [67, qml.RX, [7], [np.pi / 2]], - [68, qml.RX, [11], [np.pi / 2]], - ], - ), - ( - [2, 3, 4], - [8, 9, 10], - [ - [57, qml.Hadamard, [2], []], - [58, qml.Hadamard, [4], []], - [59, qml.Hadamard, [8], []], - [60, qml.RX, [10], [-np.pi / 2]], - [66, qml.RZ, [10], [np.pi / 24]], - [72, qml.Hadamard, [2], []], - [73, qml.Hadamard, [4], []], - [74, qml.Hadamard, [8], []], - [75, qml.RX, [10], [np.pi / 2]], - ], - ), - ( - [3, 4, 5], - [11, 12, 13, 14, 15], - [ - [92, qml.RX, [3], [-np.pi / 2]], - [93, qml.Hadamard, [5], []], - [94, qml.Hadamard, [11], []], - [95, qml.Hadamard, [15], []], - [103, qml.RZ, [15], [-np.pi / 24]], - [111, qml.RX, [3], [np.pi / 2]], - [112, qml.Hadamard, [5], []], - [113, qml.Hadamard, [11], []], - [114, qml.Hadamard, [15], []], - ], - ), - ( - [4, 5, 6, 7], - [9, 10], - [ - [95, qml.Hadamard, [4], []], - [96, qml.RX, [7], [-np.pi / 2]], - [97, qml.Hadamard, [9], []], - [98, qml.Hadamard, [10], []], - [104, qml.RZ, [10], [-np.pi / 24]], - [110, qml.Hadamard, [4], []], - [111, qml.RX, [7], [np.pi / 2]], - [112, qml.Hadamard, [9], []], - [113, qml.Hadamard, [10], []], - ], - ), - ( - [5, 6], - [10, 11, 12], - [ - [102, qml.RX, [5], [-np.pi / 2]], - [103, qml.RX, [6], [-np.pi / 2]], - [104, qml.RX, [10], [-np.pi / 2]], - [105, qml.Hadamard, [12], []], - [110, qml.RZ, [12], [-np.pi / 24]], - [115, qml.RX, [5], [np.pi / 2]], - [116, qml.RX, [6], [np.pi / 2]], - [117, qml.RX, [10], [np.pi / 2]], - [118, qml.Hadamard, [12], []], - ], - ), - ( - [3, 4, 5, 6], - [17, 18, 19], - [ - [147, qml.RX, [3], [-np.pi / 2]], - [148, qml.RX, [6], [-np.pi / 2]], - [149, qml.Hadamard, [17], []], - [150, qml.RX, [19], [-np.pi / 2]], - [157, qml.RZ, [19], [-np.pi / 24]], - [164, qml.RX, [3], [np.pi / 2]], - [165, qml.RX, [6], [np.pi / 2]], - [166, qml.Hadamard, [17], []], - [167, qml.RX, [19], [np.pi / 2]], - ], - ), - ( - [6, 7], - [8, 9], - [ - [4, qml.CNOT, [6, 7], []], - [5, qml.CNOT, [7, 8], []], - [6, qml.CNOT, [8, 9], []], - [8, qml.CNOT, [8, 9], []], - [9, qml.CNOT, [7, 8], []], - [10, qml.CNOT, [6, 7], []], - ], - ), - ( - [4, 5, 6, 7], - [8, 9, 10, 11, 12, 13], - [ - [58, qml.CNOT, [4, 5], []], - [59, qml.CNOT, [5, 6], []], - [60, qml.CNOT, [6, 7], []], - [61, qml.CNOT, [7, 8], []], - [62, qml.CNOT, [8, 9], []], - [63, qml.CNOT, [9, 10], []], - [64, qml.CNOT, [10, 11], []], - [65, qml.CNOT, [11, 12], []], - [66, qml.CNOT, [12, 13], []], - [122, qml.CNOT, [12, 13], []], - [123, qml.CNOT, [11, 12], []], - [124, qml.CNOT, [10, 11], []], - [125, qml.CNOT, [9, 10], []], - [126, qml.CNOT, [8, 9], []], - [127, qml.CNOT, [7, 8], []], - [128, qml.CNOT, [6, 7], []], - [129, qml.CNOT, [5, 6], []], - [130, qml.CNOT, [4, 5], []], - ], - ), - ], - ) - def test_double_ex_unitary_operations(self, wires1, wires2, ref_gates): - """Test the correctness of the DoubleExcitationUnitary template including the gate count - and order, the wires each operation acts on and the correct use of parameters - in the circuit.""" - - sqg = 72 - cnots = 16 * (len(wires1) - 1 + len(wires2) - 1 + 1) - weight = np.pi / 3 - - op = DoubleExcitationUnitary(weight, wires1=wires1, wires2=wires2) - - with QuantumTape() as tape: - op.decomposition(weight, wires=Wires(wires1+wires2)) - - queue = tape.operations - - assert len(queue) == sqg + cnots - - for gate in ref_gates: - idx = gate[0] - - exp_gate = gate[1] - res_gate = queue[idx] - assert type(res_gate) == exp_gate - - exp_wires = gate[2] - res_wires = queue[idx]._wires - assert res_wires == Wires(exp_wires) - - exp_weight = gate[3] - res_weight = queue[idx].parameters - assert res_weight == exp_weight - - @pytest.mark.parametrize( - ("weight", "wires1", "wires2", "msg_match"), - [ - (0.2, [0], [1, 2], "expected at least two wires representing the occupied"), - (0.2, [0, 1], [2], "expected at least two wires representing the unoccupied"), - (0.2, [0], [1], "expected at least two wires representing the occupied"), - ([0.2, 1.1], [0, 2], [4, 6], "Weight must be a scalar"), - ], - ) - def test_double_excitation_unitary_exceptions(self, weight, wires1, wires2, msg_match): - """Test that DoubleExcitationUnitary throws an exception if ``weight`` or - ``pphh`` parameter has illegal shapes, types or values.""" - dev = qml.device("default.qubit", wires=10) - - def circuit(weight=weight): - DoubleExcitationUnitary(weight=weight, wires1=wires1, wires2=wires2) - return qml.expval(qml.PauliZ(0)) - qnode = qml.QNode(circuit, dev) - with pytest.raises(ValueError, match=msg_match): - qnode(weight=weight) @pytest.mark.parametrize( ("weight", "wires1", "wires2", "expected"), From 02965db976cb30b2868117be473eae065e54207a Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Tue, 16 Mar 2021 09:44:29 +0200 Subject: [PATCH 37/64] black tests --- .../test_ansaetze/test_basic_entangler.py | 61 ++--- tests/templates/test_embeddings/test_qaoa.py | 129 +++++----- .../test_qchem/test_double_excitation.py | 230 +++--------------- tests/templates/test_subroutines.py | 31 --- 4 files changed, 135 insertions(+), 316 deletions(-) diff --git a/tests/templates/test_ansaetze/test_basic_entangler.py b/tests/templates/test_ansaetze/test_basic_entangler.py index 0febf4df543..7171a447f29 100644 --- a/tests/templates/test_ansaetze/test_basic_entangler.py +++ b/tests/templates/test_ansaetze/test_basic_entangler.py @@ -37,12 +37,14 @@ def circuit_decomposed(weights): class TestDecomposition: """Tests that the template defines the correct decomposition.""" - QUEUES = [(1, (1, 1), ['RX']), - (2, (1, 2), ['RX', 'RX', 'CNOT', 'RY', 'RY', 'RX', 'RX']), - (2, (2, 2), ['RX', 'RX', 'CNOT', 'RX', 'RX', 'CNOT']), - (3, (1, 3), ['RX', 'RX', 'RX', 'CNOT', 'CNOT', 'CNOT'])] - - @pytest.mark.parametrize('n_wires, weight_shape, expected_names', QUEUES) + QUEUES = [ + (1, (1, 1), ["RX"]), + (2, (1, 2), ["RX", "RX", "CNOT", "RY", "RY", "RX", "RX"]), + (2, (2, 2), ["RX", "RX", "CNOT", "RX", "RX", "CNOT"]), + (3, (1, 3), ["RX", "RX", "RX", "CNOT", "CNOT", "CNOT"]), + ] + + @pytest.mark.parametrize("n_wires, weight_shape, expected_names", QUEUES) def test_expansion(self, n_wires, weight_shape, expected_names): """Checks the queue for the default settings.""" @@ -54,7 +56,7 @@ def test_expansion(self, n_wires, weight_shape, expected_names): for i, gate in enumerate(tape.operations): assert gate.name == expected_names[i] - @pytest.mark.parametrize('rotation', [qml.RY, qml.RZ]) + @pytest.mark.parametrize("rotation", [qml.RY, qml.RZ]) def test_rotation(self, rotation): """Checks that custom rotation gate is used.""" @@ -93,8 +95,8 @@ def test_custom_wire_labels(self, tol): """Test that template can deal with non-numeric, nonconsecutive wire labels.""" weights = np.random.random(size=(1, 3)) - dev = qml.device('default.qubit', wires=3) - dev2 = qml.device('default.qubit', wires=['z', 'a', 'k']) + dev = qml.device("default.qubit", wires=3) + dev2 = qml.device("default.qubit", wires=["z", "a", "k"]) @qml.qnode(dev) def circuit(): @@ -103,8 +105,8 @@ def circuit(): @qml.qnode(dev2) def circuit2(): - qml.templates.BasicEntanglerLayers(weights, wires=['z', 'a', 'k']) - return qml.expval(qml.Identity('z')) + qml.templates.BasicEntanglerLayers(weights, wires=["z", "a", "k"]) + return qml.expval(qml.Identity("z")) circuit() circuit2() @@ -113,12 +115,11 @@ def circuit2(): class TestParameters: - def test_exception_wrong_dim(self): """Verifies that exception is raised if the weights shape is incorrect.""" n_wires = 1 - dev = qml.device('default.qubit', wires=n_wires) + dev = qml.device("default.qubit", wires=n_wires) @qml.qnode(dev) def circuit(weights): @@ -131,11 +132,14 @@ def circuit(weights): with pytest.raises(ValueError, match="Weights tensor must have second dimension of length"): circuit([[1, 0], [1, 0]]) - @pytest.mark.parametrize('n_layers, n_wires, expected_shape', [ - (2, 3, (2, 3)), - (2, 1, (2, 1)), - (2, 2, (2, 2)), - ]) + @pytest.mark.parametrize( + "n_layers, n_wires, expected_shape", + [ + (2, 3, (2, 3)), + (2, 1, (2, 1)), + (2, 2, (2, 2)), + ], + ) def test_shape_random_weights(self, n_layers, n_wires, expected_shape): weights1 = qml.templates.BasicEntanglerLayers.weights_uniform(n_layers, n_wires) @@ -154,7 +158,7 @@ def test_autograd(self, tol): weights = np.random.random(size=(1, 3)) - dev = qml.device('default.qubit', wires=3) + dev = qml.device("default.qubit", wires=3) circuit = qml.QNode(circuit_template, dev) circuit2 = qml.QNode(circuit_decomposed, dev) @@ -176,10 +180,10 @@ def test_jax(self, tol, skip_if_no_jax_support): weights = jnp.array(np.random.random(size=(1, 3))) - dev = qml.device('default.qubit', wires=3) + dev = qml.device("default.qubit", wires=3) - circuit = qml.QNode(circuit_template, dev, interface='jax') - circuit2 = qml.QNode(circuit_decomposed, dev, interface='jax') + circuit = qml.QNode(circuit_template, dev, interface="jax") + circuit2 = qml.QNode(circuit_decomposed, dev, interface="jax") grad_fn = jax.grad(circuit) grads = grad_fn(weights) @@ -197,10 +201,10 @@ def test_tf(self, tol, skip_if_no_tf_support): weights = tf.Variable(np.random.random(size=(1, 3))) - dev = qml.device('default.qubit', wires=3) + dev = qml.device("default.qubit", wires=3) - circuit = qml.QNode(circuit_template, dev, interface='tf') - circuit2 = qml.QNode(circuit_decomposed, dev, interface='tf') + circuit = qml.QNode(circuit_template, dev, interface="tf") + circuit2 = qml.QNode(circuit_decomposed, dev, interface="tf") with tf.GradientTape() as tape: res = circuit(weights) @@ -220,10 +224,10 @@ def test_torch(self, tol, skip_if_no_torch_support): weights = torch.tensor(np.random.random(size=(1, 3)), requires_grad=True) - dev = qml.device('default.qubit', wires=3) + dev = qml.device("default.qubit", wires=3) - circuit = qml.QNode(circuit_template, dev, interface='torch') - circuit2 = qml.QNode(circuit_decomposed, dev, interface='torch') + circuit = qml.QNode(circuit_template, dev, interface="torch") + circuit2 = qml.QNode(circuit_decomposed, dev, interface="torch") res = circuit(weights) res.backward() @@ -234,4 +238,3 @@ def test_torch(self, tol, skip_if_no_torch_support): grads2 = [weights.grad] assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) - diff --git a/tests/templates/test_embeddings/test_qaoa.py b/tests/templates/test_embeddings/test_qaoa.py index 6d51e056af5..1b98dde2f67 100644 --- a/tests/templates/test_embeddings/test_qaoa.py +++ b/tests/templates/test_embeddings/test_qaoa.py @@ -38,14 +38,22 @@ def circuit_decomposed(features, weights): class TestDecomposition: """Tests that the template defines the correct decomposition.""" - QUEUES = [(1, (1, 1), ['RX', 'RY', 'RX']), - (2, (1, 3), ['RX', 'RX', 'MultiRZ', 'RY', 'RY', 'RX', 'RX']), - (2, (2, 3), ['RX', 'RX', 'MultiRZ', 'RY', 'RY', 'RX', 'RX', - 'MultiRZ', 'RY', 'RY', 'RX', 'RX']), - (3, (1, 6), ['RX', 'RX', 'RX', 'MultiRZ', 'MultiRZ', 'MultiRZ', - 'RY', 'RY', 'RY', 'RX', 'RX', 'RX'])] - - @pytest.mark.parametrize('n_wires, weight_shape, expected_names', QUEUES) + QUEUES = [ + (1, (1, 1), ["RX", "RY", "RX"]), + (2, (1, 3), ["RX", "RX", "MultiRZ", "RY", "RY", "RX", "RX"]), + ( + 2, + (2, 3), + ["RX", "RX", "MultiRZ", "RY", "RY", "RX", "RX", "MultiRZ", "RY", "RY", "RX", "RX"], + ), + ( + 3, + (1, 6), + ["RX", "RX", "RX", "MultiRZ", "MultiRZ", "MultiRZ", "RY", "RY", "RY", "RX", "RX", "RX"], + ), + ] + + @pytest.mark.parametrize("n_wires, weight_shape, expected_names", QUEUES) def test_expansion(self, n_wires, weight_shape, expected_names): """Checks the queue for the default settings.""" @@ -58,13 +66,11 @@ def test_expansion(self, n_wires, weight_shape, expected_names): for i, gate in enumerate(tape.operations): assert gate.name == expected_names[i] - @pytest.mark.parametrize('local_field', ['X', 'Y', 'Z']) + @pytest.mark.parametrize("local_field", ["X", "Y", "Z"]) def test_local_field(self, local_field): """Checks that custom local field is used.""" - get_name = {'X': 'RX', - 'Y': 'RY', - 'Z': 'RZ'} + get_name = {"X": "RX", "Y": "RY", "Z": "RZ"} features = list(range(2)) weights = np.zeros(shape=(1, 3)) @@ -82,11 +88,13 @@ def test_exception_wrongrot(self): n_wires = 1 weights = np.zeros(shape=(1, 1)) - dev = qml.device('default.qubit', wires=n_wires) + dev = qml.device("default.qubit", wires=n_wires) @qml.qnode(dev) def circuit(x=None): - qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='A') + qml.templates.QAOAEmbedding( + features=x, weights=weights, wires=range(n_wires), local_field="A" + ) return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] with pytest.raises(ValueError, match="did not recognize"): @@ -114,35 +122,42 @@ def circuit(x=None): target = [1, -1, 0, 1, 1] assert np.allclose(res, target[:n_subsystems], atol=tol, rtol=0) - @pytest.mark.parametrize('weights, target', [([[np.pi, 0, 0]], [1, 1]), - ([[np.pi / 2, 0, 0]], [0, 0]), - ([[0, 0, 0]], [-1, -1])]) + @pytest.mark.parametrize( + "weights, target", + [([[np.pi, 0, 0]], [1, 1]), ([[np.pi / 2, 0, 0]], [0, 0]), ([[0, 0, 0]], [-1, -1])], + ) def test_output_zz(self, weights, target, tol): """Checks the output if the features and entangler weights are nonzero, which makes the circuit only depend on the ZZ gate.""" - dev = qml.device('default.qubit', wires=2) + dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x=None): qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(2)) return [qml.expval(qml.PauliZ(i)) for i in range(2)] - res = circuit(x=[np.pi/2, np.pi/2]) + res = circuit(x=[np.pi / 2, np.pi / 2]) assert np.allclose(res, target, atol=tol, rtol=0) - @pytest.mark.parametrize('n_wires, features, weights, target', [(2, [0], [[0, 0, np.pi / 2]], [1, 0]), - (3, [0, 0], [[0, 0, 0, 0, 0, np.pi / 2]], - [1, 1, 0])]) + @pytest.mark.parametrize( + "n_wires, features, weights, target", + [ + (2, [0], [[0, 0, np.pi / 2]], [1, 0]), + (3, [0, 0], [[0, 0, 0, 0, 0, np.pi / 2]], [1, 1, 0]), + ], + ) def test_state_more_qubits_than_features(self, n_wires, features, weights, target, tol): """Checks the state is correct if there are more qubits than features.""" - dev = qml.device('default.qubit', wires=n_wires) + dev = qml.device("default.qubit", wires=n_wires) @qml.qnode(dev) def circuit(x=None): - qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='Z') + qml.templates.QAOAEmbedding( + features=x, weights=weights, wires=range(n_wires), local_field="Z" + ) return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] res = circuit(x=features) @@ -151,10 +166,10 @@ def circuit(x=None): def test_custom_wire_labels(self, tol): """Test that template can deal with non-numeric, nonconsecutive wire labels.""" weights = np.random.random(size=(1, 6)) - features = np.random.random(size=(3, )) + features = np.random.random(size=(3,)) - dev = qml.device('default.qubit', wires=3) - dev2 = qml.device('default.qubit', wires=['z', 'a', 'k']) + dev = qml.device("default.qubit", wires=3) + dev2 = qml.device("default.qubit", wires=["z", "a", "k"]) @qml.qnode(dev) def circuit(): @@ -163,8 +178,8 @@ def circuit(): @qml.qnode(dev2) def circuit2(): - qml.templates.QAOAEmbedding(features, weights, wires=['z', 'a', 'k']) - return qml.expval(qml.Identity('z')) + qml.templates.QAOAEmbedding(features, weights, wires=["z", "a", "k"]) + return qml.expval(qml.Identity("z")) circuit() circuit2() @@ -175,14 +190,16 @@ def circuit2(): class TestParameters: """Test inputs and pre-processing.""" - def test_exception_fewer_qubits_than_features(self, ): + def test_exception_fewer_qubits_than_features( + self, + ): """Verifies that exception raised if there are fewer - wires than features.""" + wires than features.""" features = [0, 0, 0, 0] n_wires = 1 weights = np.zeros(shape=(1, 2 * n_wires)) - dev = qml.device('default.qubit', wires=n_wires) + dev = qml.device("default.qubit", wires=n_wires) @qml.qnode(dev) def circuit(x=None): @@ -197,7 +214,7 @@ def test_exception_wrong_feauture_shape(self): n_wires = 1 weights = np.zeros(shape=(1, 1)) features = np.zeros(shape=(2, 1)) - dev = qml.device('default.qubit', wires=n_wires) + dev = qml.device("default.qubit", wires=n_wires) @qml.qnode(dev) def circuit(): @@ -211,8 +228,8 @@ def test_exception_wrong_weight_shape(self): """Verifies that exception is raised if the shape of weights is incorrect.""" n_wires = 2 weights = np.zeros(shape=(1, 4)) - features = np.zeros(shape=(2, )) - dev = qml.device('default.qubit', wires=n_wires) + features = np.zeros(shape=(2,)) + dev = qml.device("default.qubit", wires=n_wires) @qml.qnode(dev) def circuit(): @@ -222,11 +239,14 @@ def circuit(): with pytest.raises(ValueError, match="Weights tensor must be of shape"): circuit() - @pytest.mark.parametrize('n_layers, n_wires, expected_shape', [ - (2, 3, (2, 6)), - (2, 1, (2, 1)), - (2, 2, (2, 3)), - ]) + @pytest.mark.parametrize( + "n_layers, n_wires, expected_shape", + [ + (2, 3, (2, 6)), + (2, 1, (2, 1)), + (2, 2, (2, 3)), + ], + ) def test_shape_random_weights(self, n_layers, n_wires, expected_shape): weights1 = qml.templates.QAOAEmbedding.weights_uniform(n_layers, n_wires) @@ -243,10 +263,10 @@ def test_autograd(self, tol): """Tests that gradients of template and decomposed circuit are the same in the autograd interface.""" - features = np.random.random(size=(2, )) + features = np.random.random(size=(2,)) weights = np.random.random(size=(1, 3)) - dev = qml.device('default.qubit', wires=2) + dev = qml.device("default.qubit", wires=2) circuit = qml.QNode(circuit_template, dev) circuit2 = qml.QNode(circuit_decomposed, dev) @@ -267,13 +287,13 @@ def test_jax(self, tol, skip_if_no_jax_support): import jax import jax.numpy as jnp - features = jnp.array(np.random.random(size=(2, ))) + features = jnp.array(np.random.random(size=(2,))) weights = jnp.array(np.random.random(size=(1, 3))) - dev = qml.device('default.qubit', wires=2) + dev = qml.device("default.qubit", wires=2) - circuit = qml.QNode(circuit_template, dev, interface='jax') - circuit2 = qml.QNode(circuit_decomposed, dev, interface='jax') + circuit = qml.QNode(circuit_template, dev, interface="jax") + circuit2 = qml.QNode(circuit_decomposed, dev, interface="jax") grad_fn = jax.grad(circuit) grads = grad_fn(features, weights) @@ -290,13 +310,13 @@ def test_tf(self, tol, skip_if_no_tf_support): import tensorflow as tf - features = tf.Variable(np.random.random(size=(2, ))) + features = tf.Variable(np.random.random(size=(2,))) weights = tf.Variable(np.random.random(size=(1, 3))) - dev = qml.device('default.qubit', wires=2) + dev = qml.device("default.qubit", wires=2) - circuit = qml.QNode(circuit_template, dev, interface='tf') - circuit2 = qml.QNode(circuit_decomposed, dev, interface='tf') + circuit = qml.QNode(circuit_template, dev, interface="tf") + circuit2 = qml.QNode(circuit_decomposed, dev, interface="tf") with tf.GradientTape() as tape: res = circuit(features, weights) @@ -315,13 +335,13 @@ def test_torch(self, tol, skip_if_no_torch_support): import torch - features = torch.tensor(np.random.random(size=(2, )), requires_grad=True) + features = torch.tensor(np.random.random(size=(2,)), requires_grad=True) weights = torch.tensor(np.random.random(size=(1, 3)), requires_grad=True) - dev = qml.device('default.qubit', wires=2) + dev = qml.device("default.qubit", wires=2) - circuit = qml.QNode(circuit_template, dev, interface='torch') - circuit2 = qml.QNode(circuit_decomposed, dev, interface='torch') + circuit = qml.QNode(circuit_template, dev, interface="torch") + circuit2 = qml.QNode(circuit_decomposed, dev, interface="torch") res = circuit(features, weights) res.backward() @@ -333,4 +353,3 @@ def test_torch(self, tol, skip_if_no_torch_support): assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) - diff --git a/tests/templates/test_qchem/test_double_excitation.py b/tests/templates/test_qchem/test_double_excitation.py index 28f5e624e32..bb70afb1ba4 100644 --- a/tests/templates/test_qchem/test_double_excitation.py +++ b/tests/templates/test_qchem/test_double_excitation.py @@ -12,29 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Unit tests for the QAOAEmbedding template. +Unit tests for the DoubleExcitationUnitary template. """ import pytest import numpy as np import pennylane as qml -def circuit_template(features, weights): - qml.templates.QAOAEmbedding(features, weights, range(2)) - return qml.expval(qml.PauliZ(0)) - - -def circuit_decomposed(features, weights): - qml.RX(features[0], wires=0) - qml.RX(features[1], wires=1) - qml.MultiRZ(weights[0, 0], wires=[0, 1]) - qml.RY(weights[0, 1], wires=0) - qml.RY(weights[0, 2], wires=1) - qml.RX(features[0], wires=0) - qml.RX(features[1], wires=1) - return qml.expval(qml.PauliZ(0)) - - class TestDecomposition: """Tests that the template defines the correct decomposition.""" @@ -229,7 +213,31 @@ def test_double_ex_unitary_operations(self, wires1, wires2, ref_gates): res_weight = queue[idx].parameters assert res_weight == exp_weight - + def test_custom_wire_labels(self, tol): + """Test that template can deal with non-numeric, nonconsecutive wire labels.""" + weight = np.random.random() + + dev = qml.device("default.qubit", wires=6) + dev2 = qml.device("default.qubit", wires=["z", "a", "k", "b", "t", "mm"]) + + @qml.qnode(dev) + def circuit(): + qml.templates.DoubleExcitationUnitary(weight, wires1=[0, 3, 4], wires2=[2, 5, 1]) + return qml.expval(qml.Identity(0)) + + @qml.qnode(dev2) + def circuit2(): + qml.templates.DoubleExcitationUnitary( + weight, wires1=["z", "b", "t"], wires2=["k", "mm", "a"] + ) + return qml.expval(qml.Identity("z")) + + circuit() + circuit2() + + assert np.allclose(dev.state, dev2.state, atol=tol, rtol=0) + + class TestParameters: """Test inputs and pre-processing.""" @@ -256,188 +264,8 @@ def circuit(weight=weight): with pytest.raises(ValueError, match=msg_match): qnode(weight=weight) - def test_custom_wire_labels(self, tol): - """Test that template can deal with non-numeric, nonconsecutive wire labels.""" - weight = np.random.random() - - dev = qml.device('default.qubit', wires=4) - dev2 = qml.device('default.qubit', wires=['z', 'a', 'k', 'b']) - - @qml.qnode(dev) - def circuit(): - qml.templates.DoubleExcitationUnitary(weight, wires1=[0, 3], wires2=[2, 1]) - return qml.expval(qml.Identity(0)) - - @qml.qnode(dev2) - def circuit2(): - qml.templates.QAOAEmbedding(weight, wires1=['z', 'b'], wires2=['k', 'a']) - return qml.expval(qml.Identity('z')) - - circuit() - circuit2() - - assert np.allclose(dev.state, dev2.state, atol=tol, rtol=0) - - -# class TestParameters: -# -# def test_exception_fewer_qubits_than_features(self, ): -# """Verifies that exception raised if there are fewer -# wires than features.""" -# -# features = [0, 0, 0, 0] -# n_wires = 1 -# weights = np.zeros(shape=(1, 2 * n_wires)) -# dev = qml.device('default.qubit', wires=n_wires) -# -# @qml.qnode(dev) -# def circuit(x=None): -# qml.templates.QAOAEmbedding(features=x, weights=weights, wires=range(n_wires)) -# return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] -# -# with pytest.raises(ValueError, match="Features must be of "): -# circuit(x=features) -# -# def test_exception_wrong_feauture_shape(self): -# """Verifies that exception is raised if the shape of features is incorrect.""" -# n_wires = 1 -# weights = np.zeros(shape=(1, 1)) -# features = np.zeros(shape=(2, 1)) -# dev = qml.device('default.qubit', wires=n_wires) -# -# @qml.qnode(dev) -# def circuit(): -# qml.templates.QAOAEmbedding(features, weights, wires=range(n_wires)) -# return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] -# -# with pytest.raises(ValueError, match="Features must be a one-dimensional"): -# circuit() -# -# def test_exception_wrong_weight_shape(self): -# """Verifies that exception is raised if the shape of weights is incorrect.""" -# n_wires = 2 -# weights = np.zeros(shape=(1, 4)) -# features = np.zeros(shape=(2, )) -# dev = qml.device('default.qubit', wires=n_wires) -# -# @qml.qnode(dev) -# def circuit(): -# qml.templates.QAOAEmbedding(features, weights, wires=range(n_wires)) -# return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] -# -# with pytest.raises(ValueError, match="Weights tensor must be of shape"): -# circuit() -# -# @pytest.mark.parametrize('n_layers, n_wires, expected_shape', [ -# (2, 3, (2, 6)), -# (2, 1, (2, 1)), -# (2, 2, (2, 3)), -# ]) -# def test_shape_random_weights(self, n_layers, n_wires, expected_shape): -# -# weights1 = qml.templates.QAOAEmbedding.weights_uniform(n_layers, n_wires) -# weights2 = qml.templates.QAOAEmbedding.weights_normal(n_layers, n_wires) -# -# assert weights1.shape == expected_shape -# assert weights2.shape == expected_shape -# -# -# class TestGradients: -# """Tests that the gradient is computed correctly in all three interfaces.""" -# -# def test_autograd(self, tol): -# """Tests that gradients of template and decomposed circuit -# are the same in the autograd interface.""" -# -# features = np.random.random(size=(2, )) -# weights = np.random.random(size=(1, 3)) -# -# dev = qml.device('default.qubit', wires=2) -# -# circuit = qml.QNode(circuit_template, dev) -# circuit2 = qml.QNode(circuit_decomposed, dev) -# -# grad_fn = qml.grad(circuit) -# grads = grad_fn(features, weights) -# -# grad_fn2 = qml.grad(circuit2) -# grads2 = grad_fn2(features, weights) -# -# assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) -# assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) -# -# def test_jax(self, tol, skip_if_no_jax_support): -# """Tests that gradients of template and decomposed circuit -# are the same in the jax interface.""" -# -# import jax -# import jax.numpy as jnp -# -# features = jnp.array(np.random.random(size=(2, ))) -# weights = jnp.array(np.random.random(size=(1, 3))) -# -# dev = qml.device('default.qubit', wires=2) -# -# circuit = qml.QNode(circuit_template, dev, interface='jax') -# circuit2 = qml.QNode(circuit_decomposed, dev, interface='jax') -# -# grad_fn = jax.grad(circuit) -# grads = grad_fn(features, weights) -# -# grad_fn2 = jax.grad(circuit2) -# grads2 = grad_fn2(features, weights) -# -# assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) -# assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) -# -# def test_tf(self, tol, skip_if_no_tf_support): -# """Tests that gradients of template and decomposed circuit -# are the same in the tf interface.""" -# -# import tensorflow as tf -# -# features = tf.Variable(np.random.random(size=(2, ))) -# weights = tf.Variable(np.random.random(size=(1, 3))) -# -# dev = qml.device('default.qubit', wires=2) -# -# circuit = qml.QNode(circuit_template, dev, interface='tf') -# circuit2 = qml.QNode(circuit_decomposed, dev, interface='tf') -# -# with tf.GradientTape() as tape: -# res = circuit(features, weights) -# grads = tape.gradient(res, [features, weights]) -# -# with tf.GradientTape() as tape2: -# res2 = circuit2(features, weights) -# grads2 = tape2.gradient(res2, [features, weights]) -# -# assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) -# assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) -# -# def test_torch(self, tol, skip_if_no_torch_support): -# """Tests that gradients of template and decomposed circuit -# are the same in the torch interface.""" -# -# import torch -# -# features = torch.tensor(np.random.random(size=(2, )), requires_grad=True) -# weights = torch.tensor(np.random.random(size=(1, 3)), requires_grad=True) -# -# dev = qml.device('default.qubit', wires=2) -# -# circuit = qml.QNode(circuit_template, dev, interface='torch') -# circuit2 = qml.QNode(circuit_decomposed, dev, interface='torch') -# -# res = circuit(features, weights) -# res.backward() -# grads = [features.grad, weights.grad] -# -# res2 = circuit2(features, weights) -# res2.backward() -# grads2 = [features.grad, weights.grad] -# -# assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) -# assert np.allclose(grads[1], grads2[1], atol=tol, rtol=0) +class TestGradients: + """Tests that the gradient is computed correctly in all three interfaces.""" + # ToDo: Add! diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py index 8d807bdb802..4a44ae11cc1 100644 --- a/tests/templates/test_subroutines.py +++ b/tests/templates/test_subroutines.py @@ -572,37 +572,6 @@ def circuit(weights): circuit(weights) -class TestDoubleExcitationUnitary: - """Tests for the DoubleExcitationUnitary template from the - pennylane.templates.subroutine module.""" - - - - - @pytest.mark.parametrize( - ("weight", "wires1", "wires2", "expected"), - [ - (1.34817, [0, 1], [3, 4], [0.22079189, 0.22079189, 1.0, -0.22079189, -0.22079189]), - (0.84817, [1, 2], [3, 4], [1.0, 0.66135688, 0.66135688, -0.66135688, -0.66135688]), - ], - ) - def test_integration(self, weight, wires1, wires2, expected, tol): - """Test integration with PennyLane and gradient calculations""" - - N = 5 - dev = qml.device("default.qubit", wires=N) - - @qml.qnode(dev) - def circuit(weight): - init_state = np.array([0, 0, 0, 1, 1], requires_grad=False) - qml.BasisState(init_state, wires=range(N)) - DoubleExcitationUnitary(weight, wires1=wires1, wires2=wires2) - return [qml.expval(qml.PauliZ(w)) for w in range(N)] - - res = circuit(weight) - assert np.allclose(res, np.array(expected), atol=tol) - - class TestUCCSDUnitary: """Tests for the UCCSD template from the pennylane.templates.subroutine module.""" From f4d1dbca7c1431673f5ff2e5c442cd8b59137b0a Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Tue, 16 Mar 2021 10:05:38 +0200 Subject: [PATCH 38/64] reverted old tests back to normal --- .../subroutines/double_excitation_unitary.py | 109 ++++---- tests/templates/test_embeddings.py | 173 +++++++++++- .../{test_qaoa.py => test_qaoa_embedding.py} | 0 tests/templates/test_integration.py | 18 ++ tests/templates/test_layers.py | 103 ++++++- tests/templates/test_subroutines.py | 263 ++++++++++++++++-- 6 files changed, 595 insertions(+), 71 deletions(-) rename tests/templates/test_embeddings/{test_qaoa.py => test_qaoa_embedding.py} (100%) diff --git a/pennylane/templates/subroutines/double_excitation_unitary.py b/pennylane/templates/subroutines/double_excitation_unitary.py index 551c4faede5..ce0b44403c9 100644 --- a/pennylane/templates/subroutines/double_excitation_unitary.py +++ b/pennylane/templates/subroutines/double_excitation_unitary.py @@ -19,10 +19,36 @@ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access import pennylane as qml from pennylane.ops import CNOT, RX, RZ, Hadamard -from pennylane.operation import Operation, AnyWires +from pennylane.templates.decorator import template from pennylane.wires import Wires +def _preprocess(weight, wires1, wires2): + """Validate and pre-process inputs as follows: + + * Check the shape of the weights tensor. + * Check that both wire sets have at least 2 wires. + + Args: + weight (tensor_like): trainable parameters of the template + wires1 (Wires): first set of wires + wires2 (Wires): second set of wires + """ + + if len(wires1) < 2: + raise ValueError( + "expected at least two wires representing the occupied orbitals; " + "got {}".format(len(wires1)) + ) + if len(wires2) < 2: + raise ValueError( + "expected at least two wires representing the unoccupied orbitals; " + "got {}".format(len(wires2)) + ) + + shape = qml.math.shape(weight) + if shape != (): + raise ValueError(f"Weight must be a scalar; got shape {shape}.") def _layer1(weight, s, r, q, p, set_cnot_wires): @@ -369,7 +395,8 @@ def _layer8(weight, s, r, q, p, set_cnot_wires): RX(np.pi / 2, wires=p) -class DoubleExcitationUnitary(Operation): +@template +def DoubleExcitationUnitary(weight, wires1=None, wires2=None): r"""Circuit to exponentiate the tensor product of Pauli matrices representing the double-excitation operator entering the Unitary Coupled-Cluster Singles and Doubles (UCCSD) ansatz. UCCSD is a VQE ansatz commonly used to run quantum @@ -481,65 +508,45 @@ def circuit(weight, wires1=None, wires2=None): """ - num_params = 1 - num_wires = AnyWires - par_domain = "A" - - def __init__(self, weight, wires1=None, wires2=None, do_queue=True): - self.wires1 = Wires(wires1) - self.wires2 = Wires(wires2) - wires = wires1 + wires2 + ############## + # Input checks - super().__init__(weight, wires=wires, do_queue=do_queue) - self._preprocess() + wires1 = Wires(wires1) + wires2 = Wires(wires2) - def expand(self): + _preprocess(weight, wires1, wires2) - weight = self.parameters[0] + s = wires1[0] + r = wires1[-1] + q = wires2[0] + p = wires2[-1] - s = self.wires1[0] - r = self.wires1[-1] - q = self.wires2[0] - p = self.wires2[-1] + # Sequence of the wires entering the CNOTs + cnots_occ = [wires1.subset([l, l + 1]) for l in range(len(wires1) - 1)] + cnots_unocc = [wires2.subset([l, l + 1]) for l in range(len(wires2) - 1)] - # Sequence of the wires entering the CNOTs - cnots_occ = [self.wires1.subset([l, l + 1]) for l in range(len(self.wires1) - 1)] - cnots_unocc = [self.wires2.subset([l, l + 1]) for l in range(len(self.wires2) - 1)] + set_cnot_wires = cnots_occ + [Wires([r, q])] + cnots_unocc - set_cnot_wires = cnots_occ + [Wires([r, q])] + cnots_unocc + # Apply the first layer + _layer1(weight, s, r, q, p, set_cnot_wires) - with qml.tape.QuantumTape() as tape: + # Apply the second layer + _layer2(weight, s, r, q, p, set_cnot_wires) - _layer1(weight, s, r, q, p, set_cnot_wires) - _layer2(weight, s, r, q, p, set_cnot_wires) - _layer3(weight, s, r, q, p, set_cnot_wires) - _layer4(weight, s, r, q, p, set_cnot_wires) - _layer5(weight, s, r, q, p, set_cnot_wires) - _layer6(weight, s, r, q, p, set_cnot_wires) - _layer7(weight, s, r, q, p, set_cnot_wires) - _layer8(weight, s, r, q, p, set_cnot_wires) + # Apply the third layer + _layer3(weight, s, r, q, p, set_cnot_wires) - return tape + # Apply the fourth layer + _layer4(weight, s, r, q, p, set_cnot_wires) - def _preprocess(self): - """Validate and pre-process inputs as follows: + # Apply the fifth layer + _layer5(weight, s, r, q, p, set_cnot_wires) - * Check the shape of the weights tensor. - * Check that both wire sets have at least 2 wires. - """ - weight = self.parameters[0] + # Apply the sixth layer + _layer6(weight, s, r, q, p, set_cnot_wires) - if len(self.wires1) < 2: - raise ValueError( - "expected at least two wires representing the occupied orbitals; " - "got {}".format(len(self.wires1)) - ) - if len(self.wires2) < 2: - raise ValueError( - "expected at least two wires representing the unoccupied orbitals; " - "got {}".format(len(self.wires2)) - ) + # Apply the seventh layer + _layer7(weight, s, r, q, p, set_cnot_wires) - shape = qml.math.shape(weight) - if shape != (): - raise ValueError(f"Weight must be a scalar; got shape {shape}.") + # Apply the eighth layer + _layer8(weight, s, r, q, p, set_cnot_wires) diff --git a/tests/templates/test_embeddings.py b/tests/templates/test_embeddings.py index 563c8cf0202..2ac1203df24 100644 --- a/tests/templates/test_embeddings.py +++ b/tests/templates/test_embeddings.py @@ -28,7 +28,6 @@ SqueezingEmbedding) from pennylane import Beamsplitter from pennylane.wires import Wires -from pennylane.tape import QuantumTape class TestAmplitudeEmbedding: @@ -492,7 +491,179 @@ def circuit(f=None): circuit(f=features) +class TestQAOAEmbedding: + """ Tests the QAOAEmbedding method.""" + QUEUES = [(1, (1, 1), [qml.RX, qml.RY, qml.RX]), + (2, (1, 3), [qml.RX, qml.RX, qml.MultiRZ, qml.RY, qml.RY, qml.RX, qml.RX]), + (3, (1, 6), [qml.RX, qml.RX, qml.RX, qml.MultiRZ, qml.MultiRZ, qml.MultiRZ, + qml.RY, qml.RY, qml.RY, qml.RX, qml.RX, qml.RX])] + + @pytest.mark.parametrize('n_wires, weight_shape, expected_queue', QUEUES) + def test_queue(self, n_wires, weight_shape, expected_queue): + """Checks the queue for the default settings.""" + + with qml.tape.OperationRecorder() as rec: + QAOAEmbedding(features=list(range(n_wires)), weights=np.zeros(shape=weight_shape), wires=range(n_wires)) + + for gate, expected_gate in zip(rec.queue, expected_queue): + assert isinstance(gate, expected_gate) + + def test_state_zero_weights(self, qubit_device, n_subsystems, tol): + """Checks the state produced by QAOAEmbedding() is correct if the weights are zero.""" + + features = [pi, pi / 2, pi / 4, 0] + if n_subsystems == 1: + shp = (1, 1) + elif n_subsystems == 2: + shp = (1, 3) + else: + shp = (1, 2 * n_subsystems) + + weights = np.zeros(shape=shp) + + @qml.qnode(qubit_device) + def circuit(x=None): + QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems)) + return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] + + res = circuit(x=features[:n_subsystems]) + target = [1, -1, 0, 1, 1] + assert np.allclose(res, target[:n_subsystems], atol=tol, rtol=0) + + @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[pi / 2]], [0]), + (2, [[1, pi / 2, pi / 4]], [0, 1 / np.sqrt(2)]), + (3, [[0, 0, 0, pi, pi / 2, pi / 4]], + [-1, 0, 1 / np.sqrt(2)])]) + def test_output_local_field_ry(self, n_subsystems, weights, target, tol): + """Checks the output if the features are zero. Uses RY local fields.""" + + features = np.zeros(shape=(n_subsystems,)) + dev = qml.device('default.qubit', wires=n_subsystems) + + @qml.qnode(dev) + def circuit(x=None): + QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='Y') + return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] + + res = circuit(x=features[:n_subsystems]) + assert np.allclose(res, target, atol=tol, rtol=0) + + @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[pi / 2]], [0]), + (2, [[1, pi / 2, pi / 4]], [0, 1 / np.sqrt(2)]), + (3, [[0, 0, 0, pi, pi / 2, pi / 4]], + [-1, 0, 1 / np.sqrt(2)])]) + def test_output_local_field_rx(self, n_subsystems, weights, target, tol): + """Checks the output if the features are zero. Uses RX local fields.""" + + features = np.zeros(shape=(n_subsystems,)) + dev = qml.device('default.qubit', wires=n_subsystems) + + @qml.qnode(dev) + def circuit(x=None): + QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='X') + return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] + + res = circuit(x=features[:n_subsystems]) + assert np.allclose(res, target, atol=tol, rtol=0) + + @pytest.mark.parametrize('n_subsystems, weights, target', [(1, [[pi / 2]], [1]), + (2, [[1, pi / 2, pi / 4]], [1, 1]), + (3, [[0, 0, 0, pi, pi / 2, pi / 4]], [1, 1, 1])]) + def test_output_local_field_rz(self, n_subsystems, weights, target, tol): + """Checks the output if the features are zero. Uses RZ local fields.""" + + features = np.zeros(shape=(n_subsystems,)) + dev = qml.device('default.qubit', wires=n_subsystems) + + @qml.qnode(dev) + def circuit(x=None): + QAOAEmbedding(features=x, weights=weights, wires=range(n_subsystems), local_field='Z') + return [qml.expval(qml.PauliZ(i)) for i in range(n_subsystems)] + + res = circuit(x=features[:n_subsystems]) + assert np.allclose(res, target, atol=tol, rtol=0) + + @pytest.mark.parametrize('weights, target', [([[np.pi, 0, 0]], [1, 1]), + ([[np.pi / 2, 0, 0]], [0, 0]), + ([[0, 0, 0]], [-1, -1])]) + def test_output_zz(self, weights, target, tol): + """Checks the output if the features and entangler weights are nonzero.""" + + dev = qml.device('default.qubit', wires=2) + + @qml.qnode(dev) + def circuit(x=None): + QAOAEmbedding(features=x, weights=weights, wires=range(2)) + return [qml.expval(qml.PauliZ(i)) for i in range(2)] + + res = circuit(x=[np.pi/2, np.pi/2]) + + assert np.allclose(res, target, atol=tol, rtol=0) + + @pytest.mark.parametrize('n_wires, features, weights, target', [(2, [0], [[0, 0, np.pi / 2]], [1, 0]), + (3, [0, 0], [[0, 0, 0, 0, 0, np.pi / 2]], + [1, 1, 0])]) + def test_state_more_qubits_than_features(self, n_wires, features, weights, target, tol): + """Checks the state is correct if there are more qubits than features.""" + + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(x=None): + QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='Z') + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + res = circuit(x=features) + assert np.allclose(res, target, atol=tol, rtol=0) + + def test_exception_fewer_wires_than_features(self, ): + """Verifies that exception raised if there are fewer + wires than features.""" + + features = [0, 0, 0, 0] + n_wires = 1 + weights = np.zeros(shape=(1, 2 * n_wires)) + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(x=None): + QAOAEmbedding(features=x, weights=weights, wires=range(n_wires)) + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + with pytest.raises(ValueError, match="Features must be of "): + circuit(x=features) + + def test_exception_wrongrot(self): + """Verifies exception raised if the + rotation strategy is unknown.""" + + n_wires = 1 + weights = np.zeros(shape=(1, 1)) + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(x=None): + QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='A') + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + with pytest.raises(ValueError, match="did not recognize"): + circuit(x=[1]) + + def test_exception_wrong_dim(self): + """Verifies that exception is raised if the + number of dimensions of features is incorrect.""" + n_wires = 1 + weights = np.zeros(shape=(1, 1)) + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(x=None): + QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='A') + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + with pytest.raises(ValueError, match="Features must be a one-dimensional"): + circuit(x=[[1], [0]]) class TestDisplacementEmbedding: diff --git a/tests/templates/test_embeddings/test_qaoa.py b/tests/templates/test_embeddings/test_qaoa_embedding.py similarity index 100% rename from tests/templates/test_embeddings/test_qaoa.py rename to tests/templates/test_embeddings/test_qaoa_embedding.py diff --git a/tests/templates/test_integration.py b/tests/templates/test_integration.py index edf1c69f8d7..3bb341b708c 100644 --- a/tests/templates/test_integration.py +++ b/tests/templates/test_integration.py @@ -225,6 +225,24 @@ {'weights': qml.init.random_layers_normal(n_layers=2, n_rots=2, n_wires=1), 'wires': range(1)}), (qml.templates.RandomLayers, {'weights': qml.init.random_layers_normal(n_layers=2, n_rots=2, n_wires=2), 'wires': range(2)}), + (qml.templates.QAOAEmbedding, + {'features': [1.], 'weights': qml.init.qaoa_embedding_uniform(n_layers=3, n_wires=1), + 'wires': range(1)}), + (qml.templates.QAOAEmbedding, + {'features': [1., 2.], 'weights': qml.init.qaoa_embedding_uniform(n_layers=3, n_wires=2), + 'wires': range(2)}), + (qml.templates.QAOAEmbedding, + {'features': [1., 2., 3.], 'weights': qml.init.qaoa_embedding_uniform(n_layers=2, n_wires=3), + 'wires': range(3)}), + (qml.templates.QAOAEmbedding, + {'features': [1.], 'weights': qml.init.qaoa_embedding_normal(n_layers=3, n_wires=1), + 'wires': range(1)}), + (qml.templates.QAOAEmbedding, + {'features': [1., 2.], 'weights': qml.init.qaoa_embedding_normal(n_layers=3, n_wires=2), + 'wires': range(2)}), + (qml.templates.QAOAEmbedding, + {'features': [1., 2., 3.], 'weights': qml.init.qaoa_embedding_normal(n_layers=3, n_wires=3), + 'wires': range(3)}), (qml.templates.SimplifiedTwoDesign, {'initial_layer_weights': qml.init.simplified_two_design_initial_layer_uniform(n_wires=4), 'weights': qml.init.simplified_two_design_weights_uniform(n_layers=3, n_wires=4), diff --git a/tests/templates/test_layers.py b/tests/templates/test_layers.py index 6a2f766c75f..881f4263cbd 100644 --- a/tests/templates/test_layers.py +++ b/tests/templates/test_layers.py @@ -33,7 +33,6 @@ from pennylane.wires import Wires from pennylane.numpy import tensor from pennylane.init import particle_conserving_u1_normal -from pennylane.tape import QuantumTape TOLERANCE = 1e-8 @@ -610,6 +609,108 @@ def circuit(initial_layer, weights): circuit(initial_layer, weights) +class TestBasicEntangler: + """Tests for the BasicEntanglerLayers method from the pennylane.templates.layers module.""" + + @pytest.mark.parametrize("n_wires, n_cnots", [(1, 0), (2, 1), (3, 3), (4, 4)]) + def test_circuit_queue(self, n_wires, n_cnots): + """Tests the gate types in the circuit.""" + np.random.seed(42) + n_layers = 2 + + weights = np.random.randn(n_layers, n_wires) + + with qml.tape.OperationRecorder() as rec: + BasicEntanglerLayers(weights, wires=range(n_wires)) + + # Test that gates appear in the right order + exp_gates = [qml.RX] * n_wires + [qml.CNOT] * n_cnots + exp_gates *= n_layers + res_gates = rec.queue + + for op1, op2 in zip(res_gates, exp_gates): + assert isinstance(op1, op2) + + @pytest.mark.parametrize("n_wires, n_cnots", [(1, 0), (2, 1), (3, 3), (4, 4)]) + def test_circuit_parameters(self, n_wires, n_cnots): + """Tests the parameter values in the circuit.""" + np.random.seed(42) + n_layers = 2 + + weights = np.random.randn(n_layers, n_wires) + + with qml.tape.OperationRecorder() as rec: + BasicEntanglerLayers(weights, wires=range(n_wires)) + + # test the device parameters + for l in range(n_layers): + # only select the rotation gates + layer_ops = rec.queue[l * (n_wires + n_cnots) : l * (n_wires + n_cnots) + n_wires] + + # check each rotation gate parameter + for n in range(n_wires): + res_param = layer_ops[n].parameters[0] + exp_param = weights[l, n] + assert res_param == exp_param + + @pytest.mark.parametrize("rotation", [RX, RY, RZ]) + def test_custom_rotation(self, rotation): + """Tests that non-default rotation gates are used correctly.""" + n_layers = 2 + n_wires = 4 + weights = np.ones(shape=(n_layers, n_wires)) + + with qml.tape.OperationRecorder() as rec: + BasicEntanglerLayers(weights, wires=range(n_wires), rotation=rotation) + + # assert queue contains the custom rotations and CNOTs only + gates = rec.queue + for op in gates: + if not isinstance(op, CNOT): + assert isinstance(op, rotation) + + @pytest.mark.parametrize( + "weights, n_wires, target", + [ + ([[np.pi]], 1, [-1]), + ([[np.pi] * 2], 2, [-1, 1]), + ([[np.pi] * 3], 3, [1, 1, -1]), + ([[np.pi] * 4], 4, [-1, 1, -1, 1]), + ], + ) + def test_simple_target_outputs(self, weights, n_wires, target): + """Tests the result of the template for simple cases.""" + + dev = qml.device("default.qubit", wires=n_wires) + + @qml.qnode(dev) + def circuit(weights): + BasicEntanglerLayers(weights=weights, wires=range(n_wires), rotation=RX) + return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_wires)] + + expectations = circuit(weights) + for exp, target_exp in zip(expectations, target): + assert exp == target_exp + + def test_exception_wrong_dim(self): + """Verifies that exception is raised if the + number of dimensions of features is incorrect.""" + + n_wires = 1 + dev = qml.device('default.qubit', wires=n_wires) + + @qml.qnode(dev) + def circuit(weights): + BasicEntanglerLayers(weights=weights, wires=range(n_wires)) + return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + + with pytest.raises(ValueError, match="Weights tensor must be 2-dimensional"): + circuit([1, 0]) + + with pytest.raises(ValueError, match="Weights tensor must have second dimension of length"): + circuit([[1, 0], [1, 0]]) + + class TestParticleConservingU2: """Tests for the ParticleConservingU2 template from the pennylane.templates.layers module.""" diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py index 4a44ae11cc1..693ba96c60c 100644 --- a/tests/templates/test_subroutines.py +++ b/tests/templates/test_subroutines.py @@ -21,7 +21,6 @@ import pennylane as qml from pennylane import numpy as np from pennylane.wires import Wires -from pennylane.tape import QuantumTape from pennylane.templates.subroutines import ( Interferometer, @@ -572,6 +571,246 @@ def circuit(weights): circuit(weights) +class TestDoubleExcitationUnitary: + """Tests for the DoubleExcitationUnitary template from the + pennylane.templates.subroutine module.""" + + @pytest.mark.parametrize( + ("wires1", "wires2", "ref_gates"), + [ + ( + [0, 1, 2], + [4, 5, 6], + [ + [0, qml.Hadamard, [0], []], + [1, qml.Hadamard, [2], []], + [2, qml.RX, [4], [-np.pi / 2]], + [3, qml.Hadamard, [6], []], + [9, qml.RZ, [6], [np.pi / 24]], + [15, qml.Hadamard, [0], []], + [16, qml.Hadamard, [2], []], + [17, qml.RX, [4], [np.pi / 2]], + [18, qml.Hadamard, [6], []], + ], + ), + ( + [0, 1], + [4, 5], + [ + [15, qml.RX, [0], [-np.pi / 2]], + [16, qml.Hadamard, [1], []], + [17, qml.RX, [4], [-np.pi / 2]], + [18, qml.RX, [5], [-np.pi / 2]], + [22, qml.RZ, [5], [np.pi / 24]], + [26, qml.RX, [0], [np.pi / 2]], + [27, qml.Hadamard, [1], []], + [28, qml.RX, [4], [np.pi / 2]], + [29, qml.RX, [5], [np.pi / 2]], + ], + ), + ( + [1, 2, 3], + [7, 8, 9, 10, 11], + [ + [46, qml.Hadamard, [1], []], + [47, qml.RX, [3], [-np.pi / 2]], + [48, qml.RX, [7], [-np.pi / 2]], + [49, qml.RX, [11], [-np.pi / 2]], + [57, qml.RZ, [11], [np.pi / 24]], + [65, qml.Hadamard, [1], []], + [66, qml.RX, [3], [np.pi / 2]], + [67, qml.RX, [7], [np.pi / 2]], + [68, qml.RX, [11], [np.pi / 2]], + ], + ), + ( + [2, 3, 4], + [8, 9, 10], + [ + [57, qml.Hadamard, [2], []], + [58, qml.Hadamard, [4], []], + [59, qml.Hadamard, [8], []], + [60, qml.RX, [10], [-np.pi / 2]], + [66, qml.RZ, [10], [np.pi / 24]], + [72, qml.Hadamard, [2], []], + [73, qml.Hadamard, [4], []], + [74, qml.Hadamard, [8], []], + [75, qml.RX, [10], [np.pi / 2]], + ], + ), + ( + [3, 4, 5], + [11, 12, 13, 14, 15], + [ + [92, qml.RX, [3], [-np.pi / 2]], + [93, qml.Hadamard, [5], []], + [94, qml.Hadamard, [11], []], + [95, qml.Hadamard, [15], []], + [103, qml.RZ, [15], [-np.pi / 24]], + [111, qml.RX, [3], [np.pi / 2]], + [112, qml.Hadamard, [5], []], + [113, qml.Hadamard, [11], []], + [114, qml.Hadamard, [15], []], + ], + ), + ( + [4, 5, 6, 7], + [9, 10], + [ + [95, qml.Hadamard, [4], []], + [96, qml.RX, [7], [-np.pi / 2]], + [97, qml.Hadamard, [9], []], + [98, qml.Hadamard, [10], []], + [104, qml.RZ, [10], [-np.pi / 24]], + [110, qml.Hadamard, [4], []], + [111, qml.RX, [7], [np.pi / 2]], + [112, qml.Hadamard, [9], []], + [113, qml.Hadamard, [10], []], + ], + ), + ( + [5, 6], + [10, 11, 12], + [ + [102, qml.RX, [5], [-np.pi / 2]], + [103, qml.RX, [6], [-np.pi / 2]], + [104, qml.RX, [10], [-np.pi / 2]], + [105, qml.Hadamard, [12], []], + [110, qml.RZ, [12], [-np.pi / 24]], + [115, qml.RX, [5], [np.pi / 2]], + [116, qml.RX, [6], [np.pi / 2]], + [117, qml.RX, [10], [np.pi / 2]], + [118, qml.Hadamard, [12], []], + ], + ), + ( + [3, 4, 5, 6], + [17, 18, 19], + [ + [147, qml.RX, [3], [-np.pi / 2]], + [148, qml.RX, [6], [-np.pi / 2]], + [149, qml.Hadamard, [17], []], + [150, qml.RX, [19], [-np.pi / 2]], + [157, qml.RZ, [19], [-np.pi / 24]], + [164, qml.RX, [3], [np.pi / 2]], + [165, qml.RX, [6], [np.pi / 2]], + [166, qml.Hadamard, [17], []], + [167, qml.RX, [19], [np.pi / 2]], + ], + ), + ( + [6, 7], + [8, 9], + [ + [4, qml.CNOT, [6, 7], []], + [5, qml.CNOT, [7, 8], []], + [6, qml.CNOT, [8, 9], []], + [8, qml.CNOT, [8, 9], []], + [9, qml.CNOT, [7, 8], []], + [10, qml.CNOT, [6, 7], []], + ], + ), + ( + [4, 5, 6, 7], + [8, 9, 10, 11, 12, 13], + [ + [58, qml.CNOT, [4, 5], []], + [59, qml.CNOT, [5, 6], []], + [60, qml.CNOT, [6, 7], []], + [61, qml.CNOT, [7, 8], []], + [62, qml.CNOT, [8, 9], []], + [63, qml.CNOT, [9, 10], []], + [64, qml.CNOT, [10, 11], []], + [65, qml.CNOT, [11, 12], []], + [66, qml.CNOT, [12, 13], []], + [122, qml.CNOT, [12, 13], []], + [123, qml.CNOT, [11, 12], []], + [124, qml.CNOT, [10, 11], []], + [125, qml.CNOT, [9, 10], []], + [126, qml.CNOT, [8, 9], []], + [127, qml.CNOT, [7, 8], []], + [128, qml.CNOT, [6, 7], []], + [129, qml.CNOT, [5, 6], []], + [130, qml.CNOT, [4, 5], []], + ], + ), + ], + ) + def test_double_ex_unitary_operations(self, wires1, wires2, ref_gates): + """Test the correctness of the DoubleExcitationUnitary template including the gate count + and order, the wires each operation acts on and the correct use of parameters + in the circuit.""" + + sqg = 72 + cnots = 16 * (len(wires1) - 1 + len(wires2) - 1 + 1) + weight = np.pi / 3 + with qml.tape.OperationRecorder() as rec: + DoubleExcitationUnitary(weight, wires1=wires1, wires2=wires2) + + assert len(rec.queue) == sqg + cnots + + for gate in ref_gates: + idx = gate[0] + + exp_gate = gate[1] + res_gate = rec.queue[idx] + assert isinstance(res_gate, exp_gate) + + exp_wires = gate[2] + res_wires = rec.queue[idx]._wires + assert res_wires == Wires(exp_wires) + + exp_weight = gate[3] + res_weight = rec.queue[idx].parameters + assert res_weight == exp_weight + + @pytest.mark.parametrize( + ("weight", "wires1", "wires2", "msg_match"), + [ + (0.2, [0], [1, 2], "expected at least two wires representing the occupied"), + (0.2, [0, 1], [2], "expected at least two wires representing the unoccupied"), + (0.2, [0], [1], "expected at least two wires representing the occupied"), + ([0.2, 1.1], [0, 2], [4, 6], "Weight must be a scalar"), + ], + ) + def test_double_excitation_unitary_exceptions(self, weight, wires1, wires2, msg_match): + """Test that DoubleExcitationUnitary throws an exception if ``weight`` or + ``pphh`` parameter has illegal shapes, types or values.""" + dev = qml.device("default.qubit", wires=10) + + def circuit(weight=weight): + DoubleExcitationUnitary(weight=weight, wires1=wires1, wires2=wires2) + return qml.expval(qml.PauliZ(0)) + + qnode = qml.QNode(circuit, dev) + + with pytest.raises(ValueError, match=msg_match): + qnode(weight=weight) + + @pytest.mark.parametrize( + ("weight", "wires1", "wires2", "expected"), + [ + (1.34817, [0, 1], [3, 4], [0.22079189, 0.22079189, 1.0, -0.22079189, -0.22079189]), + (0.84817, [1, 2], [3, 4], [1.0, 0.66135688, 0.66135688, -0.66135688, -0.66135688]), + ], + ) + def test_integration(self, weight, wires1, wires2, expected, tol): + """Test integration with PennyLane and gradient calculations""" + + N = 5 + dev = qml.device("default.qubit", wires=N) + + @qml.qnode(dev) + def circuit(weight): + init_state = np.array([0, 0, 0, 1, 1], requires_grad=False) + qml.BasisState(init_state, wires=range(N)) + DoubleExcitationUnitary(weight, wires1=wires1, wires2=wires2) + return [qml.expval(qml.PauliZ(w)) for w in range(N)] + + res = circuit(weight) + assert np.allclose(res, np.array(expected), atol=tol) + + class TestUCCSDUnitary: """Tests for the UCCSD template from the pennylane.templates.subroutine module.""" @@ -667,36 +906,24 @@ def test_uccsd_operations(self, s_wires, d_wires, weights, ref_gates): ref_state = np.array([1, 1, 0, 0, 0, 0]) - with QuantumTape() as tape: + with qml.tape.OperationRecorder() as rec: UCCSD(weights, wires, s_wires=s_wires, d_wires=d_wires, init_state=ref_state) - mixed_queue = tape.operations - - # hack: replace DoubleExcitationUnitary with list of gates - queue = [] - for g in mixed_queue: - if type(g) == DoubleExcitationUnitary: - with QuantumTape() as tape2: - g.decomposition(*g.parameters, g.wires) - queue.extend(tape2.operations) - else: - queue.append(g) - - assert len(queue) == sqg + cnots + 1 + assert len(rec.queue) == sqg + cnots + 1 for gate in ref_gates: idx = gate[0] exp_gate = gate[1] - res_gate = queue[idx] + res_gate = rec.queue[idx] assert isinstance(res_gate, exp_gate) exp_wires = gate[2] - res_wires = queue[idx]._wires + res_wires = rec.queue[idx]._wires assert res_wires == Wires(exp_wires) exp_weight = gate[3] - res_weight = queue[idx].parameters + res_weight = rec.queue[idx].parameters if exp_gate != qml.BasisState: assert res_weight == exp_weight else: From f31fd43f3f725fe1d2adf13c369041ea2b4de2b0 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Tue, 16 Mar 2021 10:22:56 +0200 Subject: [PATCH 39/64] fix some stuff --- pennylane/templates/layers/basic_entangler.py | 2 +- tests/templates/test_embeddings.py | 11 +- tests/templates/test_layer.py | 6 +- tests/templates/test_layers.py | 57 ---- .../test_qchem/test_double_excitation.py | 271 ------------------ 5 files changed, 5 insertions(+), 342 deletions(-) delete mode 100644 tests/templates/test_qchem/test_double_excitation.py diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 9631dd47742..20b5e23589f 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -145,7 +145,7 @@ def expand(self): for layer in range(repeat): for i in range(len(self.wires)): - self.rotation(weights[layer, i], wires=self.wires[i:i+1]) + self.rotation(weights[layer][i], wires=self.wires[i:i+1]) broadcast(unitary=qml.CNOT, pattern="ring", wires=self.wires) diff --git a/tests/templates/test_embeddings.py b/tests/templates/test_embeddings.py index 2ac1203df24..fa69b034d3b 100644 --- a/tests/templates/test_embeddings.py +++ b/tests/templates/test_embeddings.py @@ -499,15 +499,6 @@ class TestQAOAEmbedding: (3, (1, 6), [qml.RX, qml.RX, qml.RX, qml.MultiRZ, qml.MultiRZ, qml.MultiRZ, qml.RY, qml.RY, qml.RY, qml.RX, qml.RX, qml.RX])] - @pytest.mark.parametrize('n_wires, weight_shape, expected_queue', QUEUES) - def test_queue(self, n_wires, weight_shape, expected_queue): - """Checks the queue for the default settings.""" - - with qml.tape.OperationRecorder() as rec: - QAOAEmbedding(features=list(range(n_wires)), weights=np.zeros(shape=weight_shape), wires=range(n_wires)) - - for gate, expected_gate in zip(rec.queue, expected_queue): - assert isinstance(gate, expected_gate) def test_state_zero_weights(self, qubit_device, n_subsystems, tol): """Checks the state produced by QAOAEmbedding() is correct if the weights are zero.""" @@ -659,7 +650,7 @@ def test_exception_wrong_dim(self): @qml.qnode(dev) def circuit(x=None): - QAOAEmbedding(features=x, weights=weights, wires=range(n_wires), local_field='A') + QAOAEmbedding(features=x, weights=weights, wires=range(n_wires)) return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] with pytest.raises(ValueError, match="Features must be a one-dimensional"): diff --git a/tests/templates/test_layer.py b/tests/templates/test_layer.py index 05d9ca19210..29f992184ae 100644 --- a/tests/templates/test_layer.py +++ b/tests/templates/test_layer.py @@ -63,8 +63,7 @@ def MultiCircuit(parameters1, parameters2, var1, wires, var2): qml.RY(parameters1[i], wires=w) if var1 == True: - op = qml.templates.BasicEntanglerLayers([parameters2], wires=wires, do_queue=False) - op.decomposition([parameters2], wires=wires) + qml.templates.BasicEntanglerLayers([parameters2], wires=wires) UNITARIES = [ ConstantCircuit, @@ -81,7 +80,8 @@ def MultiCircuit(parameters1, parameters2, var1, wires, var2): [qml.CNOT(wires=[3, 1]), qml.Hadamard(wires=1), qml.PauliY(wires=2), qml.Hadamard(wires=0)], [qml.CNOT(wires=[3, 1]), qml.Hadamard(wires=1), qml.PauliY(wires=2), qml.Hadamard(wires=0), qml.CNOT(wires=[3, 1]), qml.Hadamard(wires=1), qml.PauliY(wires=2), qml.Hadamard(wires=[0])], [qml.RX(0.5, wires=0), qml.RX(0.5, wires=1), qml.MultiRZ(0.3, wires=[0, 1])], - [qml.RY(0.5, wires=0), qml.RY(0.4, wires=1), qml.RX(0.4, wires=0), qml.RX(0.4, wires=1), qml.CNOT(wires=[0, 1]), qml.RY(0.5, wires=0), qml.RY(0.4, wires=1)] + [qml.RY(0.5, wires=0), qml.RY(0.4, wires=1), qml.templates.BasicEntanglerLayers([[0.5, 0.4]], wires=[0, 1]), + qml.RY(0.5, wires=0), qml.RY(0.4, wires=1), qml.templates.BasicEntanglerLayers([[0.5, 0.4]], wires=[0, 1])] ] ARGS = [ [], [], [], [ [ [[0.5, 0.5], 0.3] ] ], [ [[0.5, 0.4], [0.5, 0.4]], [[0.4, 0.4], []], [True, False] ] ] diff --git a/tests/templates/test_layers.py b/tests/templates/test_layers.py index 881f4263cbd..cf342cd2579 100644 --- a/tests/templates/test_layers.py +++ b/tests/templates/test_layers.py @@ -612,63 +612,6 @@ def circuit(initial_layer, weights): class TestBasicEntangler: """Tests for the BasicEntanglerLayers method from the pennylane.templates.layers module.""" - @pytest.mark.parametrize("n_wires, n_cnots", [(1, 0), (2, 1), (3, 3), (4, 4)]) - def test_circuit_queue(self, n_wires, n_cnots): - """Tests the gate types in the circuit.""" - np.random.seed(42) - n_layers = 2 - - weights = np.random.randn(n_layers, n_wires) - - with qml.tape.OperationRecorder() as rec: - BasicEntanglerLayers(weights, wires=range(n_wires)) - - # Test that gates appear in the right order - exp_gates = [qml.RX] * n_wires + [qml.CNOT] * n_cnots - exp_gates *= n_layers - res_gates = rec.queue - - for op1, op2 in zip(res_gates, exp_gates): - assert isinstance(op1, op2) - - @pytest.mark.parametrize("n_wires, n_cnots", [(1, 0), (2, 1), (3, 3), (4, 4)]) - def test_circuit_parameters(self, n_wires, n_cnots): - """Tests the parameter values in the circuit.""" - np.random.seed(42) - n_layers = 2 - - weights = np.random.randn(n_layers, n_wires) - - with qml.tape.OperationRecorder() as rec: - BasicEntanglerLayers(weights, wires=range(n_wires)) - - # test the device parameters - for l in range(n_layers): - # only select the rotation gates - layer_ops = rec.queue[l * (n_wires + n_cnots) : l * (n_wires + n_cnots) + n_wires] - - # check each rotation gate parameter - for n in range(n_wires): - res_param = layer_ops[n].parameters[0] - exp_param = weights[l, n] - assert res_param == exp_param - - @pytest.mark.parametrize("rotation", [RX, RY, RZ]) - def test_custom_rotation(self, rotation): - """Tests that non-default rotation gates are used correctly.""" - n_layers = 2 - n_wires = 4 - weights = np.ones(shape=(n_layers, n_wires)) - - with qml.tape.OperationRecorder() as rec: - BasicEntanglerLayers(weights, wires=range(n_wires), rotation=rotation) - - # assert queue contains the custom rotations and CNOTs only - gates = rec.queue - for op in gates: - if not isinstance(op, CNOT): - assert isinstance(op, rotation) - @pytest.mark.parametrize( "weights, n_wires, target", [ diff --git a/tests/templates/test_qchem/test_double_excitation.py b/tests/templates/test_qchem/test_double_excitation.py deleted file mode 100644 index bb70afb1ba4..00000000000 --- a/tests/templates/test_qchem/test_double_excitation.py +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the DoubleExcitationUnitary template. -""" -import pytest -import numpy as np -import pennylane as qml - - -class TestDecomposition: - """Tests that the template defines the correct decomposition.""" - - @pytest.mark.parametrize( - ("wires1", "wires2", "ref_gates"), - [ - ( - [0, 1, 2], - [4, 5, 6], - [ - [0, qml.Hadamard, [0], []], - [1, qml.Hadamard, [2], []], - [2, qml.RX, [4], [-np.pi / 2]], - [3, qml.Hadamard, [6], []], - [9, qml.RZ, [6], [np.pi / 24]], - [15, qml.Hadamard, [0], []], - [16, qml.Hadamard, [2], []], - [17, qml.RX, [4], [np.pi / 2]], - [18, qml.Hadamard, [6], []], - ], - ), - ( - [0, 1], - [4, 5], - [ - [15, qml.RX, [0], [-np.pi / 2]], - [16, qml.Hadamard, [1], []], - [17, qml.RX, [4], [-np.pi / 2]], - [18, qml.RX, [5], [-np.pi / 2]], - [22, qml.RZ, [5], [np.pi / 24]], - [26, qml.RX, [0], [np.pi / 2]], - [27, qml.Hadamard, [1], []], - [28, qml.RX, [4], [np.pi / 2]], - [29, qml.RX, [5], [np.pi / 2]], - ], - ), - ( - [1, 2, 3], - [7, 8, 9, 10, 11], - [ - [46, qml.Hadamard, [1], []], - [47, qml.RX, [3], [-np.pi / 2]], - [48, qml.RX, [7], [-np.pi / 2]], - [49, qml.RX, [11], [-np.pi / 2]], - [57, qml.RZ, [11], [np.pi / 24]], - [65, qml.Hadamard, [1], []], - [66, qml.RX, [3], [np.pi / 2]], - [67, qml.RX, [7], [np.pi / 2]], - [68, qml.RX, [11], [np.pi / 2]], - ], - ), - ( - [2, 3, 4], - [8, 9, 10], - [ - [57, qml.Hadamard, [2], []], - [58, qml.Hadamard, [4], []], - [59, qml.Hadamard, [8], []], - [60, qml.RX, [10], [-np.pi / 2]], - [66, qml.RZ, [10], [np.pi / 24]], - [72, qml.Hadamard, [2], []], - [73, qml.Hadamard, [4], []], - [74, qml.Hadamard, [8], []], - [75, qml.RX, [10], [np.pi / 2]], - ], - ), - ( - [3, 4, 5], - [11, 12, 13, 14, 15], - [ - [92, qml.RX, [3], [-np.pi / 2]], - [93, qml.Hadamard, [5], []], - [94, qml.Hadamard, [11], []], - [95, qml.Hadamard, [15], []], - [103, qml.RZ, [15], [-np.pi / 24]], - [111, qml.RX, [3], [np.pi / 2]], - [112, qml.Hadamard, [5], []], - [113, qml.Hadamard, [11], []], - [114, qml.Hadamard, [15], []], - ], - ), - ( - [4, 5, 6, 7], - [9, 10], - [ - [95, qml.Hadamard, [4], []], - [96, qml.RX, [7], [-np.pi / 2]], - [97, qml.Hadamard, [9], []], - [98, qml.Hadamard, [10], []], - [104, qml.RZ, [10], [-np.pi / 24]], - [110, qml.Hadamard, [4], []], - [111, qml.RX, [7], [np.pi / 2]], - [112, qml.Hadamard, [9], []], - [113, qml.Hadamard, [10], []], - ], - ), - ( - [5, 6], - [10, 11, 12], - [ - [102, qml.RX, [5], [-np.pi / 2]], - [103, qml.RX, [6], [-np.pi / 2]], - [104, qml.RX, [10], [-np.pi / 2]], - [105, qml.Hadamard, [12], []], - [110, qml.RZ, [12], [-np.pi / 24]], - [115, qml.RX, [5], [np.pi / 2]], - [116, qml.RX, [6], [np.pi / 2]], - [117, qml.RX, [10], [np.pi / 2]], - [118, qml.Hadamard, [12], []], - ], - ), - ( - [3, 4, 5, 6], - [17, 18, 19], - [ - [147, qml.RX, [3], [-np.pi / 2]], - [148, qml.RX, [6], [-np.pi / 2]], - [149, qml.Hadamard, [17], []], - [150, qml.RX, [19], [-np.pi / 2]], - [157, qml.RZ, [19], [-np.pi / 24]], - [164, qml.RX, [3], [np.pi / 2]], - [165, qml.RX, [6], [np.pi / 2]], - [166, qml.Hadamard, [17], []], - [167, qml.RX, [19], [np.pi / 2]], - ], - ), - ( - [6, 7], - [8, 9], - [ - [4, qml.CNOT, [6, 7], []], - [5, qml.CNOT, [7, 8], []], - [6, qml.CNOT, [8, 9], []], - [8, qml.CNOT, [8, 9], []], - [9, qml.CNOT, [7, 8], []], - [10, qml.CNOT, [6, 7], []], - ], - ), - ( - [4, 5, 6, 7], - [8, 9, 10, 11, 12, 13], - [ - [58, qml.CNOT, [4, 5], []], - [59, qml.CNOT, [5, 6], []], - [60, qml.CNOT, [6, 7], []], - [61, qml.CNOT, [7, 8], []], - [62, qml.CNOT, [8, 9], []], - [63, qml.CNOT, [9, 10], []], - [64, qml.CNOT, [10, 11], []], - [65, qml.CNOT, [11, 12], []], - [66, qml.CNOT, [12, 13], []], - [122, qml.CNOT, [12, 13], []], - [123, qml.CNOT, [11, 12], []], - [124, qml.CNOT, [10, 11], []], - [125, qml.CNOT, [9, 10], []], - [126, qml.CNOT, [8, 9], []], - [127, qml.CNOT, [7, 8], []], - [128, qml.CNOT, [6, 7], []], - [129, qml.CNOT, [5, 6], []], - [130, qml.CNOT, [4, 5], []], - ], - ), - ], - ) - def test_double_ex_unitary_operations(self, wires1, wires2, ref_gates): - """Test the correctness of the DoubleExcitationUnitary template including the gate count - and order, the wires each operation acts on and the correct use of parameters - in the circuit.""" - - sqg = 72 - cnots = 16 * (len(wires1) - 1 + len(wires2) - 1 + 1) - weight = np.pi / 3 - - op = qml.templates.DoubleExcitationUnitary(weight, wires1=wires1, wires2=wires2) - tape = op.expand() - queue = tape.operations - - assert len(queue) == sqg + cnots - - for gate in ref_gates: - idx = gate[0] - - exp_gate = gate[1] - res_gate = queue[idx] - assert type(res_gate) == exp_gate - - exp_wires = gate[2] - res_wires = queue[idx]._wires - assert res_wires == qml.wires.Wires(exp_wires) - - exp_weight = gate[3] - res_weight = queue[idx].parameters - assert res_weight == exp_weight - - def test_custom_wire_labels(self, tol): - """Test that template can deal with non-numeric, nonconsecutive wire labels.""" - weight = np.random.random() - - dev = qml.device("default.qubit", wires=6) - dev2 = qml.device("default.qubit", wires=["z", "a", "k", "b", "t", "mm"]) - - @qml.qnode(dev) - def circuit(): - qml.templates.DoubleExcitationUnitary(weight, wires1=[0, 3, 4], wires2=[2, 5, 1]) - return qml.expval(qml.Identity(0)) - - @qml.qnode(dev2) - def circuit2(): - qml.templates.DoubleExcitationUnitary( - weight, wires1=["z", "b", "t"], wires2=["k", "mm", "a"] - ) - return qml.expval(qml.Identity("z")) - - circuit() - circuit2() - - assert np.allclose(dev.state, dev2.state, atol=tol, rtol=0) - - -class TestParameters: - """Test inputs and pre-processing.""" - - @pytest.mark.parametrize( - ("weight", "wires1", "wires2", "msg_match"), - [ - (0.2, [0], [1, 2], "expected at least two wires representing the occupied"), - (0.2, [0, 1], [2], "expected at least two wires representing the unoccupied"), - (0.2, [0], [1], "expected at least two wires representing the occupied"), - ([0.2, 1.1], [0, 2], [4, 6], "Weight must be a scalar"), - ], - ) - def test_double_excitation_unitary_exceptions(self, weight, wires1, wires2, msg_match): - """Test that DoubleExcitationUnitary throws an exception if ``weight`` or - ``pphh`` parameter has illegal shapes, types or values.""" - dev = qml.device("default.qubit", wires=10) - - def circuit(weight=weight): - qml.templates.DoubleExcitationUnitary(weight=weight, wires1=wires1, wires2=wires2) - return qml.expval(qml.PauliZ(0)) - - qnode = qml.QNode(circuit, dev) - - with pytest.raises(ValueError, match=msg_match): - qnode(weight=weight) - - -class TestGradients: - """Tests that the gradient is computed correctly in all three interfaces.""" - - # ToDo: Add! From bed00d78462ffb56839d6bca9975a3290af5ff17 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Tue, 16 Mar 2021 10:37:54 +0200 Subject: [PATCH 40/64] some polishing --- .../test_ansaetze/test_basic_entangler.py | 4 +++- .../test_embeddings/test_qaoa_embedding.py | 2 +- tests/test_init.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/templates/test_ansaetze/test_basic_entangler.py b/tests/templates/test_ansaetze/test_basic_entangler.py index 7171a447f29..f7c0c788ffb 100644 --- a/tests/templates/test_ansaetze/test_basic_entangler.py +++ b/tests/templates/test_ansaetze/test_basic_entangler.py @@ -115,6 +115,8 @@ def circuit2(): class TestParameters: + """Test inputs and pre-processing.""" + def test_exception_wrong_dim(self): """Verifies that exception is raised if the weights shape is incorrect.""" @@ -150,7 +152,7 @@ def test_shape_random_weights(self, n_layers, n_wires, expected_shape): class TestGradients: - """Tests that the gradient is computed correctly in all three interfaces.""" + """Tests that the gradient is computed correctly in all interfaces.""" def test_autograd(self, tol): """Tests that gradients of template and decomposed circuit diff --git a/tests/templates/test_embeddings/test_qaoa_embedding.py b/tests/templates/test_embeddings/test_qaoa_embedding.py index 1b98dde2f67..578cb84e39a 100644 --- a/tests/templates/test_embeddings/test_qaoa_embedding.py +++ b/tests/templates/test_embeddings/test_qaoa_embedding.py @@ -257,7 +257,7 @@ def test_shape_random_weights(self, n_layers, n_wires, expected_shape): class TestGradients: - """Tests that the gradient is computed correctly in all three interfaces.""" + """Tests that the gradient is computed correctly in all interfaces.""" def test_autograd(self, tol): """Tests that gradients of template and decomposed circuit diff --git a/tests/test_init.py b/tests/test_init.py index 12a8de668d3..935329af6d0 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -190,6 +190,24 @@ (qml.init.interferometer_varphi_uniform, {'n_wires': 1, 'low': 0, 'high': 1}, (1,)), + (qml.init.qaoa_embedding_normal, + {'n_layers': 2, 'n_wires': 3, 'mean': 0, 'std': 1}, + (2, 2*3)), + (qml.init.qaoa_embedding_uniform, + {'n_layers': 2, 'n_wires': 3, 'low': 0, 'high': 1}, + (2, 2*3)), + (qml.init.qaoa_embedding_uniform, + {'n_layers': 2, 'n_wires': 1, 'low': 0, 'high': 1}, + (2, 1)), + (qml.init.qaoa_embedding_uniform, + {'n_layers': 2, 'n_wires': 2, 'low': 0, 'high': 1}, + (2, 3)), + (qml.init.qaoa_embedding_normal, + {'n_layers': 2, 'n_wires': 1, 'mean': 0, 'std': 1}, + (2, 1)), + (qml.init.qaoa_embedding_normal, + {'n_layers': 2, 'n_wires': 2, 'mean': 0, 'std': 1}, + (2, 3)), (qml.init.simplified_two_design_initial_layer_uniform, {'n_wires': 1, 'low': 0, 'high': 1}, (1,)), From a39ad531320218ba1d386a78ce9440078f390dce Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Tue, 16 Mar 2021 10:43:07 +0200 Subject: [PATCH 41/64] black --- pennylane/templates/embeddings/qaoa.py | 2 +- pennylane/templates/layers/basic_entangler.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index 6af3b9f9f90..cc25d32960f 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -59,7 +59,7 @@ def qaoa_ising_hamiltonian(weights, wires, local_fields): for i in range(len(wires)): qml.MultiRZ(weights[i], wires=wires.subset([i, i + 1], periodic_boundary=True)) for i in range(len(wires)): - local_fields(weights[len(wires)+i], wires=wires[i]) + local_fields(weights[len(wires) + i], wires=wires[i]) class QAOAEmbedding(Operation): diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 20b5e23589f..3696fdfe996 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -145,7 +145,7 @@ def expand(self): for layer in range(repeat): for i in range(len(self.wires)): - self.rotation(weights[layer][i], wires=self.wires[i:i+1]) + self.rotation(weights[layer][i], wires=self.wires[i : i + 1]) broadcast(unitary=qml.CNOT, pattern="ring", wires=self.wires) From 84989ccdd73066ccccfde45e7963c1ff16e777e5 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Tue, 16 Mar 2021 11:35:41 +0200 Subject: [PATCH 42/64] increase coverage --- .../test_ansaetze/test_basic_entangler.py | 12 ++++++- .../test_embeddings/test_qaoa_embedding.py | 32 +++++++++++++------ 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/tests/templates/test_ansaetze/test_basic_entangler.py b/tests/templates/test_ansaetze/test_basic_entangler.py index f7c0c788ffb..1b3b0364450 100644 --- a/tests/templates/test_ansaetze/test_basic_entangler.py +++ b/tests/templates/test_ansaetze/test_basic_entangler.py @@ -143,14 +143,24 @@ def circuit(weights): ], ) def test_shape_random_weights(self, n_layers, n_wires, expected_shape): + """Test that the random weights have the correct shape""" weights1 = qml.templates.BasicEntanglerLayers.weights_uniform(n_layers, n_wires) weights2 = qml.templates.BasicEntanglerLayers.weights_normal(n_layers, n_wires) - assert weights1.shape == expected_shape assert weights2.shape == expected_shape + @pytest.mark.parametrize("func", [qml.templates.BasicEntanglerLayers.weights_uniform, + qml.templates.BasicEntanglerLayers.weights_normal]) + def test_seed_random_weights(self, func, tol): + """Test that the random weights are made deterministic by using a seed""" + + w_42 = func(3, 4, seed=42) + w_41 = func(3, 4, seed=41) + assert np.allclose(w_42, w_42, atol=tol, rtol=0) + assert not np.allclose(w_42, w_41, atol=tol, rtol=0) + class TestGradients: """Tests that the gradient is computed correctly in all interfaces.""" diff --git a/tests/templates/test_embeddings/test_qaoa_embedding.py b/tests/templates/test_embeddings/test_qaoa_embedding.py index 578cb84e39a..a5d2257a63d 100644 --- a/tests/templates/test_embeddings/test_qaoa_embedding.py +++ b/tests/templates/test_embeddings/test_qaoa_embedding.py @@ -209,7 +209,7 @@ def circuit(x=None): with pytest.raises(ValueError, match="Features must be of "): circuit(x=features) - def test_exception_wrong_feauture_shape(self): + def test_exception_wrong_feature_shape(self): """Verifies that exception is raised if the shape of features is incorrect.""" n_wires = 1 weights = np.zeros(shape=(1, 1)) @@ -224,17 +224,20 @@ def circuit(): with pytest.raises(ValueError, match="Features must be a one-dimensional"): circuit() - def test_exception_wrong_weight_shape(self): + @pytest.mark.parametrize('weights, n_wires', [ + (np.zeros(shape=(1, 2)), 1), + (np.zeros(shape=(1, 4)), 2), + (np.zeros(shape=(1, 3)), 3) + ]) + def test_exception_wrong_weight_shape(self, weights, n_wires): """Verifies that exception is raised if the shape of weights is incorrect.""" - n_wires = 2 - weights = np.zeros(shape=(1, 4)) - features = np.zeros(shape=(2,)) + features = np.zeros(shape=(n_wires,)) dev = qml.device("default.qubit", wires=n_wires) @qml.qnode(dev) def circuit(): qml.templates.QAOAEmbedding(features, weights, wires=range(n_wires)) - return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)] + return qml.expval(qml.PauliZ(0)) with pytest.raises(ValueError, match="Weights tensor must be of shape"): circuit() @@ -248,13 +251,22 @@ def circuit(): ], ) def test_shape_random_weights(self, n_layers, n_wires, expected_shape): - - weights1 = qml.templates.QAOAEmbedding.weights_uniform(n_layers, n_wires) - weights2 = qml.templates.QAOAEmbedding.weights_normal(n_layers, n_wires) - + """Test that the random weights have the correct shape""" + weights1 = qml.templates.QAOAEmbedding.weights_uniform(n_layers, n_wires, seed=42) + weights2 = qml.templates.QAOAEmbedding.weights_normal(n_layers, n_wires, seed=42) assert weights1.shape == expected_shape assert weights2.shape == expected_shape + @pytest.mark.parametrize("func", [qml.templates.QAOAEmbedding.weights_uniform, + qml.templates.QAOAEmbedding.weights_normal]) + def test_seed_random_weights(self, func, tol): + """Test that the random weights are made deterministic by using a seed""" + + w_42 = func(3, 4, seed=42) + w_41 = func(3, 4, seed=41) + assert np.allclose(w_42, w_42, atol=tol, rtol=0) + assert not np.allclose(w_42, w_41, atol=tol, rtol=0) + class TestGradients: """Tests that the gradient is computed correctly in all interfaces.""" From 006bc97757fcd1e4366cc8bff38c8a3277696c9d Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:21:12 +0800 Subject: [PATCH 43/64] Update tests/interfaces/test_qnode_torch.py Co-authored-by: Christina Lee --- tests/interfaces/test_qnode_torch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/test_qnode_torch.py b/tests/interfaces/test_qnode_torch.py index e7a684d0299..75e03bf7b87 100644 --- a/tests/interfaces/test_qnode_torch.py +++ b/tests/interfaces/test_qnode_torch.py @@ -351,7 +351,7 @@ def circuit(U, a): res.backward() assert np.allclose(a.grad, np.sin(a_val), atol=tol, rtol=0) - def test_differentiable_expand(self, dev_name, diff_method, mocker, tol): + def test_differentiable_expand(self, dev_name, diff_method, tol): """Test that operation and nested tapes expansion is differentiable""" From 595186dc39ab403eb103baf886d04e9bebd28ebe Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:21:26 +0800 Subject: [PATCH 44/64] Update tests/interfaces/test_qnode_torch.py Co-authored-by: Christina Lee --- tests/interfaces/test_qnode_torch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/test_qnode_torch.py b/tests/interfaces/test_qnode_torch.py index 75e03bf7b87..e5726388b95 100644 --- a/tests/interfaces/test_qnode_torch.py +++ b/tests/interfaces/test_qnode_torch.py @@ -564,7 +564,7 @@ def construct(self, args, kwargs): return new_qnode -def test_transform(monkeypatch, tol): +def test_transform(tol): """Test an example transform""" dev = qml.device("default.qubit", wires=1) From 16ba95ca9404f104a8d46537574049a9e258195d Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:21:41 +0800 Subject: [PATCH 45/64] Update tests/interfaces/test_qnode_autograd.py Co-authored-by: Christina Lee --- tests/interfaces/test_qnode_autograd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/test_qnode_autograd.py b/tests/interfaces/test_qnode_autograd.py index 17bbe2c2efc..04a4082a130 100644 --- a/tests/interfaces/test_qnode_autograd.py +++ b/tests/interfaces/test_qnode_autograd.py @@ -379,7 +379,7 @@ def circuit(U, a): res = qml.grad(circuit)(U, a) assert np.allclose(res, np.sin(a), atol=tol, rtol=0) - def test_differentiable_expand(self, dev_name, diff_method, mocker, tol): + def test_differentiable_expand(self, dev_name, diff_method, tol): """Test that operation and nested tapes expansion is differentiable""" From 9c999100fcab73db41f2c8e72672d12cf9fcdc2e Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:23:16 +0800 Subject: [PATCH 46/64] Update tests/interfaces/test_qnode_autograd.py Co-authored-by: Christina Lee --- tests/interfaces/test_qnode_autograd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/test_qnode_autograd.py b/tests/interfaces/test_qnode_autograd.py index 04a4082a130..549171b57e0 100644 --- a/tests/interfaces/test_qnode_autograd.py +++ b/tests/interfaces/test_qnode_autograd.py @@ -754,7 +754,7 @@ def construct(self, args, kwargs): "dev_name,diff_method", [("default.qubit", "finite-diff"), ("default.qubit.autograd", "backprop")], ) -def test_transform(dev_name, diff_method, monkeypatch, tol): +def test_transform(dev_name, diff_method, tol): """Test an example transform""" dev = qml.device(dev_name, wires=1) From 72c688ecaf5f0aa96d5cb446f606e52ae7e3ea9a Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:23:27 +0800 Subject: [PATCH 47/64] Update tests/interfaces/test_qnode_tf.py Co-authored-by: Christina Lee --- tests/interfaces/test_qnode_tf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/test_qnode_tf.py b/tests/interfaces/test_qnode_tf.py index 636fc9b1d6a..42aadb58378 100644 --- a/tests/interfaces/test_qnode_tf.py +++ b/tests/interfaces/test_qnode_tf.py @@ -368,7 +368,7 @@ def circuit(U, a): res = tape.jacobian(res, a) assert np.allclose(res, tf.sin(a), atol=tol, rtol=0) - def test_differentiable_expand(self, dev_name, diff_method, mocker, tol): + def test_differentiable_expand(self, dev_name, diff_method, tol): """Test that operation and nested tapes expansion is differentiable""" class U3(qml.U3): From 78846307c79f13f071ccf122e13afea94cac9b62 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:23:43 +0800 Subject: [PATCH 48/64] Update tests/interfaces/test_tape_tf.py Co-authored-by: Christina Lee --- tests/interfaces/test_tape_tf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/test_tape_tf.py b/tests/interfaces/test_tape_tf.py index 30978851cca..77e8c42c43c 100644 --- a/tests/interfaces/test_tape_tf.py +++ b/tests/interfaces/test_tape_tf.py @@ -313,7 +313,7 @@ def test_matrix_parameter(self, U, tol): res = tape.jacobian(res, a) assert np.allclose(res, tf.sin(a), atol=tol, rtol=0) - def test_differentiable_expand(self, mocker, tol): + def test_differentiable_expand(self, tol): """Test that operation and nested tapes expansion is differentiable""" From 4eb9b76ad15e50e7007c6f6d5cbdbc3fa1b1061e Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:23:57 +0800 Subject: [PATCH 49/64] Update tests/interfaces/test_qnode_tf.py Co-authored-by: Christina Lee --- tests/interfaces/test_qnode_tf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/test_qnode_tf.py b/tests/interfaces/test_qnode_tf.py index 42aadb58378..a7a82ddcbc5 100644 --- a/tests/interfaces/test_qnode_tf.py +++ b/tests/interfaces/test_qnode_tf.py @@ -562,7 +562,7 @@ def construct(self, args, kwargs): @pytest.mark.parametrize( "dev_name,diff_method", [("default.qubit", "finite-diff"), ("default.qubit.tf", "backprop")] ) -def test_transform(dev_name, diff_method, monkeypatch, tol): +def test_transform(dev_name, diff_method, tol): """Test an example transform""" dev = qml.device(dev_name, wires=1) From ebea0b0e7f98f42313e4c0c8377619ec56c28e29 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:24:27 +0800 Subject: [PATCH 50/64] Update tests/interfaces/test_tape_autograd.py Co-authored-by: Christina Lee --- tests/interfaces/test_tape_autograd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/test_tape_autograd.py b/tests/interfaces/test_tape_autograd.py index 81af633f98a..0f6a29f712e 100644 --- a/tests/interfaces/test_tape_autograd.py +++ b/tests/interfaces/test_tape_autograd.py @@ -251,7 +251,7 @@ def cost(a, U, device): res = jac_fn(a, U, device=dev) assert np.allclose(res, np.sin(a), atol=tol, rtol=0) - def test_differentiable_expand(self, mocker, tol): + def test_differentiable_expand(self, tol): """Test that operation and nested tapes expansion is differentiable""" From b86e6132fb45da7268ca0a36ea7c50dce640f1e1 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:24:41 +0800 Subject: [PATCH 51/64] Update tests/interfaces/test_tape_torch.py Co-authored-by: Christina Lee --- tests/interfaces/test_tape_torch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/test_tape_torch.py b/tests/interfaces/test_tape_torch.py index baf2f1b2f11..144104e1d78 100644 --- a/tests/interfaces/test_tape_torch.py +++ b/tests/interfaces/test_tape_torch.py @@ -287,7 +287,7 @@ def test_matrix_parameter(self, U, tol): res.backward() assert np.allclose(a.grad, np.sin(a_val), atol=tol, rtol=0) - def test_differentiable_expand(self, mocker, tol): + def test_differentiable_expand(self, tol): """Test that operation and nested tapes expansion is differentiable""" From 4cad2e44df8f527527c9dadd673118a9a6de7b52 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:25:38 +0800 Subject: [PATCH 52/64] Update pennylane/interfaces/torch.py Co-authored-by: Christina Lee --- pennylane/interfaces/torch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py index 84b996fe3a9..aef6c222fb4 100644 --- a/pennylane/interfaces/torch.py +++ b/pennylane/interfaces/torch.py @@ -161,7 +161,7 @@ class MyTorchQuantumTape(TorchInterface, JacobianTape): providing the ``dtype`` argument when applying the interface: >>> p = torch.tensor([0.1, 0.2, 0.3], requires_grad=True) - >>> with TorchInterface.apply(JacobianTape()) as qtape: + >>> with TorchInterface.apply(JacobianTape(), dtype=torch.float32) as qtape: ... qml.Rot(p[0], p[1] ** 2 + p[0] * p[2], p[1] * torch.sin(p[2]), wires=0) ... expval(qml.PauliX(0)) >>> result = qtape.execute(dev) From c0632db41dc7687c204f78dccd075bce356acdad Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 16 Mar 2021 20:31:03 +0800 Subject: [PATCH 53/64] suggested changes --- pennylane/tape/tape.py | 6 +- tests/{tape => }/interfaces/test_qnode_jax.py | 0 tests/{tape => }/interfaces/test_tape_jax.py | 0 tests/transforms/test_draw.py | 149 ++++++++++++++++++ 4 files changed, 152 insertions(+), 3 deletions(-) rename tests/{tape => }/interfaces/test_qnode_jax.py (100%) rename tests/{tape => }/interfaces/test_tape_jax.py (100%) create mode 100644 tests/transforms/test_draw.py diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py index f515fd83174..ae42d55bd7a 100644 --- a/pennylane/tape/tape.py +++ b/pennylane/tape/tape.py @@ -261,9 +261,9 @@ class QuantumTape(AnnotatedQueue): .. code-block:: python - with qml.tape.QuantumTape() as tape1: - with qml.tape.QuantumTape(do_queue=False) as tape2: - qml.RX(0.123, wires=0) + with qml.tape.QuantumTape() as tape1: + with qml.tape.QuantumTape(do_queue=False) as tape2: + qml.RX(0.123, wires=0) Here, tape2 records the RX gate, but tape1 doesn't record tape2. diff --git a/tests/tape/interfaces/test_qnode_jax.py b/tests/interfaces/test_qnode_jax.py similarity index 100% rename from tests/tape/interfaces/test_qnode_jax.py rename to tests/interfaces/test_qnode_jax.py diff --git a/tests/tape/interfaces/test_tape_jax.py b/tests/interfaces/test_tape_jax.py similarity index 100% rename from tests/tape/interfaces/test_tape_jax.py rename to tests/interfaces/test_tape_jax.py diff --git a/tests/transforms/test_draw.py b/tests/transforms/test_draw.py new file mode 100644 index 00000000000..fa0a4b2259e --- /dev/null +++ b/tests/transforms/test_draw.py @@ -0,0 +1,149 @@ +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests for the draw transform. +""" +import pytest + +import pennylane as qml +from pennylane import numpy as np + + +def test_drawing(): + """Test circuit drawing""" + + x = np.array(0.1, requires_grad=True) + y = np.array([0.2, 0.3], requires_grad=True) + z = np.array(0.4, requires_grad=True) + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev, interface="autograd") + def circuit(p1, p2=y, **kwargs): + qml.RX(p1, wires=0) + qml.RY(p2[0] * p2[1], wires=1) + qml.RX(kwargs["p3"], wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) + + result = qml.draw(circuit)(p1=x, p3=z) + expected = """\ + 0: ──RX(0.1)───RX(0.4)──╭C──╭┤ ⟨Z ⊗ X⟩ + 1: ──RY(0.06)───────────╰X──╰┤ ⟨Z ⊗ X⟩ +""" + + assert result == expected + + +def test_drawing_ascii(): + """Test circuit drawing when using ASCII characters""" + from pennylane import numpy as np + + x = np.array(0.1, requires_grad=True) + y = np.array([0.2, 0.3], requires_grad=True) + z = np.array(0.4, requires_grad=True) + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev, interface="autograd") + def circuit(p1, p2=y, **kwargs): + qml.RX(p1, wires=0) + qml.RY(p2[0] * p2[1], wires=1) + qml.RX(kwargs["p3"], wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) + + result = qml.draw(circuit, charset="ascii")(p1=x, p3=z) + expected = """\ + 0: --RX(0.1)---RX(0.4)--+C--+| + 1: --RY(0.06)-----------+X--+| +""" + + assert result == expected + + +def test_show_all_wires_error(): + """Test that show_all_wires will raise an error if the provided wire + order does not contain all wires on the device""" + + dev = qml.device('default.qubit', wires=[-1, "a", "q2", 0]) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(wires=-1) + qml.CNOT(wires=[-1, "q2"]) + return qml.expval(qml.PauliX(wires="q2")) + + with pytest.raises(ValueError, match="must contain all wires"): + qml.draw(circuit, show_all_wires=True, wire_order=[-1, "a"])() + + +def test_missing_wire(): + """Test that wires not specifically mentioned in the wire + reordering are appended at the bottom of the circuit drawing""" + + dev = qml.device('default.qubit', wires=["a", -1, "q2"]) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(wires=-1) + qml.CNOT(wires=["a", "q2"]) + qml.RX(0.2, wires="a") + return qml.expval(qml.PauliX(wires="q2")) + + # test one missing wire + res = qml.draw(circuit, wire_order=["q2", "a"])() + expected = [ + " q2: ──╭X───────────┤ ⟨X⟩ ", + " a: ──╰C──RX(0.2)──┤ ", + " -1: ───H───────────┤ \n" + ] + + assert res == "\n".join(expected) + + # test one missing wire + res = qml.draw(circuit, wire_order=["q2", -1])() + expected = [ + " q2: ─────╭X───────────┤ ⟨X⟩ ", + " -1: ──H──│────────────┤ ", + " a: ─────╰C──RX(0.2)──┤ \n" + ] + + assert res == "\n".join(expected) + + # test multiple missing wires + res = qml.draw(circuit, wire_order=["q2"])() + expected = [ + " q2: ─────╭X───────────┤ ⟨X⟩ ", + " -1: ──H──│────────────┤ ", + " a: ─────╰C──RX(0.2)──┤ \n" + ] + + assert res == "\n".join(expected) + + +def test_invalid_wires(): + """Test that an exception is raised if a wire in the wire + ordering does not exist on the device""" + dev = qml.device('default.qubit', wires=["a", -1, "q2"]) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(wires=-1) + qml.CNOT(wires=["a", "q2"]) + qml.RX(0.2, wires="a") + return qml.expval(qml.PauliX(wires="q2")) + + with pytest.raises(ValueError, match="contains wires not contained on the device"): + qml.draw(circuit, wire_order=["q2", 5])() From 0343c22373393845d7d7b9a92ee5edca2b446de4 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Thu, 18 Mar 2021 14:02:09 +0800 Subject: [PATCH 54/64] Update pennylane/qnode.py Co-authored-by: Theodor --- pennylane/qnode.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pennylane/qnode.py b/pennylane/qnode.py index 8f14a9dd62d..95d1abc1529 100644 --- a/pennylane/qnode.py +++ b/pennylane/qnode.py @@ -666,7 +666,6 @@ def circuit(): a: ──╰C──RX(0.2)──┤ -1: ───H───────────┤ """ - # Currently it only exists to match the signature of non-tape mode draw. if self.qtape is None: raise qml.QuantumFunctionError( "The QNode can only be drawn after its quantum tape has been constructed." From e54694be139ae4d9f6a2888540c0f8a33d5900a2 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Thu, 18 Mar 2021 14:04:28 +0800 Subject: [PATCH 55/64] suggested changes --- .../templates/layers/simplified_two_design.py | 4 +-- .../circuit_graph/test_circuit_graph_hash.py | 35 ------------------- tests/tape/test_qnode.py | 2 +- 3 files changed, 3 insertions(+), 38 deletions(-) diff --git a/pennylane/templates/layers/simplified_two_design.py b/pennylane/templates/layers/simplified_two_design.py index 559431b63c9..bcf7ba87b6f 100644 --- a/pennylane/templates/layers/simplified_two_design.py +++ b/pennylane/templates/layers/simplified_two_design.py @@ -61,8 +61,8 @@ def entangler(par1, par2, wires): """Implements a two qubit unitary consisting of a controlled-Z entangler and Pauli-Y rotations. Args: - par1 (float or qml.Variable): parameter of first Pauli-Y rotation - par2 (float or qml.Variable): parameter of second Pauli-Y rotation + par1 (float): parameter of first Pauli-Y rotation + par2 (float): parameter of second Pauli-Y rotation wires (Wires): two wire indices that unitary acts on """ diff --git a/tests/circuit_graph/test_circuit_graph_hash.py b/tests/circuit_graph/test_circuit_graph_hash.py index e221a949e00..7b9fc1d9395 100644 --- a/tests/circuit_graph/test_circuit_graph_hash.py +++ b/tests/circuit_graph/test_circuit_graph_hash.py @@ -184,41 +184,6 @@ def circuit2(x, y): assert circuit_hash_1 == circuit_hash_2 - @pytest.mark.parametrize( - "a,b", - zip(np.linspace(0.1, 2 * np.pi, 3), np.linspace(0, 2 * np.pi, 3)), - ) - @pytest.mark.parametrize( - "x,y", - zip(np.linspace(-2 * np.pi, 0, 3), np.linspace(-2 * np.pi, 0, 3)), - ) - @pytest.mark.xfail(reason="This test will not work in tape mode") - def test_evaluate_circuit_hash_symbolic_assigned_arguments_do_not_matter(self, a, b, x, y): - """Tests that the circuit hashes of identical circuits where different values are assigned to symbolic parameters are equal""" - dev = qml.device("default.qubit", wires=2) - - def circuit1(a, b): - qml.RX(a, wires=[0]) - qml.RY(b, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - node1 = qml.QNode(circuit1, dev) - node1(a, b) - circuit_hash_1 = node1.qtape.graph.hash - - def circuit2(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - node2 = qml.QNode(circuit2, dev) - node2(x, y) - circuit_hash_2 = node2.qtape.graph.hash - - assert circuit_hash_1 == circuit_hash_2 - @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), diff --git a/tests/tape/test_qnode.py b/tests/tape/test_qnode.py index ba2c6fa2ca9..e4efc63074c 100644 --- a/tests/tape/test_qnode.py +++ b/tests/tape/test_qnode.py @@ -764,7 +764,7 @@ class TestQNodeCollection: """Unittests for the QNodeCollection""" def test_multi_thread(self): - """Test that multi-threaded queuing in tape mode works correctly""" + """Test that multi-threaded queuing works correctly""" n_qubits = 4 n_batches = 5 dev = qml.device("default.qubit", wires=n_qubits) From 032d2b104c98efc59cc72cd87d1f8a717d5b4e1f Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Thu, 18 Mar 2021 14:18:15 +0800 Subject: [PATCH 56/64] fix test --- tests/templates/test_subroutines_qmc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/templates/test_subroutines_qmc.py b/tests/templates/test_subroutines_qmc.py index e86bd4f4061..a64b111bdf3 100644 --- a/tests/templates/test_subroutines_qmc.py +++ b/tests/templates/test_subroutines_qmc.py @@ -240,7 +240,7 @@ def test_expected_circuit(self): p = np.ones(4) / 4 target_wires, estimation_wires = Wires(range(3)), Wires(range(3, 5)) - with qml.tape.tapes.QuantumTape() as tape: + with qml.tape.QuantumTape() as tape: QuantumMonteCarlo(p, self.func, target_wires, estimation_wires) queue_before_qpe = tape.queue[:2] @@ -259,7 +259,7 @@ def test_expected_circuit(self): Q = make_Q(A, R) - with qml.tape.tapes.QuantumTape() as qpe_tape: + with qml.tape.QuantumTape() as qpe_tape: qml.templates.QuantumPhaseEstimation(Q, target_wires, estimation_wires) assert len(queue_after_qpe) == len(qpe_tape.queue) From 672e5e9d05d636e028b09a2acf6f06f9424bc62d Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 22 Mar 2021 07:35:07 +0200 Subject: [PATCH 57/64] changed weight initialisation methods to return shape --- pennylane/templates/embeddings/qaoa.py | 59 ++++--------------- pennylane/templates/layers/basic_entangler.py | 58 ++++++------------ .../test_ansaetze/test_basic_entangler.py | 20 ++----- .../test_embeddings/test_qaoa_embedding.py | 21 ++----- 4 files changed, 39 insertions(+), 119 deletions(-) diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index cc25d32960f..8530f1bddca 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -137,14 +137,16 @@ def circuit(weights, f=None): print(circuit(weights, f=features)) - **Using parameter initialization functions** + **Parameter shape** - A random numpy weights array can be generated using the static methods - `QAOAEmbedding.weights_normal` and `QAOAEmbedding.weights_uniform`. + The shape of the weights argument can be computed by the static method + :meth:`QAOAEmbedding.parameter_shape` and used when creating randomly + initialised weight tensors: .. code-block:: python - weights = QAOAEmbedding.weights_normal(n_layers=2, n_wires=2, mean=0, std=0.2) + shape = QAOAEmbedding.shape(n_layers=2, n_wires=2) + weights = np.random.random(shape) **Training the embedding** @@ -280,56 +282,19 @@ def _preprocess(self): ) @staticmethod - def weights_uniform(n_layers, n_wires, low=0, high=2 * np.pi, seed=None): - r"""Creates a standard numpy weights array whose entries are drawn from a uniform - distribution. + def shape(n_layers, n_wires): + r"""Returns the shape of the weight tensor required for this template. Args: n_layers (int): number of layers n_wires (int): number of qubits - low (float): minimum value of uniform distribution - high (float): maximum value of uniform distribution - seed (int): seed used in sampling the parameters, makes function call deterministic Returns: - array: numpy array + tuple[int]: shape """ - if seed is not None: - np.random.seed(seed) if n_wires == 1: - shp = (n_layers, 1) + return n_layers, 1 elif n_wires == 2: - shp = (n_layers, 3) - else: - shp = (n_layers, 2 * n_wires) - - params = np.random.uniform(low=low, high=high, size=shp) - return params - - @staticmethod - def weights_normal(n_layers, n_wires, mean=0, std=0.1, seed=None): - r"""Creates a standard numpy weights array whose entries are drawn from a normal distribution. - - Args: - n_layers (int): number of layers - n_wires (int): number of qubits - mean (float): mean of parameters - std (float): standard deviation of parameters - seed (int): seed used in sampling the parameters, makes function call deterministic - - Returns: - array: numpy array - """ - if seed is not None: - np.random.seed(seed) - - if n_wires == 1: - shp = (n_layers, 1) - elif n_wires == 2: - shp = (n_layers, 3) - else: - shp = (n_layers, 2 * n_wires) - - params = np.random.normal(loc=mean, scale=std, size=shp) - return params + return n_layers, 3 + return n_layers, 2 * n_wires diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 3696fdfe996..46143c510b9 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -79,14 +79,16 @@ def circuit(weights): >>> circuit([[pi, pi, pi]]) [1., 1., -1.] - **Parameter initialization function** + **Parameter shape** - A random numpy weights array can be generated using the static methods - `BasicEntanglerLayers.weights_normal` and `BasicEntanglerLayers.weights_uniform`. + The shape of the weights argument can be computed by the static method + :meth:`BasicEntanglerLayers.parameter_shape` and used when creating randomly + initialised weight tensors: .. code-block:: python - weights = BasicEntanglerLayers.weights_normal(n_layers=2, n_wires=2, mean=0, std=0.2) + shape = BasicEntanglerLayers.parameter_shape(n_layers=2, n_wires=2) + weights = np.random.random(size=shape) **No periodic boundary for two wires** @@ -145,9 +147,15 @@ def expand(self): for layer in range(repeat): for i in range(len(self.wires)): - self.rotation(weights[layer][i], wires=self.wires[i : i + 1]) + self.rotation(weights[layer][i], wires=self.wires[i: i + 1]) - broadcast(unitary=qml.CNOT, pattern="ring", wires=self.wires) + if len(self.wires) == 2: + qml.CNOT(wires=self.wires) + + elif len(self.wires) > 2: + for i in range(len(self.wires)): + w = self.wires.subset([i, i + 1], periodic_boundary=True) + qml.CNOT(wires=w) return tape @@ -168,45 +176,15 @@ def _preprocess(self): ) @staticmethod - def weights_normal(n_layers, n_wires, mean=0, std=0.1, seed=None): - r"""Creates a standard numpy weights array whose entries are drawn from a normal - distribution. + def shape(n_layers, n_wires): + r"""Returns the shape of the weight tensor required for this template. Args: n_layers (int): number of layers n_wires (int): number of qubits - mean (float): mean of parameters - std (float): standard deviation of parameters - seed (int): seed used in sampling the parameters, makes function call deterministic Returns: - array: weights array + tuple[int]: shape """ - if seed is not None: - np.random.seed(seed) - - params = np.random.normal(loc=mean, scale=std, size=(n_layers, n_wires)) - - return params - - @staticmethod - def weights_uniform(n_layers, n_wires, low=0, high=2 * np.pi, seed=None): - r"""Creates a standard numpy weights array whose entries are drawn from a uniform - distribution. - - Args: - n_layers (int): number of layers - n_wires (int): number of qubits - low (float): minimum value of uniform distribution - high (float): maximum value of uniform distribution - seed (int): seed used in sampling the parameters, makes function call deterministic - - Returns: - array: weights array - """ - if seed is not None: - np.random.seed(seed) - - params = np.random.uniform(low=low, high=high, size=(n_layers, n_wires)) - return params + return n_layers, n_wires diff --git a/tests/templates/test_ansaetze/test_basic_entangler.py b/tests/templates/test_ansaetze/test_basic_entangler.py index 1b3b0364450..10e223e19da 100644 --- a/tests/templates/test_ansaetze/test_basic_entangler.py +++ b/tests/templates/test_ansaetze/test_basic_entangler.py @@ -142,25 +142,13 @@ def circuit(weights): (2, 2, (2, 2)), ], ) - def test_shape_random_weights(self, n_layers, n_wires, expected_shape): - """Test that the random weights have the correct shape""" + def test_shape(self, n_layers, n_wires, expected_shape): + """Test that the shape method returns the correct shape of the weights tensor""" - weights1 = qml.templates.BasicEntanglerLayers.weights_uniform(n_layers, n_wires) - weights2 = qml.templates.BasicEntanglerLayers.weights_normal(n_layers, n_wires) - assert weights1.shape == expected_shape - assert weights2.shape == expected_shape + shape = qml.templates.BasicEntanglerLayers.shape(n_layers, n_wires) + assert shape == expected_shape - @pytest.mark.parametrize("func", [qml.templates.BasicEntanglerLayers.weights_uniform, - qml.templates.BasicEntanglerLayers.weights_normal]) - def test_seed_random_weights(self, func, tol): - """Test that the random weights are made deterministic by using a seed""" - w_42 = func(3, 4, seed=42) - w_41 = func(3, 4, seed=41) - assert np.allclose(w_42, w_42, atol=tol, rtol=0) - assert not np.allclose(w_42, w_41, atol=tol, rtol=0) - - class TestGradients: """Tests that the gradient is computed correctly in all interfaces.""" diff --git a/tests/templates/test_embeddings/test_qaoa_embedding.py b/tests/templates/test_embeddings/test_qaoa_embedding.py index a5d2257a63d..003392ceda5 100644 --- a/tests/templates/test_embeddings/test_qaoa_embedding.py +++ b/tests/templates/test_embeddings/test_qaoa_embedding.py @@ -250,22 +250,11 @@ def circuit(): (2, 2, (2, 3)), ], ) - def test_shape_random_weights(self, n_layers, n_wires, expected_shape): - """Test that the random weights have the correct shape""" - weights1 = qml.templates.QAOAEmbedding.weights_uniform(n_layers, n_wires, seed=42) - weights2 = qml.templates.QAOAEmbedding.weights_normal(n_layers, n_wires, seed=42) - assert weights1.shape == expected_shape - assert weights2.shape == expected_shape - - @pytest.mark.parametrize("func", [qml.templates.QAOAEmbedding.weights_uniform, - qml.templates.QAOAEmbedding.weights_normal]) - def test_seed_random_weights(self, func, tol): - """Test that the random weights are made deterministic by using a seed""" - - w_42 = func(3, 4, seed=42) - w_41 = func(3, 4, seed=41) - assert np.allclose(w_42, w_42, atol=tol, rtol=0) - assert not np.allclose(w_42, w_41, atol=tol, rtol=0) + def test_shape(self, n_layers, n_wires, expected_shape): + """Test that the shape method returns the correct shape of the weights tensor""" + + shape = qml.templates.QAOAEmbedding.shape(n_layers, n_wires) + assert shape == expected_shape class TestGradients: From 2cb10ded73320569de0111c94bc9deeb18f75b0e Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 22 Mar 2021 07:39:03 +0200 Subject: [PATCH 58/64] black --- pennylane/templates/layers/basic_entangler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 46143c510b9..4bfa5861ed7 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -147,7 +147,7 @@ def expand(self): for layer in range(repeat): for i in range(len(self.wires)): - self.rotation(weights[layer][i], wires=self.wires[i: i + 1]) + self.rotation(weights[layer][i], wires=self.wires[i : i + 1]) if len(self.wires) == 2: qml.CNOT(wires=self.wires) From 4c2b807eaa7d926eeaa5b431f7f74193b0423ce6 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 22 Mar 2021 08:14:17 +0200 Subject: [PATCH 59/64] simplify preprocess logic --- pennylane/templates/embeddings/qaoa.py | 31 ++++++++++--------- pennylane/templates/layers/basic_entangler.py | 22 ++++++------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index 8530f1bddca..e124b4a19cd 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -140,7 +140,7 @@ def circuit(weights, f=None): **Parameter shape** The shape of the weights argument can be computed by the static method - :meth:`QAOAEmbedding.parameter_shape` and used when creating randomly + :meth:`~.QAOAEmbedding.shape` and used when creating randomly initialised weight tensors: .. code-block:: python @@ -218,13 +218,14 @@ def __init__(self, features, weights, wires, local_field="Y", do_queue=True): else: raise ValueError(f"did not recognize local field {local_field}") + wires = Wires(wires) + self._preprocess(features, weights, wires) super().__init__(features, weights, wires=wires, do_queue=do_queue) - self._preprocess() def expand(self): - features = self.data[0] - weights = self.data[1] + features = self.parameters[0] + weights = self.parameters[1] # first dimension of the weights tensor determines # the number of layers @@ -242,17 +243,19 @@ def expand(self): return tape - def _preprocess(self): + @staticmethod + def _preprocess(features, weights, wires): """Validate and pre-process inputs as follows: * Check that the features tensor is one-dimensional. * Check that the first dimension of the features tensor has length :math:`n` or less, where :math:`n` is the number of qubits. * Check that the shape of the weights tensor is correct for the number of qubits. - """ - features = self.parameters[0] - weights = self.parameters[1] + Args: + features (tensor-like): feature tensor + weights (tensor-like): weight tensor + """ shape = qml.math.shape(features) @@ -260,25 +263,25 @@ def _preprocess(self): raise ValueError(f"Features must be a one-dimensional tensor; got shape {shape}.") n_features = shape[0] - if n_features > len(self.wires): + if n_features > len(wires): raise ValueError( - f"Features must be of length {len(self.wires)} or less; got length {n_features}." + f"Features must be of length {len(wires)} or less; got length {n_features}." ) shape = qml.math.shape(weights) repeat = shape[0] - if len(self.wires) == 1: + if len(wires) == 1: if shape != (repeat, 1): raise ValueError(f"Weights tensor must be of shape {(repeat, 1)}; got {shape}") - elif len(self.wires) == 2: + elif len(wires) == 2: if shape != (repeat, 3): raise ValueError(f"Weights tensor must be of shape {(repeat, 3)}; got {shape}") else: - if shape != (repeat, 2 * len(self.wires)): + if shape != (repeat, 2 * len(wires)): raise ValueError( - f"Weights tensor must be of shape {(repeat, 2*len(self.wires))}; got {shape}" + f"Weights tensor must be of shape {(repeat, 2*len(wires))}; got {shape}" ) @staticmethod diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 4bfa5861ed7..43a9ec1e278 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -14,10 +14,8 @@ r""" Contains the ``BasicEntanglerLayers`` template. """ -import numpy as np import pennylane as qml from pennylane.operation import Operation, AnyWires -from pennylane.templates import broadcast from pennylane.wires import Wires @@ -82,12 +80,12 @@ def circuit(weights): **Parameter shape** The shape of the weights argument can be computed by the static method - :meth:`BasicEntanglerLayers.parameter_shape` and used when creating randomly + :meth:`~.BasicEntanglerLayers.shape` and used when creating randomly initialised weight tensors: .. code-block:: python - shape = BasicEntanglerLayers.parameter_shape(n_layers=2, n_wires=2) + shape = BasicEntanglerLayers.shape(n_layers=2, n_wires=2) weights = np.random.random(size=shape) **No periodic boundary for two wires** @@ -132,12 +130,13 @@ def __init__(self, weights, wires=None, rotation=None, do_queue=True): self.rotation = rotation or qml.RX + wires = Wires(wires) + self._preprocess(weights, wires) super().__init__(weights, wires=wires, do_queue=do_queue) - self._preprocess() def expand(self): - weights = self.data[0] + weights = self.parameters[0] # first dimension of the weights tensor determines # the number of layers @@ -147,7 +146,7 @@ def expand(self): for layer in range(repeat): for i in range(len(self.wires)): - self.rotation(weights[layer][i], wires=self.wires[i : i + 1]) + self.rotation(weights[layer][i], wires=self.wires[i: i + 1]) if len(self.wires) == 2: qml.CNOT(wires=self.wires) @@ -159,20 +158,21 @@ def expand(self): return tape - def _preprocess(self): + @staticmethod + def _preprocess(weights, wires): """Validate and pre-process inputs as follows: * Check the shape of the weights tensor, making sure that the second dimension has length :math:`n`, where :math:`n` is the number of qubits. """ - shape = qml.math.shape(self.parameters[0]) + shape = qml.math.shape(weights) if len(shape) != 2: raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") - if shape[1] != len(self.wires): + if shape[1] != len(wires): raise ValueError( - f"Weights tensor must have second dimension of length {len(self.wires)}; got {shape[1]}" + f"Weights tensor must have second dimension of length {len(wires)}; got {shape[1]}" ) @staticmethod From 408d1c8a4c59973c7ce79d1734308eea7661b5b8 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 22 Mar 2021 08:35:57 +0200 Subject: [PATCH 60/64] black --- pennylane/templates/layers/basic_entangler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index 43a9ec1e278..c948cc9f56b 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -146,7 +146,7 @@ def expand(self): for layer in range(repeat): for i in range(len(self.wires)): - self.rotation(weights[layer][i], wires=self.wires[i: i + 1]) + self.rotation(weights[layer][i], wires=self.wires[i : i + 1]) if len(self.wires) == 2: qml.CNOT(wires=self.wires) From c62cfde2c0b51dd21f57ed630d394ff36bf98683 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 22 Mar 2021 08:37:56 +0200 Subject: [PATCH 61/64] use pl numpy in autograd tests --- tests/templates/test_ansaetze/test_basic_entangler.py | 2 ++ tests/templates/test_embeddings/test_qaoa_embedding.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/templates/test_ansaetze/test_basic_entangler.py b/tests/templates/test_ansaetze/test_basic_entangler.py index 10e223e19da..87e91e52cf9 100644 --- a/tests/templates/test_ansaetze/test_basic_entangler.py +++ b/tests/templates/test_ansaetze/test_basic_entangler.py @@ -17,6 +17,7 @@ import pytest import numpy as np import pennylane as qml +from pennylane import numpy as pnp def circuit_template(weights): @@ -157,6 +158,7 @@ def test_autograd(self, tol): are the same in the autograd interface.""" weights = np.random.random(size=(1, 3)) + weights = pnp.array(weights, requires_grad=True) dev = qml.device("default.qubit", wires=3) diff --git a/tests/templates/test_embeddings/test_qaoa_embedding.py b/tests/templates/test_embeddings/test_qaoa_embedding.py index 003392ceda5..97f88b241e6 100644 --- a/tests/templates/test_embeddings/test_qaoa_embedding.py +++ b/tests/templates/test_embeddings/test_qaoa_embedding.py @@ -17,7 +17,7 @@ import pytest import numpy as np import pennylane as qml - +from pennylane import numpy as pnp def circuit_template(features, weights): qml.templates.QAOAEmbedding(features, weights, range(2)) @@ -265,7 +265,10 @@ def test_autograd(self, tol): are the same in the autograd interface.""" features = np.random.random(size=(2,)) + features = pnp.array(features, requires_grad=True) + weights = np.random.random(size=(1, 3)) + weights = pnp.array(weights, requires_grad=True) dev = qml.device("default.qubit", wires=2) From 8d1303c4d3bc5b4d87a4f0c076d9b9ea1550ee08 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Mon, 22 Mar 2021 16:00:07 +0200 Subject: [PATCH 62/64] update docstring --- .github/CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 4d2a09cfb05..9d0140201a9 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -234,6 +234,18 @@

Improvements

+- The `QAOAEmbedding` and `BasicEntanglerLayers` are now classes inheriting + from `Operation`, and define the ansatz in their `expand()` method. This + change does not affect the user interface. + + For convenience, the class has a method that returns the shape of the + trainable parameter tensor, i.e., + + ```python + shape = qml.templates.BasicEntanglerLayers.shape(n_layers=2, n_wires=4) + weights = np.random.random(shape) + ``` + - ``QubitUnitary`` now validates to ensure the input matrix is two dimensional. [(#1128)](https://github.com/PennyLaneAI/pennylane/pull/1128) From 4132efd5a54605a4bdc8f08a5c252fcedb36bad8 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Tue, 23 Mar 2021 11:04:04 +0200 Subject: [PATCH 63/64] polish --- pennylane/templates/embeddings/qaoa.py | 5 ++-- pennylane/templates/layers/basic_entangler.py | 28 ++++++------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index e124b4a19cd..6ede166a932 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -15,7 +15,6 @@ Contains the QAOAEmbedding template. """ # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access -import numpy as np import pennylane as qml from pennylane.operation import Operation, AnyWires from pennylane.wires import Wires @@ -298,6 +297,8 @@ def shape(n_layers, n_wires): if n_wires == 1: return n_layers, 1 - elif n_wires == 2: + + if n_wires == 2: return n_layers, 3 + return n_layers, 2 * n_wires diff --git a/pennylane/templates/layers/basic_entangler.py b/pennylane/templates/layers/basic_entangler.py index c948cc9f56b..556f94a6e83 100644 --- a/pennylane/templates/layers/basic_entangler.py +++ b/pennylane/templates/layers/basic_entangler.py @@ -14,6 +14,7 @@ r""" Contains the ``BasicEntanglerLayers`` template. """ +# pylint: disable=consider-using-enumerate import pennylane as qml from pennylane.operation import Operation, AnyWires from pennylane.wires import Wires @@ -130,8 +131,14 @@ def __init__(self, weights, wires=None, rotation=None, do_queue=True): self.rotation = rotation or qml.RX - wires = Wires(wires) - self._preprocess(weights, wires) + shape = qml.math.shape(weights) + if len(shape) != 2: + raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") + if shape[1] != len(wires): + raise ValueError( + f"Weights tensor must have second dimension of length {len(wires)}; got {shape[1]}" + ) + super().__init__(weights, wires=wires, do_queue=do_queue) def expand(self): @@ -158,23 +165,6 @@ def expand(self): return tape - @staticmethod - def _preprocess(weights, wires): - """Validate and pre-process inputs as follows: - - * Check the shape of the weights tensor, making sure that the second dimension - has length :math:`n`, where :math:`n` is the number of qubits. - """ - shape = qml.math.shape(weights) - - if len(shape) != 2: - raise ValueError(f"Weights tensor must be 2-dimensional; got shape {shape}") - - if shape[1] != len(wires): - raise ValueError( - f"Weights tensor must have second dimension of length {len(wires)}; got {shape[1]}" - ) - @staticmethod def shape(n_layers, n_wires): r"""Returns the shape of the weight tensor required for this template. From 92466bb0534655e898b802a1466d0201f4dbd7f2 Mon Sep 17 00:00:00 2001 From: Maria Schuld Date: Tue, 23 Mar 2021 11:04:44 +0200 Subject: [PATCH 64/64] merge master --- pennylane/templates/embeddings/qaoa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/templates/embeddings/qaoa.py b/pennylane/templates/embeddings/qaoa.py index 6ede166a932..6ef6d1fb129 100644 --- a/pennylane/templates/embeddings/qaoa.py +++ b/pennylane/templates/embeddings/qaoa.py @@ -300,5 +300,5 @@ def shape(n_layers, n_wires): if n_wires == 2: return n_layers, 3 - + return n_layers, 2 * n_wires