Repository: PennyLaneAI/pennylane
Pull request #1175 (2021-03-29 09:30:02)

Add methods for getting and stopping the currently recording tape
**Context:** In #1111, the `do_queue` keyword argument was added to `Tape.__init__()`, to modify the queuing behaviour.

**Description of the Change:**

Provides two new methods for working with and constructing tapes, that negates the need for the `do_queue` keyword argument.

Firstly, the `get_tape()` function returns the currently recording tape:

```pycon
>>> with qml.tape.QuantumTape():
...     qml.RX(0.2, wires="a")
...     tape = qml.get_tape()
...     qml.RY(0.1, wires="b")
>>> print(tape)
<QuantumTape: wires=['a', 'b'], params=2>
```

If no tape is recording operations, `None` is returned:
```pycon
>>> print(qml.tape.get_tape())
None
```

Secondly, the `tape.stop_recording()` context manager is added to temporarily stop a tape from recording operations:

```pycon
>>> with qml.tape.QuantumTape() as tape:
...     qml.RX(0, wires=0)
...     with tape.stop_recording():
...         qml.RY(1.0, wires=1)
...     qml.RZ(2, wires=1)
>>> tape.operations
[RX(0, wires=[0]), RZ(2, wires=[1])]
```

This can be used as a 'scratch space', to create temporary tapes that are ignored by the PennyLane queuing context:

```pycon
>>> with QuantumTape() as tape:
...     qml.RX(0, wires=0)
...     with tape.stop_recording(), QuantumTape() as temp_tape:
...         qml.RY(1.0, wires=1)
...     qml.RZ(2, wires=1)
>>> print(tape.operations)
[RX(0, wires=[0]), RZ(2, wires=[1])]
>>> print(temp_tape.operations)
[RY(1.0, wires=[1])]
```

**Benefits:**

- Existing tapes are now in charge of their recording behaviour, rather than requiring nested tapes/operations to turn on and off their queuing.

- Allows `do_queue` to be deprecated and removed from all tapes and operations.

**Possible Drawbacks:** n/a

**Related GitHub Issues:** n/a


codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175?src=pr&el=h1) Report
> Merging [#1175](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175?src=pr&el=desc) (1849c3b) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/0021c3feecf16745c70418ea5444197731c7943f?el=desc) (0021c3f) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master    #1175   +/-   ##
=======================================
  Coverage   98.12%   98.13%           
=======================================
  Files         144      144           
  Lines       10833    10859   +26     
=======================================
+ Hits        10630    10656   +26     
  Misses        203      203           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pennylane/measure.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175/diff?src=pr&el=tree#diff-cGVubnlsYW5lL21lYXN1cmUucHk=) | `91.76% <100.00%> (ø)` | |
| [pennylane/operation.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175/diff?src=pr&el=tree#diff-cGVubnlsYW5lL29wZXJhdGlvbi5weQ==) | `95.82% <100.00%> (ø)` | |
| [pennylane/queuing.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3F1ZXVpbmcucHk=) | `97.10% <100.00%> (+0.04%)` | :arrow_up: |
| [pennylane/tape/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RhcGUvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/tape/tape.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RhcGUvdGFwZS5weQ==) | `98.51% <100.00%> (+0.05%)` | :arrow_up: |
| [pennylane/ops/qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175/diff?src=pr&el=tree#diff-cGVubnlsYW5lL29wcy9xdWJpdC5weQ==) | `98.30% <0.00%> (+<0.01%)` | :arrow_up: |
| [...ennylane/circuit\_drawer/representation\_resolver.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175/diff?src=pr&el=tree#diff-cGVubnlsYW5lL2NpcmN1aXRfZHJhd2VyL3JlcHJlc2VudGF0aW9uX3Jlc29sdmVyLnB5) | `99.33% <0.00%> (+0.04%)` | :arrow_up: |
| [pennylane/\_device.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175/diff?src=pr&el=tree#diff-cGVubnlsYW5lL19kZXZpY2UucHk=) | `96.61% <0.00%> (+0.04%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175?src=pr&el=footer). Last update [0021c3f...1849c3b](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1175?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 0d1493ef0..2fe217d74 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -483,6 +483,32 @@
   transforms.
   [(#1064)](https://github.com/PennyLaneAI/pennylane/pull/1064)
 
+* Adds a new transform `qml.invisible`.
+  [(#1175)](https://github.com/PennyLaneAI/pennylane/pull/1175)
+
+  Marking a quantum function as invisible will inhibit any internal
+  quantum operation processing from being recorded by the QNode:
+
+  ```pycon
+  >>> @qml.transforms.invisible
+  ... def list_of_ops(params, wires):
+  ...     return [
+  ...         qml.RX(params[0], wires=wires),
+  ...         qml.RY(params[1], wires=wires),
+  ...         qml.RZ(params[2], wires=wires)
+  ...     ]
+  >>> @qml.qnode(dev)
+  ... def circuit(params):
+  ...     # list_of_ops is invisible, so quantum operations
+  ...     # instantiated within it will not be queued.
+  ...     ops = list_of_ops(params, wires=0)
+  ...     # apply only the last operation from the list
+  ...     ops[-1].queue()
+  ...     return qml.expval(qml.PauliZ(0))
+  >>> print(qml.draw(circuit)([1, 2, 3]))
+   0: ──RZ(3)──┤ ⟨Z⟩
+  ```
+
 <h3>Improvements</h3>
 
 * Edited the ``MottonenStatePreparation`` template to improve performance on states with only real amplitudes
@@ -596,6 +622,28 @@
 * Due to the addition of `density_matrix()` as a return type from a QNode, tuples are now supported by the `output_dim` parameter in `qnn.KerasLayer`.
   [(#1070)](https://github.com/PennyLaneAI/pennylane/pull/1070)
 
+* Two new utility methods are provided for working with quantum tapes.
+  [(#1175)](https://github.com/PennyLaneAI/pennylane/pull/1175)
+
+  - `qml.tape.get_active_tape()` gets the currently recording tape.
+
+  - `tape.stop_recording()` is a context manager that temporarily
+    stops the currently recording tape from recording additional
+    tapes or quantum operations.
+
+  For example:
+
+  ```pycon
+  >>> with qml.tape.QuantumTape():
+  ...     qml.RX(0, wires=0)
+  ...     current_tape = qml.tape.get_active_tape()
+  ...     with current_tape.stop_recording():
+  ...         qml.RY(1.0, wires=1)
+  ...     qml.RZ(2, wires=1)
+  >>> current_tape.operations
+  [RX(0, wires=[0]), RZ(2, wires=[1])]
+  ```
+
 <h3>Breaking changes</h3>
 
 * Devices do not have an `analytic` argument or attribute anymore. 
diff --git a/pennylane/measure.py b/pennylane/measure.py
index a695cb845..3d3410243 100644
--- a/pennylane/measure.py
+++ b/pennylane/measure.py
@@ -192,7 +192,7 @@ class MeasurementProcess:
         if self.obs is not None:
             try:
                 qml.QueuingContext.update_info(self.obs, owner=self)
-            except ValueError:
+            except qml.queuing.QueuingError:
                 self.obs.queue()
                 qml.QueuingContext.update_info(self.obs, owner=self)
 
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 87b07ea15..3289d49f2 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -1121,7 +1121,7 @@ class Tensor(Observable):
 
             try:
                 qml.QueuingContext.update_info(o, owner=self)
-            except ValueError:
+            except qml.queuing.QueuingError:
                 o.queue()
                 qml.QueuingContext.update_info(o, owner=self)
             except NotImplementedError:
diff --git a/pennylane/queuing.py b/pennylane/queuing.py
index 9b05fefa2..e88df39aa 100644
--- a/pennylane/queuing.py
+++ b/pennylane/queuing.py
@@ -18,6 +18,10 @@ import abc
 from collections import OrderedDict, deque
 
 
+class QueuingError(Exception):
+    """Exception that is raised when there is a queuing error"""
+
+
 class QueuingContext(abc.ABC):
     """Abstract base class for classes that exposes a queue for objects.
 
@@ -232,13 +236,13 @@ class AnnotatedQueue(QueuingContext):
 
     def _update_info(self, obj, **kwargs):
         if obj not in self._queue:
-            raise ValueError(f"Object {obj} not in the queue.")
+            raise QueuingError(f"Object {obj} not in the queue.")
 
         self._queue[obj].update(kwargs)
 
     def _get_info(self, obj):
         if obj not in self._queue:
-            raise ValueError(f"Object {obj} not in the queue.")
+            raise QueuingError(f"Object {obj} not in the queue.")
 
         return self._queue[obj]
 
diff --git a/pennylane/tape/__init__.py b/pennylane/tape/__init__.py
index abac43975..017b65c40 100644
--- a/pennylane/tape/__init__.py
+++ b/pennylane/tape/__init__.py
@@ -15,7 +15,7 @@
 This subpackage contains various quantum tapes, which track, queue,
 validate, execute, and differentiate quantum circuits.
 """
-from .tape import QuantumTape
+from .tape import QuantumTape, get_active_tape
 from .jacobian_tape import JacobianTape
 from .cv_param_shift import CVParamShiftTape
 from .qubit_param_shift import QubitParamShiftTape
diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py
index 578a027c0..8179be4a5 100644
--- a/pennylane/tape/tape.py
+++ b/pennylane/tape/tape.py
@@ -15,14 +15,15 @@
 This module contains the base quantum tape.
 """
 # pylint: disable=too-many-instance-attributes,protected-access,too-many-branches,too-many-public-methods
+from collections import Counter, deque
+import contextlib
 import copy
-from collections import Counter
 from threading import RLock
 
 import numpy as np
 
 import pennylane as qml
-from pennylane.queuing import AnnotatedQueue, QueuingContext
+from pennylane.queuing import AnnotatedQueue, QueuingContext, QueuingError
 from pennylane.operation import Sample
 
 # CV ops still need to support state preparation operations prior to any
@@ -80,6 +81,24 @@ https://github.com/Qiskit/openqasm/blob/master/examples/stdgates.inc
 """
 
 
+def get_active_tape():
+    """Returns the currently recording tape.
+    If no tape is currently recording, ``None`` is returned.
+
+    **Example**
+
+    >>> with qml.tape.QuantumTape():
+    ...     qml.RX(0.2, wires="a")
+    ...     tape = qml.get_active_tape()
+    ...     qml.RY(0.1, wires="b")
+    >>> print(tape)
+    <QuantumTape: wires=['a', 'b'], params=2>
+    >>> print(qml.tape.get_active_tape())
+    None
+    """
+    return QueuingContext.active_context()
+
+
 def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False):
     """Expand all objects in a tape to a specific depth.
 
@@ -339,6 +358,31 @@ class QuantumTape(AnnotatedQueue):
         """str, None: automatic differentiation interface used by the quantum tape (if any)"""
         return None
 
+    @contextlib.contextmanager
+    def stop_recording(self):
+        """Context manager to temporarily stop recording operations
+        onto the tape. This is useful is scratch space is needed.
+
+        **Example**
+
+        >>> with qml.tape.QuantumTape() as tape:
+        ...     qml.RX(0, wires=0)
+        ...     with tape.stop_recording():
+        ...         qml.RY(1.0, wires=1)
+        ...     qml.RZ(2, wires=1)
+        >>> tape.operations
+        [RX(0, wires=[0]), RZ(2, wires=[1])]
+        """
+        if QueuingContext.active_context() is not self:
+            raise QueuingError(
+                "Cannot stop recording requested tape " "as it is not currently recording."
+            )
+
+        active_contexts = QueuingContext._active_contexts
+        QueuingContext._active_contexts = deque()
+        yield
+        QueuingContext._active_contexts = active_contexts
+
     # ========================================================
     # construction methods
     # ========================================================
diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py
index a508ba297..5cb280a01 100644
--- a/pennylane/transforms/__init__.py
+++ b/pennylane/transforms/__init__.py
@@ -16,5 +16,6 @@ This subpackage contains tape and QNode transforms.
 """
 from .classical_jacobian import classical_jacobian
 from .draw import draw
+from .invisible import invisible
 from .measurement_grouping import measurement_grouping
 from .metric_tensor import metric_tensor
diff --git a/pennylane/transforms/invisible.py b/pennylane/transforms/invisible.py
new file mode 100644
index 000000000..1e44b00c2
--- /dev/null
+++ b/pennylane/transforms/invisible.py
@@ -0,0 +1,87 @@
+# 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.
+"""Contains a transform to make quantum function non-recordable
+or invisible within a QNode or quantum tape context."""
+
+from functools import wraps
+
+import pennylane as qml
+
+
+def invisible(fn):
+    """A transform to make a quantum function non-recordable
+    or invisible within a QNode or quantum tape context.
+
+    Args:
+        fn (function): A quantum function that applies quantum operations.
+
+    Returns:
+        function: The input function transformed, so that it will not be
+        recorded by QNodes or quantum tapes.
+
+    **Example**
+
+    Consider the following quantum function:
+
+    >>> def list_of_ops(params, wires):
+    ...     return [
+    ...         qml.RX(params[0], wires=wires),
+    ...         qml.RY(params[1], wires=wires),
+    ...         qml.RZ(params[2], wires=wires)
+    ...     ]
+
+    If executed within a QNode or a tape context, these operations will be
+    recorded, even if this is not the intention:
+
+    >>> dev = qml.device("default.qubit", wires=2)
+    >>> @qml.qnode(dev)
+    ... def circuit(params):
+    ...     ops = list_of_ops(params, wires=0)
+    ...     # apply only the last operation from the list
+    ...     ops[-1].queue()
+    ...     return qml.expval(qml.PauliZ(0))
+    >>> print(qml.draw(circuit)([1, 2, 3]))
+     0: ──RX(1)──RY(2)──RZ(3)──┤ ⟨Z⟩
+
+    Marking the quantum function as invisible will inhibit any internal
+    quantum operation processing from being recorded by the QNode:
+
+    >>> @qml.transforms.invisible
+    ... def list_of_ops(params, wires):
+    ...     return [
+    ...         qml.RX(params[0], wires=wires),
+    ...         qml.RY(params[1], wires=wires),
+    ...         qml.RZ(params[2], wires=wires)
+    ...     ]
+    >>> @qml.qnode(dev)
+    ... def circuit(params):
+    ...     ops = list_of_ops(params, wires=0)
+    ...     # apply only the last operation from the list
+    ...     ops[-1].queue()
+    ...     return qml.expval(qml.PauliZ(0))
+    >>> print(qml.draw(circuit)([1, 2, 3]))
+     0: ──RZ(3)──┤ ⟨Z⟩
+    """
+
+    @wraps(fn)
+    def wrapper(*args, **kwargs):
+        tape = qml.tape.get_active_tape()
+
+        if tape is None:
+            return fn(*args, **kwargs)
+
+        with tape.stop_recording():
+            return fn(*args, **kwargs)
+
+    return wrapper


Test patch:
diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py
index 94b8fad3b..e296ec970 100644
--- a/tests/tape/test_tape.py
+++ b/tests/tape/test_tape.py
@@ -1299,3 +1299,115 @@ class TestTapeCopying:
         # check that the casting worked
         assert isinstance(copied_tape, qml.tape.JacobianTape)
         assert not isinstance(tape, qml.tape.JacobianTape)
+
+
+class TestStopRecording:
+    """Test that the stop_recording function works as expected"""
+
+    def test_tape_not_recording(self):
+        """Test that an error is raised if the tape is no
+        longer recording"""
+        with QuantumTape() as tape:
+            qml.RX(0.1, wires=0)
+
+        with pytest.raises(qml.queuing.QueuingError, match="Cannot stop recording"):
+            with tape.stop_recording():
+                pass
+
+    def test_nested_tape_not_recording(self):
+        """Test that an error is raised if the tape is no
+        longer recording"""
+        with QuantumTape() as tape1:
+            qml.RX(0.1, wires=0)
+
+            with QuantumTape() as tape2:
+                qml.RX(0.1, wires=0)
+
+                with pytest.raises(qml.queuing.QueuingError, match="Cannot stop recording"):
+                    with tape1.stop_recording():
+                        pass
+
+    def test_recording_stopped(self):
+        """Test that recording is stopped within a tape context"""
+
+        with QuantumTape() as tape:
+            op0 = qml.RX(0, wires=0)
+            assert tape.active_context() is tape
+
+            with tape.stop_recording():
+                op1 = qml.RY(1.0, wires=1)
+                assert tape.active_context() is None
+
+            op2 = qml.RZ(2, wires=1)
+            assert tape.active_context() is tape
+
+        assert len(tape.operations) == 2
+        assert tape.operations[0] == op0
+        assert tape.operations[1] == op2
+
+    def test_nested_recording_stopped(self):
+        """Test that recording is stopped within a nested tape context"""
+
+        with QuantumTape() as tape1:
+            op0 = qml.RX(0, wires=0)
+            assert tape1.active_context() is tape1
+
+            with QuantumTape() as tape2:
+                assert tape1.active_context() is tape2
+                op1 = qml.RY(1.0, wires=1)
+
+                with tape2.stop_recording():
+                    assert tape1.active_context() is None
+                    op2 = qml.RZ(0.6, wires=2)
+                    op3 = qml.CNOT(wires=[0, 2])
+
+                op4 = qml.Hadamard(wires=0)
+
+            op5 = qml.RZ(2, wires=1)
+            assert tape1.active_context() is tape1
+
+        assert len(tape1.operations) == 3
+        assert tape1.operations[0] == op0
+        assert tape1.operations[1] == tape2
+        assert tape1.operations[2] == op5
+
+        assert len(tape2.operations) == 2
+        assert tape2.operations[0] == op1
+        assert tape2.operations[1] == op4
+
+    def test_creating_scratch_tape(self):
+        """Test that a tape created inside the 'scratch'
+        space is properly created and accessible"""
+        with QuantumTape() as tape:
+            op0 = qml.RX(0, wires=0)
+            assert tape.active_context() is tape
+
+            with tape.stop_recording(), QuantumTape() as temp_tape:
+                assert tape.active_context() is temp_tape
+                op1 = qml.RY(1.0, wires=1)
+
+            op2 = qml.RZ(2, wires=1)
+            assert tape.active_context() is tape
+
+        assert len(tape.operations) == 2
+        assert tape.operations[0] == op0
+        assert tape.operations[1] == op2
+
+        assert len(temp_tape.operations) == 1
+        assert temp_tape.operations[0] == op1
+
+
+def test_gate_tape():
+    """Test that the get_active_tape() function returns the currently
+    recording tape, or None if no tape is recording"""
+    assert qml.tape.get_active_tape() is None
+
+    with QuantumTape() as tape1:
+        assert qml.tape.get_active_tape() is tape1
+
+        with QuantumTape() as tape2:
+            assert qml.tape.get_active_tape() is tape2
+
+        assert qml.tape.get_active_tape() is tape1
+
+    assert qml.tape.get_active_tape() is None
diff --git a/tests/test_queuing.py b/tests/test_queuing.py
index 172e56987..581024795 100644
--- a/tests/test_queuing.py
+++ b/tests/test_queuing.py
@@ -20,7 +20,7 @@ import pytest
 import pennylane as qml
 import numpy as np
 
-from pennylane.queuing import AnnotatedQueue, AnnotatedQueue, Queue, QueuingContext
+from pennylane.queuing import AnnotatedQueue, AnnotatedQueue, Queue, QueuingContext, QueuingError
 from pennylane.tape import OperationRecorder
 
 
@@ -279,7 +279,7 @@ class TestAnnotatedQueue:
 
         B = qml.PauliY(1)
 
-        with pytest.raises(ValueError, match="not in the queue"):
+        with pytest.raises(QueuingError, match="not in the queue"):
             q._get_info(B)
 
     def test_get_info_none(self):
@@ -313,7 +313,7 @@ class TestAnnotatedQueue:
 
         B = qml.PauliY(1)
 
-        with pytest.raises(ValueError, match="not in the queue"):
+        with pytest.raises(QueuingError, match="not in the queue"):
             q._update_info(B, inv=True)
 
     def test_append_annotating_object(self):
diff --git a/tests/transforms/test_invisible.py b/tests/transforms/test_invisible.py
new file mode 100644
index 000000000..54a840d9d
--- /dev/null
+++ b/tests/transforms/test_invisible.py
@@ -0,0 +1,129 @@
+# 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.
+
+import numpy as np
+
+import pennylane as qml
+from pennylane.transforms.invisible import invisible
+
+
+def test_invisible_on_function_inside_QNode():
+    """Test that the invisible transform when applied to a function
+    is not recorded by a QNode"""
+    dev = qml.device("default.qubit", wires=1)
+
+    @invisible
+    def my_op():
+        return [
+            qml.RX(0.123, wires=0),
+            qml.RY(2.32, wires=0),
+            qml.RZ(1.95, wires=0)
+        ]
+
+    res = []
+
+    @qml.qnode(dev)
+    def my_circuit():
+        res.extend(my_op())
+        return qml.expval(qml.PauliZ(0))
+
+    my_circuit.construct([], {})
+    tape = my_circuit.qtape
+
+    assert len(tape.operations) == 0
+    assert len(res) == 3
+
+
+def test_invisible_directly_on_op():
+    """Test that invisible transform works when directly applied to an op"""
+    dev = qml.device("default.qubit", wires=1)
+    res = []
+
+    @qml.qnode(dev)
+    def my_circuit():
+        op1 = invisible(qml.RX)(np.pi/4.0, wires=0)
+        op2 = qml.RY(np.pi/4.0, wires=0)
+        res.extend([op1, op2])
+        return qml.expval(qml.PauliZ(0))
+
+    my_circuit.construct([], {})
+    tape = my_circuit.qtape
+
+    assert len(tape.operations) == 1
+    assert tape.operations[0] == res[1]
+    assert len(res) == 2
+
+
+def test_nested_invisible_on_function():
+    """Test that invisible works when nested with other invisibles"""
+
+    @invisible
+    @invisible
+    def my_op():
+        return [
+            qml.RX(0.123, wires=0),
+            qml.RY(2.32, wires=0),
+            qml.RZ(1.95, wires=0),
+        ]
+
+    # the invisible function will still work outside of any queuing contexts
+    res = my_op()
+    assert len(res) == 3
+
+    dev = qml.device("default.qubit", wires=1)
+    @qml.qnode(dev)
+    def my_circuit():
+        my_op()
+        invisible(my_op)()
+        qml.Hadamard(wires=0)
+        invisible(invisible(my_op))()
+        return qml.state()
+
+    my_circuit.construct([], {})
+    tape = my_circuit.qtape
+
+    assert len(tape.operations) == 1
+    assert tape.operations[0].name == "Hadamard"
+
+
+def test_invisible_qnode_qfunc():
+    """A QNode with an invisible qfunc will result in no quantum measurements."""
+    dev = qml.device("default.qubit", wires=1)
+
+    @qml.qnode(dev)
+    @invisible
+    def my_circuit():
+        qml.PauliX(wires=0)
+        return qml.expval(qml.PauliZ(0))
+
+    result = my_circuit()
+    assert len(result) == 0
+
+    tape = my_circuit.qtape
+    assert len(tape.operations) == 0
+    assert len(tape.measurements) == 0
+
+
+def test_invisible_qnode():
+    """An invisible QNode is unaffected"""
+    dev = qml.device("default.qubit", wires=1)
+
+    @invisible
+    @qml.qnode(dev)
+    def my_circuit():
+        qml.RX(np.pi, wires=0)
+        return qml.expval(qml.PauliZ(0))
+
+    result = my_circuit()
+    assert result == -1.0