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),