Repository: PennyLaneAI/pennylane
Pull request #1433 (2021-06-24 09:20:24)

Add function to apply instantiated operations to queuing contexts
**Context:**

Currently, there is no consistent, user-facing approach for applying (already instantiated) operators to a queuing context. Instead, there is a combination of different approaches:

- `op.queue()`: the most common approach. Almost all operators provide their own queuing logic currently, bar `Tensor`, which inherits a basic implementation from the base class and leads to inconsistent behaviour.

- `qml.QueuingContext.append()`: appends the object _directly_ to the currently active queuing context.

- `context._append()` appends the object to a specific queuing context.

This situation is not ideal, since `op.queue()` is not consistent, not documented, and not user-facing.

**Description of the Change:**

- Adds `Tensor.queue()` to make the `Tensor` class consistent with other `Operator` subclasses. This results in several accidental bug fixes that have plagued the Hamiltonian class (see #1142)

- Modifies the existing `queue()` methods, so that they now take an optional `context` keyword argument. This allows operators to be queued to _specific_, _provided_, contexts, rather than just the currently active context.

- Adds a new function `qml.apply`, which will be the new user and developer facing method for queuing already instantiated operations.

**Benefits:**

- There is now a consistent, documented, and well-tested method for queuing instantiated operators to queuing contexts.

**Possible Drawbacks:**

- The existing `qml.apply` function was an import of `qml.collections.apply`. This function was infrequently used or advertised, and now must be accessed by importing directly from the collections module.

**Related GitHub Issues:** n/a


github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [.github/CHANGELOG.md](/PennyLaneAI/pennylane/blob/master/.github/CHANGELOG.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1433](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (07e9edb) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/a6a82ee0cb636533f36ed058065bc71e63a76206?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (a6a82ee) will **decrease** coverage by `0.03%`.
> The diff coverage is `89.13%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@            Coverage Diff             @@
##           master    #1433      +/-   ##
==========================================
- Coverage   98.23%   98.19%   -0.04%     
==========================================
  Files         160      160              
  Lines       11964    11985      +21     
==========================================
+ Hits        11753    11769      +16     
- Misses        211      216       +5     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/queuing.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3F1ZXVpbmcucHk=) | `91.35% <58.33%> (-5.75%)` | :arrow_down: |
| [pennylane/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19faW5pdF9fLnB5) | `98.57% <100.00%> (ø)` | |
| [pennylane/measure.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmUucHk=) | `91.86% <100.00%> (+0.09%)` | :arrow_up: |
| [pennylane/operation.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wZXJhdGlvbi5weQ==) | `95.92% <100.00%> (+0.07%)` | :arrow_up: |
| [pennylane/vqe/vqe.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3ZxZS92cWUucHk=) | `92.19% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [a6a82ee...07e9edb](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1433?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

josh146: @glassnotes @mariaschuld I have renamed the function to `qml.apply()` 🙂 

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 7d1cf6c49..211983c0a 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -2,6 +2,28 @@
 
 <h3>New features since last release</h3>
 
+* 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 @@
 
 <h3>Breaking changes</h3>
 
+* 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)
+
 <h3>Bug fixes</h3>
 
 * 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 1524d56e6..bcb19c199 100644
--- a/pennylane/__init__.py
+++ b/pennylane/__init__.py
@@ -16,11 +16,13 @@ This is the top level module from which all basic functions and classes of
 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.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
+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 54e5b17bd..343f15eb7 100644
--- a/pennylane/measure.py
+++ b/pennylane/measure.py
@@ -187,18 +187,20 @@ class MeasurementProcess:
 
         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 fea8a597e..45d947249 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -464,10 +464,9 @@ class Operator(abc.ABC):
         """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 e88df39aa..b0abb4434 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 @@ class Queue(QueuingContext):
     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 @@ class AnnotatedQueue(QueuingContext):
 
         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]), <QuantumTape: wires=[0], params=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 253294574..2b0369ca9 100644
--- a/pennylane/vqe/vqe.py
+++ b/pennylane/vqe/vqe.py
@@ -404,18 +404,18 @@ class Hamiltonian:
             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
 
 


Test patch:
diff --git a/tests/collections/test_collections.py b/tests/collections/test_collections.py
index 22466a58a..6842ad241 100644
--- a/tests/collections/test_collections.py
+++ b/tests/collections/test_collections.py
@@ -217,7 +217,7 @@ class TestApply:
         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 @@ class TestApply:
             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 581024795..c7bc0a7eb 100644
--- a/tests/test_queuing.py
+++ b/tests/test_queuing.py
@@ -319,29 +319,10 @@ class TestAnnotatedQueue:
     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 @@ class TestOperationRecorder:
             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 bd0c7d079..64b3467ea 100644
--- a/tests/test_vqe.py
+++ b/tests/test_vqe.py
@@ -810,6 +810,8 @@ class TestHamiltonian:
         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),