Repository: PennyLaneAI/pennylane (python)

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.

Pull request description:
QNSPSA bugfix
Fixes #4420 

The operator method `Operator.adjoint` is for defining a custom decomposition for the adjoint of an operator, and is only defined for cases where the adjoint is another fundamental gate.  `qml.adjoint` is the more public, always works version of `Operator.adjoint`.

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