Repository: PennyLaneAI/pennylane

Issue:
[BUG] Adjoint method does not respect global wires return types
### Expected behavior

Found by @Shiro-Raven:

The adjoint method _validation_ takes the same wires into account for measurements as forward execution unrelated to the adjoint method.

### Actual behavior

An error is raised.

### Additional information

The adjoint method validation uses the wires of the differentiated tape without taking the device wires into account, because it is missing the corresponding transform from its transformprogram.
For measurements that rely on the device wires, because they act globally on "all" wires, this causes the error below.

### Source code

```shell
dev = qml.device("default.qubit", wires=2)


@qml.qnode(dev, diff_method="adjoint")
def circuit():
    return qml.state()


circuit()
```


### Tracebacks

```shell
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 9
      4 @qml.qnode(dev, diff_method="adjoint")
      5 def circuit():
      6     return qml.state()
----> 9 circuit()

File ~/repos/pennylane/pennylane/workflow/qnode.py:1095, in QNode.__call__(self, *args, **kwargs)
   1092 self.construct(args, kwargs)
   1094 original_grad_fn = [self.gradient_fn, self.gradient_kwargs, self.device]
-> 1095 self._update_gradient_fn(shots=override_shots, tape=self._tape)
   1097 try:
   1098     res = self._execution_component(args, kwargs, override_shots=override_shots)

File ~/repos/pennylane/pennylane/workflow/qnode.py:604, in QNode._update_gradient_fn(self, shots, tape)
    597 if (
    598     self.device.name == "lightning.qubit"
    599     and qml.metric_tensor in self.transform_program
    600     and self.diff_method == "best"
    601 ):
    602     diff_method = "parameter-shift"
--> 604 self.gradient_fn, self.gradient_kwargs, self.device = self.get_gradient_fn(
    605     self._original_device, self.interface, diff_method, tape=tape
    606 )
    607 self.gradient_kwargs.update(self._user_gradient_kwargs or {})

File ~/repos/pennylane/pennylane/workflow/qnode.py:650, in QNode.get_gradient_fn(device, interface, diff_method, tape)
    648 config = _make_execution_config(None, diff_method)
    649 if isinstance(device, qml.devices.Device):
--> 650     if device.supports_derivatives(config, circuit=tape):
    651         new_config = device.preprocess(config)[1]
    652         return new_config.gradient_method, {}, device

File ~/repos/pennylane/pennylane/devices/default_qubit.py:476, in DefaultQubit.supports_derivatives(self, execution_config, circuit)
    471     return not circuit.shots and not any(
    472         isinstance(m.obs, qml.SparseHamiltonian) for m in circuit.measurements
    473     )
    475 if execution_config.gradient_method in {"adjoint", "best"}:
--> 476     return _supports_adjoint(circuit, device_wires=self.wires, device_name=self.name)
    477 return False

File ~/repos/pennylane/pennylane/devices/default_qubit.py:196, in _supports_adjoint(circuit, device_wires, device_name)
    193 _add_adjoint_transforms(prog)
    195 try:
--> 196     prog((circuit,))
    197 except (qml.operation.DecompositionUndefinedError, qml.DeviceError, AttributeError):
    198     return False

File ~/repos/pennylane/pennylane/transforms/core/transform_program.py:509, in TransformProgram.__call__(self, tapes)
    507 if self._argnums is not None and self._argnums[i] is not None:
    508     tape.trainable_params = self._argnums[i][j]
--> 509 new_tapes, fn = transform(tape, *targs, **tkwargs)
    510 execution_tapes.extend(new_tapes)
    512 fns.append(fn)

File ~/repos/pennylane/pennylane/devices/default_qubit.py:166, in adjoint_state_measurements(tape, device_vjp)
    164 complex_data = [qml.math.cast(p, complex) for p in params]
    165 tape = tape.bind_new_parameters(complex_data, list(range(len(params))))
--> 166 new_mp = qml.measurements.StateMP(wires=tape.wires)
    167 state_tape = qml.tape.QuantumScript(tape.operations, [new_mp])
    168 return (state_tape,), partial(
    169     all_state_postprocessing, measurements=tape.measurements, wire_order=tape.wires
    170 )

File ~/repos/pennylane/pennylane/measurements/state.py:140, in StateMP.__init__(self, wires, id)
    139 def __init__(self, wires: Optional[Wires] = None, id: Optional[str] = None):
--> 140     super().__init__(wires=wires, id=id)

File ~/repos/pennylane/pennylane/measurements/measurements.py:177, in MeasurementProcess.__init__(self, obs, wires, eigvals, id)
    175 if wires is not None:
    176     if len(wires) == 0:
--> 177         raise ValueError("Cannot set an empty list of wires.")
    178     if obs is not None:
    179         raise ValueError("Cannot set the wires if an observable is provided.")

ValueError: Cannot set an empty list of wires.
```


