Repository: PennyLaneAI/pennylane (python)

Issue:
[BUG] Documentation examples on `pennylane.fourier.qnode_spectrum` no longer work
### Expected behavior

I expect to be able to run all the examples in the `pennylane.fourier.qnode_spectrum` https://docs.pennylane.ai/en/stable/code/api/pennylane.fourier.qnode_spectrum.html examples.

### Actual behavior

Examples in the documentation should be fully executable, and not provide any errors.

### Additional information

_No response_

### Source code

```shell
n_qubits = 3
dev = qml.device("default.qubit", wires=n_qubits)

@qml.qnode(dev)
def circuit(x, y, z, w):
    for i in range(n_qubits):
        qml.RX(0.5*x[i], wires=i)
        qml.Rot(w[0,i,0], w[0,i,1], w[0,i,2], wires=i)
        qml.RY(2.3*y[i], wires=i)
        qml.Rot(w[1,i,0], w[1,i,1], w[1,i,2], wires=i)
        qml.RX(z, wires=i)
    return qml.expval(qml.Z(0))

x = np.array([1., 2., 3.])
y = np.array([0.1, 0.3, 0.5])
z = -1.8
w = np.random.random((2, n_qubits, 3))
print(qml.draw(circuit)(x, y, z, w))

res = qml.fourier.qnode_spectrum(circuit, argnum=[0, 1, 2])(x, y, z, w)
for inp, freqs in res.items():
    print(f"{inp}: {freqs}")
```


### Tracebacks

```shell
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-4-a66971ae3a6a> in <cell line: 24>()
     22 print(qml.draw(circuit)(x, y, z, w))
     23 
---> 24 res = qml.fourier.qnode_spectrum(circuit, argnum=[0, 1, 2])(x, y, z, w)
     25 for inp, freqs in res.items():
     26     print(f"{inp}: {freqs}")

6 frames
/usr/local/lib/python3.10/dist-packages/numpy/core/numeric.py in within_tol(x, y, atol, rtol)
   2330     def within_tol(x, y, atol, rtol):
   2331         with errstate(invalid='ignore'), _no_nep50_warning():
-> 2332             return less_equal(abs(x-y), atol + rtol * abs(y))
   2333 
   2334     x = asanyarray(a)

ValueError: operands could not be broadcast together with shapes (27,3) (3,3)
```


### System information

```shell
Name: PennyLane
Version: 0.39.0
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: /usr/local/lib/python3.10/dist-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, packaging, pennylane-lightning, requests, rustworkx, scipy, toml, typing-extensions
Required-by: PennyLane_Lightning

Platform info:           Linux-6.1.85+-x86_64-with-glibc2.35
Python version:          3.10.12
Numpy version:           1.26.4
Scipy version:           1.13.1
Installed devices:
- lightning.qubit (PennyLane_Lightning-0.39.0)
- default.clifford (PennyLane-0.39.0)
- default.gaussian (PennyLane-0.39.0)
- default.mixed (PennyLane-0.39.0)
- default.qubit (PennyLane-0.39.0)
- default.qutrit (PennyLane-0.39.0)
- default.qutrit.mixed (PennyLane-0.39.0)
- default.tensor (PennyLane-0.39.0)
- null.qubit (PennyLane-0.39.0)
- reference.qubit (PennyLane-0.39.0)
```


### Existing GitHub issues

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

Pull request description:
Fix `qnode_spectrum` with `interface="auto"`
**Context:**
An example from the docstring of `qnode_spectrum` is failing because the detection of trainable arguments fails when using Autograd via vanilla numpy inputs and `interface="auto"` (default).

Note that this problem is caused by a bug in `qml.math.is_independent`, but so far only surfaces in the specific context of `qnode_spectrum`, which uses this function to check a Jacobian to be constant.

**Description of the Change:**
Add a warning to the docs that pure numpy parameters are not supported.
Raise an error if pure numpy parameters are present.
Smaller updates relating to code quality/linting.

**Benefits:**
Fixes #6593 

**Possible Drawbacks:**
N/A

**Related GitHub Issues:**
Closes #6593 
[sc-78453]

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 7d9dfbc1e..6eabafdeb 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -60,6 +60,10 @@
 
 <h3>Improvements 🛠</h3>
 
