Repository: PennyLaneAI/pennylane (python)

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`. 

Pull request description:
Fix `qml.adjoint` with no queuing context
**Context:**

The logic for the `qml.adjoint` top-level function was created by assuming that a queuing context (e.g., a QNode, tape, etc.) is active. The logic includes stopping the recoding of that queuing context such that the operations passed to `qml.adjoint` can be recorded by a temporary tape. This temporary tape is then used for creating the adjoint of the operation/list of operations.

When we have no active queuing context recording, the stopping logic raises an error (see #1457).

**Description of the Change:**

* Changes the logic of `qml.adjoint` to work when we're not within a queuing context. Further changes the `adjoint` method of the `ControlledOperation` class used for `qml.ctrl`.
* The change includes returning the list of operations adjointed by `qml.adjoint` just as operations or templates would return operation objects.

**Benefits:**

The `qml.adjoint` transform works for operations and templates outside of queuing contexts too. Further to that, any operation/template transformed by `qml.ctrl` have their `adjoint` method working well too outside of queuing contexts.

**Possible Drawbacks:**

N/A

**Related GitHub Issues:**
Closes #1457

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: