Repository: PennyLaneAI/pennylane

Issue:
[BUG] Error when applying `qml.adjoint` outside a tape context 
* *Expected behavior:* (What you expect to happen)
The documentation for `qml.adjoint` includes the example 
```python
qml.adjoint(qml.RX)(0.123, wires=0)
```
  I expect this to produce the gate that is the adjoint.

* *Actual behavior:* (What actually happens)
The following error is thrown, related to there not being an active tape context:
```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/tmp/ipykernel_480218/666980449.py in <module>
----> 1 qml.adjoint(qml.RX)(0.123, wires=0)

~/Code/PennyLane/pennylane/transforms/adjoint.py in wrapper(*args, **kwargs)
    114     @wraps(fn)
    115     def wrapper(*args, **kwargs):
--> 116         with get_active_tape().stop_recording(), QuantumTape() as tape:
    117             fn(*args, **kwargs)
    118 

AttributeError: 'NoneType' object has no attribute 'stop_recording'
```
* *Reproduces how often:* (What percentage of the time does it reproduce?)

Always, and for any choice of gate.

* *System information:* (post the output of `import pennylane as qml; qml.about()`)

Running on the dev branch of #1455 . 

```
Name: PennyLane
Version: 0.17.0.dev0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: None
Author-email: None
License: Apache License 2.0
Location: /home/olivia/Code/PennyLane
Requires: numpy, scipy, networkx, autograd, toml, appdirs, semantic-version, autoray
Required-by: 
Platform info:           Linux-5.8.0-59-generic-x86_64-with-glibc2.31
Python version:          3.9.5
Numpy version:           1.21.0
Scipy version:           1.7.0
Installed devices:
- default.gaussian (PennyLane-0.17.0.dev0)
- default.mixed (PennyLane-0.17.0.dev0)
- default.qubit (PennyLane-0.17.0.dev0)
- default.qubit.autograd (PennyLane-0.17.0.dev0)
- default.qubit.jax (PennyLane-0.17.0.dev0)
- default.qubit.tf (PennyLane-0.17.0.dev0)
- default.tensor (PennyLane-0.17.0.dev0)
- default.tensor.tf (PennyLane-0.17.0.dev0)
```

* *Potential solution:* 
Recommendation from @josh146  is to replace the existing logic in `adjoint` with the following, that checks whether there is an active tape before stopping the recording:

```python
current_tape = get_active_tape()
if current_tape is not None:
    with current_tape.stop_recording(), QuantumTape() as tape:
        # existing logic
else:
    with QuantumTape() as tape:
        # existing logic
```

Note: this would have to be done where the same line occurs in `qml.control`. 

