diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 7d1cf6c49e5..211983c0aad 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,28 @@

New features since last release

+* The new ``qml.apply`` function can be used to add operations that might have + already been instantiated elsewhere to the QNode and other queuing contexts: + [(#1433)](https://github.com/PennyLaneAI/pennylane/pull/1433) + + ```python + op = qml.RX(0.4, wires=0) + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(x): + qml.RY(x, wires=0) + qml.apply(op) + return qml.expval(qml.PauliZ(0)) + ``` + + ```pycon + >>> print(qml.draw(circuit)(0.6)) + 0: ──RY(0.6)──RX(0.4)──┤ ⟨Z⟩ + ``` + + Previously instantiated measurements can also be applied to QNodes. + * Ising YY gate functionality added. [(#1358)](https://github.com/PennyLaneAI/pennylane/pull/1358) @@ -9,6 +31,11 @@

Breaking changes

+* The existing `pennylane.collections.apply` function is no longer accessible + via `qml.apply`, and needs to be imported directly from the ``collections`` + package. + [(#1358)](https://github.com/PennyLaneAI/pennylane/pull/1358) +

Bug fixes

* Fixed a bug in the `torch` interface that prevented gradients from being @@ -24,7 +51,7 @@ This release contains contributions from (in alphabetical order): -Olivia Di Matteo, Ashish Panigrahi +Olivia Di Matteo, Josh Izaac, Ashish Panigrahi # Release 0.16.0 (current release) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 1524d56e6b7..bcb19c199eb 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -16,11 +16,13 @@ PennyLane can be directly imported. """ from importlib import reload +import pkg_resources import numpy as _np -import pkg_resources from semantic_version import Spec, Version +from pennylane.queuing import apply, QueuingContext + import pennylane.init import pennylane.fourier import pennylane.kernels @@ -59,8 +61,7 @@ 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 +from .collections import QNodeCollection, dot, map, sum import pennylane.grouping # pylint:disable=wrong-import-order # Look for an existing configuration file diff --git a/pennylane/measure.py b/pennylane/measure.py index 54e5b17bd28..343f15eb7df 100644 --- a/pennylane/measure.py +++ b/pennylane/measure.py @@ -187,18 +187,20 @@ def expand(self): return tape - def queue(self): + def queue(self, context=qml.QueuingContext): """Append the measurement process to an annotated queue.""" if self.obs is not None: try: - qml.QueuingContext.update_info(self.obs, owner=self) + context.update_info(self.obs, owner=self) except qml.queuing.QueuingError: - self.obs.queue() - qml.QueuingContext.update_info(self.obs, owner=self) + self.obs.queue(context=context) + context.update_info(self.obs, owner=self) - qml.QueuingContext.append(self, owns=self.obs) + context.append(self, owns=self.obs) else: - qml.QueuingContext.append(self) + context.append(self) + + return self def expval(op): diff --git a/pennylane/operation.py b/pennylane/operation.py index fea8a597ee4..45d9472496c 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -464,10 +464,9 @@ def parameters(self): """Current parameter values.""" return self.data.copy() - def queue(self): + def queue(self, context=qml.QueuingContext): """Append the operator to the Operator queue.""" - qml.QueuingContext.append(self) - + context.append(self) return self # so pre-constructed Observable instances can be queued and returned in a single statement @@ -1112,27 +1111,37 @@ class Tensor(Observable): par_domain = None def __init__(self, *args): # pylint: disable=super-init-not-called - self._eigvals_cache = None self.obs = [] + self._args = args + self.queue(init=True) - for o in args: - if isinstance(o, Tensor): - self.obs.extend(o.obs) - elif isinstance(o, Observable): - self.obs.append(o) - else: - raise ValueError("Can only perform tensor products between observables.") + def queue(self, context=qml.QueuingContext, init=False): # pylint: disable=arguments-differ + constituents = self.obs + + if init: + constituents = self._args + + for o in constituents: + + if init: + if isinstance(o, Tensor): + self.obs.extend(o.obs) + elif isinstance(o, Observable): + self.obs.append(o) + else: + raise ValueError("Can only perform tensor products between observables.") try: - qml.QueuingContext.update_info(o, owner=self) + context.update_info(o, owner=self) except qml.queuing.QueuingError: - o.queue() - qml.QueuingContext.update_info(o, owner=self) + o.queue(context=context) + context.update_info(o, owner=self) except NotImplementedError: pass - qml.QueuingContext.append(self, owns=tuple(args)) + context.append(self, owns=tuple(constituents)) + return self def __copy__(self): cls = self.__class__ diff --git a/pennylane/queuing.py b/pennylane/queuing.py index e88df39aa26..b0abb4434bd 100644 --- a/pennylane/queuing.py +++ b/pennylane/queuing.py @@ -15,6 +15,7 @@ This module contains the :class:`QueuingContext` abstract base class. """ import abc +import copy from collections import OrderedDict, deque @@ -220,6 +221,12 @@ def _append(self, obj, **kwargs): def _remove(self, obj): self.queue.remove(obj) + # Overwrite the inherited class methods, so that if queue.append is called, + # it is appended to the instantiated queue (rather than being added to the + # currently active queuing context, which may be a different queue). + append = _append + remove = _remove + class AnnotatedQueue(QueuingContext): """Lightweight class that maintains a basic queue of operations, in addition @@ -246,7 +253,133 @@ def _get_info(self, obj): return self._queue[obj] + # Overwrite the inherited class methods, so that if annotated_queue.append is called, + # it is appended to the instantiated queue (rather than being added to the + # currently active queuing context, which may be a different queue). + append = _append + remove = _remove + update_info = _update_info + get_info = _get_info + @property def queue(self): """Returns a list of objects in the annotated queue""" return list(self._queue.keys()) + + +def apply(op, context=QueuingContext): + """Apply an instantiated operator or measurement to a queuing context. + + Args: + op (.Operator or .MeasurementProcess): the operator or measurement to apply/queue + context (.QueuingContext): The queuing context to queue the operator to. + Note that if no context is specified, the operator is + applied to the currently active queuing context. + Returns: + .Operator or .MeasurementProcess: the input operator is returned for convenience + + **Example** + + In PennyLane, **operations and measurements are 'queued' or added to a circuit + when they are instantiated**. + + The ``apply`` function can be used to add operations that might have + already been instantiated elsewhere to the QNode: + + .. code-block:: python + + op = qml.RX(0.4, wires=0) + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(x): + qml.RY(x, wires=0) # applied during instantiation + qml.apply(op) # manually applied + return qml.expval(qml.PauliZ(0)) + + >>> print(qml.draw(circuit)(0.6)) + 0: ──RY(0.6)──RX(0.4)──┤ ⟨Z⟩ + + It can also be used to apply functions repeatedly: + + .. code-block:: python + + @qml.qnode(dev) + def circuit(x): + qml.apply(op) + qml.RY(x, wires=0) + qml.apply(op) + return qml.expval(qml.PauliZ(0)) + + >>> print(qml.draw(circuit)(0.6)) + 0: ──RX(0.4)──RY(0.6)──RX(0.4)──┤ ⟨Z⟩ + + .. UsageDetails:: + + Instantiated measurements can also be applied to queuing contexts + using ``apply``: + + .. code-block:: python + + meas = qml.expval(qml.PauliZ(0) @ qml.PauliY(1)) + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(x): + qml.RY(x, wires=0) + qml.CNOT(wires=[0, 1]) + return qml.apply(meas) + + >>> print(qml.draw(circuit)(0.6)) + 0: ──RY(0.6)──╭C──╭┤ ⟨Z ⊗ Y⟩ + 1: ───────────╰X──╰┤ ⟨Z ⊗ Y⟩ + + By default, ``apply`` will queue operators to the currently + active queuing context. + + When working with low-level queuing contexts such as quantum tapes, + the desired context to queue the operation to can be explicitly + passed: + + .. code-block:: python + + with qml.tape.QuantumTape() as tape1: + qml.Hadamard(wires=1) + + with qml.tape.QuantumTape() as tape2: + # Due to the nesting behaviour of queuing contexts, + # tape2 will be queued to tape1. + + # The following PauliX operation will be queued + # to the active queuing context, tape2, during instantiation. + op1 = qml.PauliX(wires=0) + + # We can use qml.apply to apply the same operation to tape1 + # without leaving the tape2 context. + qml.apply(op1, context=tape1) + + qml.RZ(0.2, wires=0) + + qml.CNOT(wires=[0, 1]) + + >>> tape1.operations + [Hadamard(wires=[1]), , PauliX(wires=[0]), CNOT(wires=[0, 1])] + >>> tape2.operations + [PauliX(wires=[0]), RZ(0.2, wires=[0])] + """ + if not QueuingContext.recording(): + raise RuntimeError("No queuing context available to append operation to.") + + if op in getattr(context, "queue", QueuingContext.active_context().queue): + # Queuing contexts can only contain unique objects. + # If the object to be queued already exists, copy it. + op = copy.copy(op) + + if hasattr(op, "queue"): + # operator provides its own logic for queuing + op.queue(context=context) + else: + # append the operator directly to the relevant queuing context + context.append(op) + + return op diff --git a/pennylane/vqe/vqe.py b/pennylane/vqe/vqe.py index 2532945741d..2b0369ca9c5 100644 --- a/pennylane/vqe/vqe.py +++ b/pennylane/vqe/vqe.py @@ -404,18 +404,18 @@ def __isub__(self, H): return self raise ValueError(f"Cannot subtract {type(H)} from Hamiltonian") - def queue(self): + def queue(self, context=qml.QueuingContext): """Queues a qml.Hamiltonian instance""" for o in self.ops: try: - qml.QueuingContext.update_info(o, owner=self) + context.update_info(o, owner=self) except QueuingError: - o.queue() - qml.QueuingContext.update_info(o, owner=self) + o.queue(context=context) + context.update_info(o, owner=self) except NotImplementedError: pass - qml.QueuingContext.append(self, owns=tuple(self.ops)) + context.append(self, owns=tuple(self.ops)) return self diff --git a/tests/collections/test_collections.py b/tests/collections/test_collections.py index 22466a58ab5..6842ad24151 100644 --- a/tests/collections/test_collections.py +++ b/tests/collections/test_collections.py @@ -217,7 +217,7 @@ def test_apply_summation(self, qnodes, interface, tf_support, torch_support, tol else: sfn = np.sum - cost = qml.apply(sfn, qc) + cost = qml.collections.apply(sfn, qc) params = [0.5643, -0.45] res = cost(params) @@ -250,7 +250,7 @@ def test_nested_apply(self, qnodes, interface, tf_support, torch_support, tol): sinfn = np.sin sfn = np.sum - cost = qml.apply(sfn, qml.apply(sinfn, qc)) + cost = qml.collections.apply(sfn, qml.collections.apply(sinfn, qc)) params = [0.5643, -0.45] res = cost(params) diff --git a/tests/test_queuing.py b/tests/test_queuing.py index 5810247957e..c7bc0a7eb1f 100644 --- a/tests/test_queuing.py +++ b/tests/test_queuing.py @@ -319,29 +319,10 @@ def test_update_error(self): 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) + tensor_op = qml.operation.Tensor(A, B) assert q.queue == [A, B, tensor_op] assert q._get_info(A) == {"owner": tensor_op} @@ -444,3 +425,129 @@ def template(x): template(3) assert str(recorder) == expected_output + + +test_observables = [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.operation.Tensor(qml.PauliZ(0), qml.PauliX(1)), + qml.operation.Tensor(qml.PauliZ(0), qml.PauliX(1)) @ qml.Hadamard(2), + qml.Hamiltonian( + [0.1, 0.2, 0.3], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliY(1), qml.Identity(2)] + ), +] + + +class TestApplyOp: + """Tests for the apply function""" + + def test_error(self): + """Test that applying an operation without an active + context raises an error""" + with pytest.raises(RuntimeError, match="No queuing context"): + qml.apply(qml.PauliZ(0)) + + def test_default_queue_operation_inside(self): + """Test applying an operation instantiated within the queuing + context to the existing active queue""" + with qml.tape.QuantumTape() as tape: + op1 = qml.PauliZ(0) + op2 = qml.apply(op1) + + assert tape.operations == [op1, op2] + + def test_default_queue_operation_outside(self): + """Test applying an operation instantiated outside a queuing context + to an existing active queue""" + op = qml.PauliZ(0) + + with qml.tape.QuantumTape() as tape: + qml.apply(op) + + assert tape.operations == [op] + + @pytest.mark.parametrize("obs", test_observables) + def test_default_queue_measurements_outside(self, obs): + """Test applying a measurement instantiated outside a queuing context + to an existing active queue""" + op = qml.expval(obs) + + with qml.tape.QuantumTape() as tape: + qml.apply(op) + + assert tape.measurements == [op] + + @pytest.mark.parametrize("obs", test_observables) + def test_default_queue_measurements_outside(self, obs): + """Test applying a measurement instantiated inside a queuing context + to an existing active queue""" + + with qml.tape.QuantumTape() as tape: + op1 = qml.expval(obs) + op2 = qml.apply(op1) + + assert tape.measurements == [op1, op2] + + def test_different_queue_operation_inside(self): + """Test applying an operation instantiated within the queuing + context to a specfied queuing context""" + with qml.tape.QuantumTape() as tape1: + with qml.tape.QuantumTape() as tape2: + op1 = qml.PauliZ(0) + op2 = qml.apply(op1, tape1) + + assert tape1.operations == [tape2, op2] + assert tape2.operations == [op1] + + def test_different_queue_operation_outside(self): + """Test applying an operation instantiated outside a queuing context + to a specfied queuing context""" + op = qml.PauliZ(0) + + with qml.tape.QuantumTape() as tape1: + with qml.tape.QuantumTape() as tape2: + qml.apply(op, tape1) + + assert tape1.operations == [tape2, op] + assert tape2.operations == [] + + @pytest.mark.parametrize("obs", test_observables) + def test_different_queue_measurements_outside(self, obs): + """Test applying a measurement instantiated outside a queuing context + to a specfied queuing context""" + op = qml.expval(obs) + + with qml.tape.QuantumTape() as tape1: + with qml.tape.QuantumTape() as tape2: + qml.apply(op, tape1) + + assert tape1.measurements == [op] + assert tape2.measurements == [] + + @pytest.mark.parametrize("obs", test_observables) + def test_different_queue_measurements_outside(self, obs): + """Test applying a measurement instantiated inside a queuing context + to a specfied queuing context""" + + with qml.tape.QuantumTape() as tape1: + with qml.tape.QuantumTape() as tape2: + op1 = qml.expval(obs) + op2 = qml.apply(op1, tape1) + + assert tape1.measurements == [op2] + assert tape2.measurements == [op1] + + def test_apply_no_queue_method(self): + """Test that an object with no queue method is still + added to the queuing context""" + with qml.tape.QuantumTape() as tape1: + with qml.tape.QuantumTape() as tape2: + op1 = qml.apply(5) + op2 = qml.apply(6, tape1) + + assert tape1.queue == [tape2, op2] + assert tape2.queue == [op1] + + # note that tapes don't know how to process integers, + # so they are not included after queue processing + assert tape1.operations == [tape2] + assert tape2.operations == [] diff --git a/tests/test_vqe.py b/tests/test_vqe.py index bd0c7d07936..64b3467eae4 100644 --- a/tests/test_vqe.py +++ b/tests/test_vqe.py @@ -810,6 +810,8 @@ def test_hamiltonian_queue(self): queue = [ qml.Hadamard(wires=1), qml.PauliX(wires=0), + qml.PauliZ(0), + qml.PauliZ(2), qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliX(1), qml.PauliZ(1),