### System information

```shell
pl dev
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.

Discussion:
dwierichs: Is the current approach not a good idea? Note that the execution is not affected by the bug, it's just the validation, because it calls `_add_adjoint_transforms(prog)` before using the device wire validation to update the global measurement types.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5761?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.53%. Comparing base [(`f5b805b`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/f5b805b1c57b3deaaa22ba4b2166c4dd77bc500b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`83b4595`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/83b4595456f95a5c9db9c8960d997c75664eb804?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 7 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5761      +/-   ##
==========================================
- Coverage   99.67%   99.53%   -0.15%     
==========================================
  Files         416      418       +2     
  Lines       38686    38874     +188     
==========================================
+ Hits        38562    38692     +130     
- Misses        124      182      +58     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5761?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 489fd740d..a7d698c73 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -190,6 +190,9 @@
 
 <h3>Bug fixes 🐛</h3>
 
+* The validation of the adjoint method in `DefaultQubit` correctly handles device wires now.
+  [(#5761)](https://github.com/PennyLaneAI/pennylane/pull/5761)
+
 * `QuantumPhaseEstimation.map_wires` on longer modifies the original operation instance.
   [(#5698)](https://github.com/PennyLaneAI/pennylane/pull/5698)
 
diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py
index 613f73956..35dc391a6 100644
--- a/pennylane/devices/default_qubit.py
+++ b/pennylane/devices/default_qubit.py
@@ -184,11 +184,12 @@ def adjoint_observables(obs: qml.operation.Operator) -> bool:
     return obs.has_matrix
 
 
-def _supports_adjoint(circuit):
+def _supports_adjoint(circuit, device_wires, device_name):
     if circuit is None:
         return True
 
     prog = TransformProgram()
+    prog.add_transform(validate_device_wires, device_wires, name=device_name)
     _add_adjoint_transforms(prog)
 
     try:
@@ -474,7 +475,7 @@ class DefaultQubit(Device):
             )
 
         if execution_config.gradient_method in {"adjoint", "best"}:
-            return _supports_adjoint(circuit=circuit)
+            return _supports_adjoint(circuit, device_wires=self.wires, device_name=self.name)
         return False
 
     @debug_logger


Test patch:
diff --git a/tests/devices/default_qubit/test_default_qubit.py b/tests/devices/default_qubit/test_default_qubit.py
index e1fd668f5..4af0d167e 100644
--- a/tests/devices/default_qubit/test_default_qubit.py
+++ b/tests/devices/default_qubit/test_default_qubit.py
@@ -127,15 +127,24 @@ class TestSupportsDerivatives:
         assert dev.supports_jvp(config) is True
         assert dev.supports_vjp(config) is True
 
-    def test_supports_adjoint(self):
+    @pytest.mark.parametrize(
+        "device_wires, measurement",
+        [
+            (None, qml.expval(qml.PauliZ(0))),
+            (2, qml.expval(qml.PauliZ(0))),
+            (2, qml.probs()),
+            (2, qml.probs([0])),
+        ],
+    )
+    def test_supports_adjoint(self, device_wires, measurement):
         """Test that DefaultQubit says that it supports adjoint differentiation."""
-        dev = DefaultQubit()
+        dev = DefaultQubit(wires=device_wires)
         config = ExecutionConfig(gradient_method="adjoint", use_device_gradient=True)
         assert dev.supports_derivatives(config) is True
         assert dev.supports_jvp(config) is True
         assert dev.supports_vjp(config) is True
 
-        qs = qml.tape.QuantumScript([], [qml.expval(qml.PauliZ(0))])
+        qs = qml.tape.QuantumScript([], [measurement])
         assert dev.supports_derivatives(config, qs) is True
         assert dev.supports_jvp(config, qs) is True
         assert dev.supports_vjp(config, qs) is True