+* Raises a comprehensive error when using `qml.fourier.qnode_spectrum` with standard numpy
+  arguments and `interface="auto"`.
+  [(#6622)](https://github.com/PennyLaneAI/pennylane/pull/6622)
+
 * Added support for the `wire_options` dictionary to customize wire line formatting in `qml.draw_mpl` circuit
   visualizations, allowing global and per-wire customization with options like `color`, `linestyle`, and `linewidth`.
   [(#6486)](https://github.com/PennyLaneAI/pennylane/pull/6486)
@@ -120,6 +124,11 @@
 
 <h3>Breaking changes 💔</h3>
 
+* `qml.fourier.qnode_spectrum` no longer automatically converts pure numpy parameters to the
+  Autograd framework. As the function uses automatic differentiation for validation, parameters
+  from an autodiff framework have to be used.
+  [(#6622)](https://github.com/PennyLaneAI/pennylane/pull/6622)
+
 * `qml.math.jax_argnums_to_tape_trainable` is moved and made private to avoid a qnode dependency
   in the math module.
   [(#6609)](https://github.com/PennyLaneAI/pennylane/pull/6609)
@@ -251,3 +260,4 @@ William Maxwell,
 Andrija Paurevic,
 Justin Pickering,
 Jay Soni,
+David Wierichs,
diff --git a/pennylane/fourier/circuit_spectrum.py b/pennylane/fourier/circuit_spectrum.py
index 030418c9c..65a2c4382 100644
--- a/pennylane/fourier/circuit_spectrum.py
+++ b/pennylane/fourier/circuit_spectrum.py
@@ -122,9 +122,9 @@ def circuit_spectrum(
         res = qml.fourier.circuit_spectrum(circuit)(x, w)
 
     >>> print(qml.draw(circuit)(x, w))
-    0: ──RX(1.00)──Rot(0.53,0.70,0.90)──RX(1.00)──Rot(0.81,0.38,0.43)──RZ(1.00)─┤  <Z>
-    1: ──RX(2.00)──Rot(0.56,0.61,0.96)──RX(2.00)──Rot(0.32,0.49,0.77)───────────┤
-    2: ──RX(3.00)──Rot(0.11,0.63,0.31)──RX(3.00)──Rot(0.52,0.46,0.83)───────────┤
+    0: ──RX(1.00,"x0")──Rot(0.03,0.03,0.37)──RX(1.00,"x0")──Rot(0.35,0.89,0.29)──RZ(1.00,"x0")─┤  <Z>
+    1: ──RX(2.00,"x1")──Rot(0.70,0.12,0.60)──RX(2.00,"x1")──Rot(0.04,0.03,0.88)────────────────┤
+    2: ──RX(3.00,"x2")──Rot(0.65,0.87,0.05)──RX(3.00,"x2")──Rot(0.37,0.53,0.02)────────────────┤
 
     >>> for inp, freqs in res.items():
     >>>     print(f"{inp}: {freqs}")
diff --git a/pennylane/fourier/qnode_spectrum.py b/pennylane/fourier/qnode_spectrum.py
index 3e050fd49..6c5e27803 100644
--- a/pennylane/fourier/qnode_spectrum.py
+++ b/pennylane/fourier/qnode_spectrum.py
@@ -224,6 +224,12 @@ def qnode_spectrum(qnode, encoding_args=None, argnum=None, decimals=8, validatio
         as long as this processing is *linear*. In particular, constant
         prefactors for the encoding arguments are allowed.
 
+    .. warning::
+
+        In order to validate the preprocessing of the QNode arguments, automatic
+        differentiation is used by ``qnode_spectrum``. Therefore, pure Numpy parameters
+        are not supported, but one of the machine learning frameworks has to be used.
+
     **Example**
 
     Consider the following example, which uses non-trainable inputs ``x``, ``y`` and ``z``
@@ -246,10 +252,11 @@ def qnode_spectrum(qnode, encoding_args=None, argnum=None, decimals=8, validatio
 
     This circuit looks as follows:
 
-    >>> x = np.array([1., 2., 3.])
-    >>> y = np.array([0.1, 0.3, 0.5])
-    >>> z = -1.8
-    >>> w = np.random.random((2, n_qubits, 3))
+    >>> from pennylane import numpy as pnp
+    >>> x = pnp.array([1., 2., 3.])
+    >>> y = pnp.array([0.1, 0.3, 0.5])
+    >>> z = pnp.array(-1.8)
+    >>> w = pnp.random.random((2, n_qubits, 3))
     >>> print(qml.draw(circuit)(x, y, z, w))
     0: ──RX(0.50)──Rot(0.09,0.46,0.54)──RY(0.23)──Rot(0.59,0.22,0.05)──RX(-1.80)─┤  <Z>
     1: ──RX(1.00)──Rot(0.98,0.61,0.07)──RY(0.69)──Rot(0.62,0.00,0.28)──RX(-1.80)─┤
@@ -351,7 +358,7 @@ def qnode_spectrum(qnode, encoding_args=None, argnum=None, decimals=8, validatio
         First, note that we assigned ``id`` labels to the gates for which we will use
         ``circuit_spectrum``. This allows us to choose these gates in the computation:
 
-        >>> x, y, z = 0.1, 0.2, 0.3
+        >>> x, y, z = pnp.array(0.1, 0.2, 0.3)
         >>> circuit_spec_fn = qml.fourier.circuit_spectrum(circuit, encoding_gates=["x","y0","y1"])
         >>> circuit_spec = circuit_spec_fn(x, y, z)
         >>> for _id, spec in circuit_spec.items():
@@ -363,7 +370,7 @@ def qnode_spectrum(qnode, encoding_args=None, argnum=None, decimals=8, validatio
         As we can see, the preprocessing in the QNode is not included in the simple spectrum.
         In contrast, the output of ``qnode_spectrum`` is:
 
-        >>> adv_spec = qml.fourier.qnode_spectrum(circuit, encoding_args={"y", "z"})
+        >>> adv_spec = qml.fourier.qnode_spectrum(circuit, encoding_args={"y", "z"})(x, y, z)
         >>> for _id, spec in adv_spec.items():
         ...     print(f"{_id}: {spec}")
         y: {(): [-2.3, 0.0, 2.3]}
@@ -387,7 +394,15 @@ def qnode_spectrum(qnode, encoding_args=None, argnum=None, decimals=8, validatio
         old_interface = qnode.interface
 
         if old_interface == "auto":
-            qnode.interface = qml.math.get_interface(*args, *list(kwargs.values()))
+            new_interface = qml.math.get_interface(*args, *list(kwargs.values()))
+            interfaces = [qml.math.get_interface(arg) for arg in args]
+            if any(interface == "numpy" for interface in interfaces):
+                raise ValueError(
+                    "qnode_spectrum requires an automatic differentiation library to validate "
+                    "classical processing in the QNode. Only pure numpy arguments were provided:"
+                    f"\n{args}\n{kwargs}"
+                )
+            qnode.interface = new_interface
 
         jac_fn = qml.gradients.classical_jacobian(
             qnode, argnum=argnum, expand_fn=qml.transforms.expand_multipar
diff --git a/pennylane/math/is_independent.py b/pennylane/math/is_independent.py
index d66750aee..9ad2d30fc 100644
--- a/pennylane/math/is_independent.py
+++ b/pennylane/math/is_independent.py
@@ -21,6 +21,7 @@ a function is independent of its arguments for the interfaces
 * PyTorch
 """
 import warnings
+from functools import partial
 
 import numpy as np
 from autograd.core import VJPNode
@@ -74,7 +75,7 @@ def _autograd_is_indep_analytic(func, *args, **kwargs):
     return True
 
 
-# pylint: disable=import-outside-toplevel,unnecessary-lambda-assignment,unnecessary-lambda
+# pylint: disable=import-outside-toplevel
 def _jax_is_indep_analytic(func, *args, **kwargs):
     """Test analytically whether a function is independent of its arguments
     using JAX.
@@ -110,7 +111,8 @@ def _jax_is_indep_analytic(func, *args, **kwargs):
     """
     import jax
 
-    mapped_func = lambda *_args: func(*_args, **kwargs)
+    mapped_func = partial(func, **kwargs)
+
     _vjp = jax.vjp(mapped_func, *args)[1]
     if _vjp.args[0].args != ((),):
         return False
@@ -182,7 +184,10 @@ def _get_random_args(args, interface, num, seed, bounds):
         tf.random.set_seed(seed)
         rnd_args = []
         for _ in range(num):
-            _args = (tf.random.uniform(tf.shape(_arg)) * width + bounds[0] for _arg in args)
+            _args = (
+                tf.random.uniform(tf.shape(_arg), dtype=_arg.dtype) * width + bounds[0]
+                for _arg in args
+            )
             _args = tuple(
                 tf.Variable(_arg) if isinstance(arg, tf.Variable) else _arg
                 for _arg, arg in zip(_args, args)
@@ -207,6 +212,7 @@ def _get_random_args(args, interface, num, seed, bounds):
     return rnd_args
 
 
+# pylint:disable=too-many-arguments,too-many-positional-arguments
 def _is_indep_numerical(func, interface, args, kwargs, num_pos, seed, atol, rtol, bounds):
     """Test whether a function returns the same output at random positions.
 
@@ -227,8 +233,6 @@ def _is_indep_numerical(func, interface, args, kwargs, num_pos, seed, atol, rtol
         chosen points.
     """
 
-    # pylint:disable=too-many-arguments
-
     rnd_args = _get_random_args(args, interface, num_pos, seed, bounds)
     original_output = func(*args, **kwargs)
     is_tuple_valued = isinstance(original_output, tuple)
@@ -247,6 +251,7 @@ def _is_indep_numerical(func, interface, args, kwargs, num_pos, seed, atol, rtol
     return True
 
 
+# pylint:disable=too-many-arguments,too-many-positional-arguments
 def is_independent(
     func,
     interface,


Test patch:
diff --git a/tests/fourier/test_qnode_spectrum.py b/tests/fourier/test_qnode_spectrum.py
index 4918efc11..8fe89517a 100644
--- a/tests/fourier/test_qnode_spectrum.py
+++ b/tests/fourier/test_qnode_spectrum.py
@@ -261,7 +261,7 @@ class TestCircuits:
                     qml.RY(0.4, wires=i)
             return qml.expval(qml.PauliZ(wires=0))
 
-        res = qnode_spectrum(circuit)(0.1)
+        res = qnode_spectrum(circuit)(pnp.array(0.1, requires_grad=True))
         expected_degree = n_qubits * n_layers
         assert list(res.keys()) == ["x"] and list(res["x"].keys()) == [()]
         assert np.allclose(res["x"][()], range(-expected_degree, expected_degree + 1))
@@ -334,13 +334,31 @@ class TestCircuits:
                 qml.RX(x, wires=2)
             return qml.expval(qml.PauliZ(wires=0))
 
-        x = 0.9
+        x = pnp.array(0.9, requires_grad=True)
         res_true = qnode_spectrum(circuit, argnum=[0])(x, last_gate=True)
         assert np.allclose(res_true["x"][()], range(-3, 4))
 
         res_false = qnode_spectrum(circuit, argnum=[0])(x, last_gate=False)
         assert np.allclose(res_false["x"][()], range(-2, 3))
 
+    def test_numpy_parameters_auto_interface(self):
+        """Test that the spectrum computation raises an error with pure numpy inputs
+        and the "auto" interface."""
+
+        dev = qml.device("default.qubit", wires=3)
+
+        @qml.qnode(dev)
+        def circuit(x, y):
+            qml.RX(x[0], wires=0)
+            qml.RX(x[1], wires=0)
+            qml.RX(y, wires=1)
+            return qml.expval(qml.PauliZ(wires=0))
+
+        x = np.array([0.9, 0.7])
+        y = -0.5
+        with pytest.raises(ValueError, match="Only pure numpy arguments"):
+            _ = qnode_spectrum(circuit, argnum=[0, 1])(x, y)
+
     def test_multi_par_error(self):
         """Test that an error is thrown if the spectrum of
         a multi-parameter gate that cannot be decomposed is requested."""
@@ -361,7 +379,7 @@ class TestCircuits:
         with pytest.raises(
             RecursionError, match="Reached recursion limit trying to decompose operations."
         ):
-            qnode_spectrum(circuit)(1.5)
+            qnode_spectrum(circuit)(pnp.array(1.5))
 
     @pytest.mark.parametrize(
         "measurement",
@@ -378,7 +396,7 @@ class TestCircuits:
             return [qml.expval(qml.PauliX(1)), qml.apply(measurement)]
 
         with pytest.raises(ValueError, match=f"{measurement.__class__.__name__} is not supported"):
-            qnode_spectrum(circuit)(1.5)
+            qnode_spectrum(circuit)(pnp.array(1.5))
 
 
 def circuit9(x, w):
@@ -498,7 +516,7 @@ class TestJax:
         import jax
 
         x = jax.numpy.array([1.0, 2.0, 3.0])
-        w = [[-1.0, -2.0, -3.0], [-4.0, -5.0, -6.0]]
+        w = jax.numpy.array([[-1.0, -2.0, -3.0], [-4.0, -5.0, -6.0]])
 
         dev = qml.device("default.qubit", wires=3)
         qnode = qml.QNode(circuit9, dev)
diff --git a/tests/math/test_is_independent.py b/tests/math/test_is_independent.py
index 5451e3830..b58eeb473 100644
--- a/tests/math/test_is_independent.py
+++ b/tests/math/test_is_independent.py
@@ -324,7 +324,7 @@ class TestIsIndependentTensorflow:
             (tf.Variable(0.2),),
             (tf.Variable(1.1), tf.constant(3.2), tf.Variable(0.2)),
             (tf.Variable(np.array([[0, 9.2], [-1.2, 3.2]])),),
-            (tf.Variable(0.3), [1, 4, 2], tf.Variable(np.array([0.3, 9.1]))),
+            (tf.Variable(0.3), tf.constant([1.0, 4, 2]), tf.Variable(np.array([0.3, 9.1]))),
         ],
     )
     @pytest.mark.parametrize("bounds", [(-1, 1), (0.1, 1.0211)])
@@ -336,7 +336,8 @@ class TestIsIndependentTensorflow:
         tf.random.set_seed(seed)
         for _rnd_args in rnd_args:
             expected = tuple(
-                tf.random.uniform(tf.shape(arg)) * (bounds[1] - bounds[0]) + bounds[0]
+                tf.random.uniform(tf.shape(arg), dtype=arg.dtype) * (bounds[1] - bounds[0])
+                + bounds[0]
                 for arg in args
             )
             expected = tuple(