Repository: PennyLaneAI/pennylane

Issue:
[BUG] Quantum Fisher with Different Devices
### Expected behavior

I was trying to calculate quantum fisher information matrices using different devices. As far as I understand the documentation (https://docs.pennylane.ai/en/stable/code/api/pennylane.qinfo.transforms.quantum_fisher.html), whether the function uses `adjoint_metric_tensor` and `metric_tensor` should automatically change depending on the exact hardware used and the respective available operations.
I would expect the function to return the QFIM or at least a meaningful error message saying that the function is not supported with these devices for now.

### Actual behavior

I get a `MatrixUndefinedError` when using a different simulator than `default.qubit`, which is what happens when you call `adjoint_metric_tensor` with any other simulator than `default.qubit`. 

### Additional information

_No response_

### Source code

```shell
import pennylane as qml
from pennylane import numpy as np

from sklearn import datasets as ds

NUM_WIRES = 4

def get_circuit(data, parameters):
    qml.IQPEmbedding(data, wires=range(NUM_WIRES), n_repeats=1)
    for i in range(NUM_WIRES):
        qml.RX(parameters[i], wires=i)
    for j in range(NUM_WIRES-1):
        qml.CNOT(wires=[j, j+1])
    return qml.expval(qml.PauliZ(0))

# use different devices here, i.e. lightning.qubit, default.qubit, lightning.kokkos
dev = qml.device("default.mixed", wires=NUM_WIRES)

def qfim(X_train, parameters):
    circuit = qml.QNode(get_circuit, dev)
    data = np.array(X_train[0], requires_grad=False)
    return qml.qinfo.transforms.quantum_fisher(circuit)(data, parameters)

X = ds.load_iris().data
parameters = np.random.random(size=NUM_WIRES, requires_grad=True)
print(qfim(X, parameters))
```


### Tracebacks

```shell
Traceback (most recent call last):
  File "/home/path/mwe_simulator.py", line 26, in <module>
    print(qfim(X, parameters))
  File "/home/path/mwe_simulator.py", line 22, in qfim
    return qml.adjoint_metric_tensor(circuit)(data, parameters)
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/workflow/qnode.py", line 1048, in __call__
    res = qml.execute(
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/workflow/execution.py", line 680, in execute
    return post_processing(tapes)
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/workflow/execution.py", line 677, in post_processing
    return program_post_processing(program_pre_processing(results))
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/transforms/core/transform_program.py", line 86, in _apply_postprocessing_stack
    results = postprocessing(results)
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/transforms/core/transform_program.py", line 56, in _batch_postprocessing
    return tuple(fn(results[sl]) for fn, sl in zip(individual_fns, slices))
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/transforms/core/transform_program.py", line 56, in <genexpr>
    return tuple(fn(results[sl]) for fn, sl in zip(individual_fns, slices))
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/gradients/adjoint_metric_tensor.py", line 184, in processing_fn
    psi = qml.devices.qubit.apply_operation(op, psi)
  File "/home/path/miniconda3/lib/python3.10/functools.py", line 889, in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/devices/qubit/apply_operation.py", line 204, in apply_operation
    return _apply_operation_default(op, state, is_state_batched, debugger)
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/devices/qubit/apply_operation.py", line 215, in _apply_operation_default
    return apply_operation_tensordot(op, state, is_state_batched=is_state_batched)
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/devices/qubit/apply_operation.py", line 117, in apply_operation_tensordot
    mat = op.matrix()
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/operation.py", line 791, in matrix
    canonical_matrix = self.compute_matrix(*self.parameters, **self.hyperparameters)
  File "/home/path/venv/lib/python3.10/site-packages/pennylane/operation.py", line 760, in compute_matrix
    raise MatrixUndefinedError
pennylane.operation.MatrixUndefinedError
```


### System information

```shell
Name: PennyLane
Version: 0.35.1
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /home/path/venv/lib/python3.10/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane_Lightning, PennyLane_Lightning_Kokkos

Platform info:           Linux-6.7.7-200.fc39.x86_64-x86_64-with-glibc2.38
Python version:          3.10.10
Numpy version:           1.26.2
Scipy version:           1.11.4
Installed devices:
- lightning.qubit (PennyLane_Lightning-0.35.1)
- default.clifford (PennyLane-0.35.1)
- default.gaussian (PennyLane-0.35.1)
- default.mixed (PennyLane-0.35.1)
- default.qubit (PennyLane-0.35.1)
- default.qubit.autograd (PennyLane-0.35.1)
- default.qubit.jax (PennyLane-0.35.1)
- default.qubit.legacy (PennyLane-0.35.1)
- default.qubit.tf (PennyLane-0.35.1)
- default.qubit.torch (PennyLane-0.35.1)
- default.qutrit (PennyLane-0.35.1)
- null.qubit (PennyLane-0.35.1)
- lightning.kokkos (PennyLane_Lightning_Kokkos-0.35.1)
```


### Existing GitHub issues

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


Discussion:
Thanks @sabrinaherbst! This does look like a potential bug which seems to be coming from the presence of `qml.IQPEmbedding`. We'll get back to you soon on the best way to resolve this.
So I'm a little confused now.  I see in the documentation that we say:
```
 a device with finite shots is used, the hardware compatible transform :func:`~.pennylane.metric_tensor` is used.
```

but then is source-code we use the `metric_tensor` when:
```
if device.shots and isinstance(device, (DefaultQubitLegacy, DefaultQubit)):
```
AKA only when it's a numpy simulator.  Potentially we just mean `not isinstance` instead?
@albi3ro yeah seems like a bug/typo 🤔 

Although perhaps it should be

```python
if (device.shots is not None) or (device is not defaultqubit):
```

or equivalently

```python
if not (device.shots is None and device is defaultqubit):
```

since the adjoint metric tensor can only be used if:

- `shots is None` _and_
- device is `default.qubit`

Also, treating `device.shots` (None or int) as a boolean could be problematic in edge cases?


---

On second thought, inverting the if statement would make this easier to read:

```python
if device.shots is None and isinstance(device, defaultqubit):
    return adjoint_metric_tensor

return hw_compatible_metric_tensor
```

Resolving patch:
diff --git a/pennylane/qinfo/transforms.py b/pennylane/qinfo/transforms.py
--- a/pennylane/qinfo/transforms.py
+++ b/pennylane/qinfo/transforms.py
@@ -736,7 +736,7 @@ def circ(params):
 
     """
 
-    if device.shots and isinstance(device, (DefaultQubitLegacy, DefaultQubit)):
+    if device.shots or not isinstance(device, (DefaultQubitLegacy, DefaultQubit)):
         tapes, processing_fn = metric_tensor(tape, *args, **kwargs)
 
         def processing_fn_multiply(res):


Test patch:
diff --git a/tests/qinfo/test_fisher.py b/tests/qinfo/test_fisher.py
--- a/tests/qinfo/test_fisher.py
+++ b/tests/qinfo/test_fisher.py
@@ -145,12 +145,19 @@ def circ(params):
         res = qml.qinfo.classical_fisher(circ)(params)
         assert np.allclose(res, n_wires * np.ones((n_params, n_params)), atol=1)
 
-    def test_quantum_fisher_info(self):
+    @pytest.mark.parametrize(
+        "dev",
+        (
+            qml.device("default.qubit"),
+            qml.device("default.mixed", wires=3),
+            qml.device("lightning.qubit", wires=3),
+        ),
+    )
+    def test_quantum_fisher_info(self, dev):
         """Integration test of quantum fisher information matrix CFIM. This is just calling ``qml.metric_tensor`` or ``qml.adjoint_metric_tensor`` and multiplying by a factor of 4"""
 
         n_wires = 2
 
-        dev = qml.device("default.qubit", wires=n_wires)
         dev_hard = qml.device("default.qubit", wires=n_wires + 1, shots=1000)
 
         def qfunc(params):