Discussion:
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/1532?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> :exclamation: No coverage uploaded for pull request base (`v0.17.0-rc0@6e5cd0e`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#section-missing-base-commit).
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1532/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/1532?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@              Coverage Diff               @@
##             v0.17.0-rc0    #1532   +/-   ##
==============================================
  Coverage               ?   98.20%           
==============================================
  Files                  ?      184           
  Lines                  ?    13230           
  Branches               ?        0           
==============================================
  Hits                   ?    12992           
  Misses                 ?      238           
  Partials               ?        0           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1532?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/1532?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [6e5cd0e...0a0b084](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1532?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).

antalszava: [ch7541]

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 64e14a9bf..d80d1ad66 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -547,6 +547,11 @@
 
 <h3>Bug fixes</h3>
 
+* Fixes a bug in `qml.adjoint` and `qml.ctrl`
+  where the adjoint of operations outside of a `QNode` or a `QuantumTape` could
+  not be obtained.
+  [(#1532)](https://github.com/PennyLaneAI/pennylane/pull/1532)
+
 * Fixes a bug in `GradientDescentOptimizer` and `NesterovMomentumOptimizer`
   where a cost function with one trainable parameter and non-trainable
   parameters raised an error.
diff --git a/pennylane/transforms/adjoint.py b/pennylane/transforms/adjoint.py
index d0dfcba5b..ef9dc8f77 100644
--- a/pennylane/transforms/adjoint.py
+++ b/pennylane/transforms/adjoint.py
@@ -113,20 +113,37 @@ def adjoint(fn):
 
     @wraps(fn)
     def wrapper(*args, **kwargs):
-        with get_active_tape().stop_recording(), QuantumTape() as tape:
-            fn(*args, **kwargs)
+        active_tape = get_active_tape()
+
+        if active_tape is not None:
+            with active_tape.stop_recording(), QuantumTape() as tape:
+                fn(*args, **kwargs)
+        else:
+            # Not within a queuing context
+            with QuantumTape() as tape:
+                fn(*args, **kwargs)
 
         if not tape.operations:
+            # we called op.expand(): get the outputted tape
             tape = fn(*args, **kwargs)
 
+        adjoint_ops = []
         for op in reversed(tape.operations):
             try:
-                op.adjoint()
+                new_op = op.adjoint()
+                adjoint_ops.append(new_op)
             except NotImplementedError:
                 # Expand the operation and adjoint the result.
-                # We do not do anything with the output since
-                # calling adjoint on the expansion will automatically
-                # queue the new operations.
-                adjoint(op.expand)()
+                new_ops = adjoint(op.expand)()
+
+                if isinstance(new_ops, QuantumTape):
+                    new_ops = new_ops.operations
+
+                adjoint_ops.extend(new_ops)
+
+        if len(adjoint_ops) == 1:
+            adjoint_ops = adjoint_ops[0]
+
+        return adjoint_ops
 
     return wrapper
diff --git a/pennylane/transforms/control.py b/pennylane/transforms/control.py
index 44884246d..fe116ef12 100644
--- a/pennylane/transforms/control.py
+++ b/pennylane/transforms/control.py
@@ -113,9 +113,25 @@ class ControlledOperation(Operation):
 
     def adjoint(self):
         """Returns a new ControlledOperation that is equal to the adjoint of `self`"""
-        with get_active_tape().stop_recording(), QuantumTape() as new_tape:
-            # Execute all ops adjointed.
-            adjoint(requeue_ops_in_tape)(self.subtape)
+
+        active_tape = get_active_tape()
+
+        if active_tape is not None:
+            with get_active_tape().stop_recording(), QuantumTape() as new_tape:
+                # Execute all ops adjointed.
+                adjoint(requeue_ops_in_tape)(self.subtape)
+
+        else:
+            # Not within a queuing context
+            with QuantumTape() as new_tape:
+                # Execute all ops adjointed.
+                ops = adjoint(requeue_ops_in_tape)(self.subtape)
+
+            if not new_tape.operations:
+                with qml.tape.QuantumTape() as new_tape:
+                    for op in ops:
+                        op.queue()
+
         return ControlledOperation(new_tape, self.control_wires)
 
     def _controlled(self, wires):


Test patch:
diff --git a/tests/transforms/test_adjoint.py b/tests/transforms/test_adjoint.py
index a111df0ba..9144fa289 100644
--- a/tests/transforms/test_adjoint.py
+++ b/tests/transforms/test_adjoint.py
@@ -87,6 +87,67 @@ def test_nested_adjoint_on_function():
     )
 
 
+class TestOutsideOfQueuing:
+    """Test that operations and templates work with the adjoint transform when
+    created outside of a queuing context"""
+
+    non_param_ops = [(qml.S, 0), (qml.PauliZ, 3), (qml.CNOT, [32, 3])]
+
+    @pytest.mark.parametrize("op,wires", non_param_ops)
+    def test_single_op_non_param_adjoint(self, op, wires):
+        """Test that the adjoint correctly inverts non-parametrized
+        operations"""
+        op_adjoint = qml.adjoint(op)(wires=wires)
+        expected = op(wires=wires).adjoint()
+
+        assert type(op_adjoint) == type(expected)
+        assert op_adjoint.wires == expected.wires
+
+    param_ops = [(qml.RX, [0.123], 0), (qml.Rot, [0.1, 0.2, 0.3], [1]), (qml.CRY, [0.1], [1, 4])]
+
+    @pytest.mark.parametrize("op,par,wires", param_ops)
+    def test_single_op_param_adjoint(self, op, par, wires):
+        """Test that the adjoint correctly inverts operations with a single
+        parameter"""
+        param_op_adjoint = qml.adjoint(op)(*par, wires=wires)
+        expected = op(*par, wires=wires).adjoint()
+
+        assert type(param_op_adjoint) == type(expected)
+        assert param_op_adjoint.parameters == expected.parameters
+        assert param_op_adjoint.wires == expected.wires
+
+    template_ops = [
+        (qml.templates.AngleEmbedding, [np.ones((1))], [2, 3]),
+        (qml.templates.StronglyEntanglingLayers, [np.ones((1, 2, 3))], [2, 3]),
+    ]
+
+    @pytest.mark.parametrize("template, par, wires", template_ops)
+    def test_templates_adjoint(self, template, par, wires):
+        """Test that the adjoint correctly inverts templates"""
+        res = qml.adjoint(template)(*par, wires=wires)
+        result = res if hasattr(res, "__iter__") else [res]  # handle single operation case
+        expected_ops = template(*par, wires=wires)
+
+        expected_ops = expected_ops.expand().operations
+        for o1, o2 in zip(result, reversed(expected_ops)):
+            o2 = o2.adjoint()
+            assert type(o1) == type(o2)
+            assert o1.parameters == o2.parameters
+            assert o1.wires == o2.wires
+
+    def test_cv_template_adjoint(self):
+        """Test that the adjoint correctly inverts CV templates"""
+        template, par, wires = qml.templates.Interferometer, [[1], [0.3], [0.2, 0.3]], [2, 3]
+        result = qml.adjoint(template)(*par, wires=wires)
+        expected_ops = template(*par, wires=wires)
+
+        for o1, o2 in zip(result, reversed(expected_ops)):
+            o2 = o2.adjoint()
+            assert type(o1) == type(o2)
+            assert o1.parameters == o2.parameters
+            assert o1.wires == o2.wires
+
+
 test_functions = [
     lambda fn, *args, **kwargs: adjoint(fn)(*args, **kwargs),
     lambda fn, *args, **kwargs: qml.inv(fn(*args, **kwargs)),
diff --git a/tests/transforms/test_control.py b/tests/transforms/test_control.py
index 6f4181ec2..6b802f2d7 100644
--- a/tests/transforms/test_control.py
+++ b/tests/transforms/test_control.py
@@ -1,9 +1,12 @@
 from functools import partial
+
 import numpy as np
+import pytest
+
 import pennylane as qml
 from pennylane.tape import QuantumTape
-from pennylane.transforms.control import ctrl, ControlledOperation
 from pennylane.tape.tape import expand_tape
+from pennylane.transforms.control import ControlledOperation, ctrl
 
 
 def assert_equal_operations(ops1, ops2):
@@ -79,6 +82,57 @@ def test_adjoint_of_control():
         assert_equal_operations(expanded.operations, expected)
 
 
+class TestAdjointOutsideQueuing:
+    """Test calling the adjoint method of ControlledOperation outside of a
+    queuing context"""
+
+    def test_single_par_op(self):
+        """Test a single parametrized operation for the adjoint method of
+        ControlledOperation"""
+        op, par, control_wires, wires = qml.RY, np.array(0.3), qml.wires.Wires(1), [2]
+        adjoint_of_controlled_op = qml.ctrl(op, control=control_wires)(par, wires=wires).adjoint()
+
+        assert adjoint_of_controlled_op.control_wires == control_wires
+        res_ops = adjoint_of_controlled_op.subtape.operations
+        op1 = res_ops[0]
+        op2 = qml.adjoint(op)(par, wires=wires)
+
+        assert type(op1) == type(op2)
+        assert op1.parameters == op2.parameters
+        assert op1.wires == op2.wires
+
+    def test_template(self):
+        """Test a template for the adjoint method of ControlledOperation"""
+        op, par = qml.templates.StronglyEntanglingLayers, np.ones((1, 2, 3))
+        control_wires, wires = qml.wires.Wires(1), [2, 3]
+        adjoint_of_controlled_op = qml.ctrl(op, control=control_wires)(par, wires=wires).adjoint()
+
+        assert adjoint_of_controlled_op.control_wires == control_wires
+        res_ops = adjoint_of_controlled_op.subtape.operations[0].operations
+        expected = qml.adjoint(op)(par, wires=wires)
+
+        for op1, op2 in zip(res_ops, expected):
+            assert type(op1) == type(op2)
+            assert op1.parameters == op2.parameters
+            assert op1.wires == op2.wires
+
+    def test_cv_template(self):
+        """Test a CV template that returns a list of operations for the adjoint
+        method of ControlledOperation"""
+        op, par = qml.templates.Interferometer, [[1], [0.3], [0.2, 0.3]]
+        control_wires, wires = qml.wires.Wires(1), [2, 3]
+        adjoint_of_controlled_op = qml.ctrl(op, control=control_wires)(*par, wires=wires).adjoint()
+
+        assert adjoint_of_controlled_op.control_wires == control_wires
+        res_ops = adjoint_of_controlled_op.subtape.operations
+        expected = qml.adjoint(op)(*par, wires=wires)
+
+        for op1, op2 in zip(res_ops, expected):
+            assert type(op1) == type(op2)
+            assert op1.parameters == op2.parameters
+            assert op1.wires == op2.wires
+
+
 def test_nested_control():
     """Test nested use of control"""
     with QuantumTape() as tape: