Repository: PennyLaneAI/pennylane

Issue:
[BUG] `qml.QNSPSAOptimizer` includes `op.adjoint` instead of `qml.adjoint(op)` in source
### Expected behavior

Circuit that includes `qml.RandomLayers` and uses `qml.QNSPSAOptimizer` executes normally.

### Actual behavior

Execution raises `AdjointUndefinedError`. Other optimizers execute fine.

### Additional information

`qml.QNSPSAOptimizer` includes `op.adjoint` in the source instead of `qml.adjoint(op)`, as spotted by @albi3ro 

Bug discovered by [Lucas Tecot on the PennyLane Discussion Forum](https://discuss.pennylane.ai/t/basisstate-raises-adjoint-undefined-error/2160/8).

### Source code

```shell
import pennylane as qml
from pennylane import numpy as plnp

num_qubits = 2
dev = qml.device("default.qubit", wires=num_qubits)
@qml.qnode(dev)
def cost(params):
    qml.RandomLayers(weights=params, wires=range(num_qubits), seed=42)
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))

params = plnp.random.normal(0, plnp.pi, (2, 4))
opt = qml.QNSPSAOptimizer(stepsize=5e-2)
for i in range(51):
    params, loss = opt.step_and_cost(cost, params)
    if i % 10 == 0:
        print(f"Step {i}: cost = {loss:.4f}")
```


### Tracebacks

_No response_

### System information

```shell
(Not relevant here.)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4421?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#4421](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4421?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (23b3ec9) into [master](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/7d64bdcc89e9241e1db375a0faf8cecdfef40775?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (7d64bdc) will **not change** coverage.
> Report is 1 commits behind head on master.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master    #4421   +/-   ##
=======================================
  Coverage   99.76%   99.76%           
=======================================
  Files         351      351           
  Lines       32678    32678           
=======================================
  Hits        32600    32600           
  Misses         78       78           
```


| [Files Changed](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4421?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/\_\_init\_\_.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4421?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/math/multi\_dispatch.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4421?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvbXVsdGlfZGlzcGF0Y2gucHk=) | `99.59% <100.00%> (ø)` | |
| [pennylane/optimize/qnspsa.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4421?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wdGltaXplL3Fuc3BzYS5weQ==) | `100.00% <100.00%> (ø)` | |



Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index bb4a9d4b8..2a9141cd3 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -196,6 +196,9 @@
   `Tensor.sparse_matrix()`, it is recommended to use the `wire_order` keyword argument over `wires`. 
   [(#4424)](https://github.com/PennyLaneAI/pennylane/pull/4424)
   
+* Replace `op.adjoint` with `qml.adjoint` in `QNSPSAOptimizer`.
+  [(#4421)](https://github.com/PennyLaneAI/pennylane/pull/4421)
+
 * Replace deprecated `jax.ad` by `jax.interpreters.ad`.
   [(#4403)](https://github.com/PennyLaneAI/pennylane/pull/4403)
 
diff --git a/pennylane/optimize/qnspsa.py b/pennylane/optimize/qnspsa.py
index 9ee7bdd8f..f870809c5 100644
--- a/pennylane/optimize/qnspsa.py
+++ b/pennylane/optimize/qnspsa.py
@@ -408,7 +408,7 @@ class QNSPSAOptimizer:
         op_forward = self._get_operations(cost, args1, kwargs)
         op_inv = self._get_operations(cost, args2, kwargs)
 
-        new_ops = op_forward + [op.adjoint() for op in reversed(op_inv)]
+        new_ops = op_forward + [qml.adjoint(op) for op in reversed(op_inv)]
         return qml.tape.QuantumScript(new_ops, [qml.probs(wires=cost.tape.wires.labels)])
 
     @staticmethod


Test patch:
diff --git a/tests/optimize/test_qnspsa.py b/tests/optimize/test_qnspsa.py
index e72c87e4a..132ba2ae4 100644
--- a/tests/optimize/test_qnspsa.py
+++ b/tests/optimize/test_qnspsa.py
@@ -418,3 +418,20 @@ class TestQNSPSAOptimizer:
         # blocking should stop params from updating from this minimum
         new_params, _ = opt.step_and_cost(qnode, params)
         assert np.allclose(new_params, params)
+
+
+def test_template_no_adjoint():
+    """Test that qnspsa iterates when the operations do not have a custom adjoint."""
+
+    num_qubits = 2
+    dev = qml.device("default.qubit", wires=num_qubits)
+
+    @qml.qnode(dev)
+    def cost(params):
+        qml.RandomLayers(weights=params, wires=range(num_qubits), seed=42)
+        return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
+
+    params = np.random.normal(0, np.pi, (2, 4))
+    opt = qml.QNSPSAOptimizer(stepsize=5e-2)
+    assert opt.step_and_cost(cost, params)  # just checking it runs without error
+    assert not qml.RandomLayers.has_adjoint