Repository: PennyLaneAI/pennylane (python)

Issue:
Raise exception or warning when `qml.adjoint` is used on operation list instead of function.
The newer `qml.adjoint` function does not have any effect when acting on operation lists like in the following example:
```python
params = list(range(4))
qml.adjoint(qml.templates.AngleEmbedding(params))
```
Users might try this, because it worked like this with `qml.inv` which `qml.adjoint` is replacing. Therefore, we should raise
an exception whenever this is attempted by checking for the input to be `callable`. Alternatively, a warning could be raised, but 
the behaviour differs fundamentally from the expected, an exception seems more appropriate.



Pull request description:
Raise an error if `adjoint` is applied to a non-callable object.
**Context:**
This implements #1336 

**Description of the Change:**
The object that `qml.adjoint` is applied to is checked to be callable before constructing the transform.

**Benefits:**
Catches scenarios in which `qml.adjoint` would silently simply do nothing.

**Possible Drawbacks:**
n/a

**Related GitHub Issues:**
closes #1336

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index ab4d6fc8d..51ba7850e 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -148,6 +148,45 @@
 
 <h3>Improvements</h3>
 
+* The `adjoint` transform now raises and error whenever the object it is applied to
+  is not callable.
+  [(#2060)](https://github.com/PennyLaneAI/pennylane/pull/2060)
+
+  An example is a list of operations to which one might apply `qml.adjoint`:
+
+  ```python
+  dev = qml.device("default.qubit", wires=2)
+  @qml.qnode(dev)
+  def circuit_wrong(params):
+      # Note the difference:                  v                         v
+      qml.adjoint(qml.templates.AngleEmbedding(params, wires=dev.wires))
+      return qml.state()
+
+  @qml.qnode(dev)
+  def circuit_correct(params):
+      # Note the difference:                  v                         v
+      qml.adjoint(qml.templates.AngleEmbedding)(params, wires=dev.wires)
+      return qml.state()
+
+  params = list(range(1, 3))
+  ```
+  
+  The produced state is
+
+  ```pycon
+  >>> circuit_wrong(params)
+  [ 0.47415988+0.j          0.        -0.73846026j  0.        -0.25903472j
+   -0.40342268+0.j        ]
+  ```
+
+  but if we apply the `adjoint` correctly, we get
+
+  ```pycon
+  >>> circuit_correct(params)
+  [ 0.47415988+0.j          0.         0.73846026j  0.         0.25903472j
+   -0.40342268+0.j        ]
+  ```
+  
 * A precision argument has been added to the tape's ``to_openqasm`` function 
   to control the precision of parameters.
   [(#2071)](https://github.com/PennyLaneAI/pennylane/pull/2071)
diff --git a/pennylane/transforms/adjoint.py b/pennylane/transforms/adjoint.py
index 28ab1eee1..920d84230 100644
--- a/pennylane/transforms/adjoint.py
+++ b/pennylane/transforms/adjoint.py
@@ -108,6 +108,12 @@ def adjoint(fn):
         >>> print(qml.draw(circuit)())
         0: --RX(0.123)--RX(-0.123)--| <Z>
     """
+    if not callable(fn):
+        raise ValueError(
+            f"The object {fn} of type {type(fn)} is not callable. "
+            "This error might occur if you apply adjoint to a list "
+            "of operations instead of a function or template."
+        )
 
     @wraps(fn)
     def wrapper(*args, **kwargs):


Test patch:
diff --git a/tests/transforms/test_adjoint.py b/tests/transforms/test_adjoint.py
index 5698f0b26..3b652f122 100644
--- a/tests/transforms/test_adjoint.py
+++ b/tests/transforms/test_adjoint.py
@@ -59,7 +59,7 @@ def test_barrier_adjoint():
 
     @qml.qnode(dev)
     def my_circuit():
-        qml.adjoint(qml.Barrier)(wires=0)
+        adjoint(qml.Barrier)(wires=0)
         return qml.state()
 
     assert my_circuit()[0] == 1.0
@@ -99,6 +99,26 @@ def test_nested_adjoint_on_function():
     )
 
 
+with qml.tape.JacobianTape() as tape:
+    qml.PauliX(0)
+    qml.Hadamard(1)
+
+noncallable_objects = [
+    qml.RX(0.2, wires=0),
+    qml.AngleEmbedding(list(range(2)), wires=range(2)),
+    [qml.Hadamard(1), qml.RX(-0.2, wires=1)],
+    tape,
+]
+
+
+@pytest.mark.parametrize("obj", noncallable_objects)
+def test_error_adjoint_on_noncallable(obj):
+    """Test that an error is raised if qml.adjoint is applied to an object that
+    is not callable, as it silently does not have any effect on those."""
+    with pytest.raises(ValueError, match=f"{type(obj)} is not callable."):
+        adjoint(obj)
+
+
 class TestOutsideOfQueuing:
     """Test that operations and templates work with the adjoint transform when
     created outside of a queuing context"""
@@ -109,7 +129,7 @@ class TestOutsideOfQueuing:
     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)
+        op_adjoint = adjoint(op)(wires=wires)
         expected = op(wires=wires).adjoint()
 
         assert type(op_adjoint) == type(expected)
@@ -121,7 +141,7 @@ class TestOutsideOfQueuing:
     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)
+        param_op_adjoint = adjoint(op)(*par, wires=wires)
         expected = op(*par, wires=wires).adjoint()
 
         assert type(param_op_adjoint) == type(expected)
@@ -136,7 +156,7 @@ class TestOutsideOfQueuing:
     @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)
+        res = adjoint(template)(*par, wires=wires)
         result = res if hasattr(res, "__iter__") else [res]  # handle single operation case
         expected_ops = template(*par, wires=wires)
 
@@ -150,7 +170,7 @@ class TestOutsideOfQueuing:
     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).expand().operations
+        result = adjoint(template)(*par, wires=wires).expand().operations
         expected_ops = template(*par, wires=wires).expand().operations
 
         for o1, o2 in zip(result, reversed(expected_ops)):