Skip to content

Commit a48fbf5

Browse files
authored
Fix qnode_spectrum with interface="auto" (#6622)
**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]
1 parent cb683fe commit a48fbf5

File tree

6 files changed

+71
-22
lines changed

6 files changed

+71
-22
lines changed

doc/releases/changelog-dev.md

+10
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@
6060

6161
<h3>Improvements 🛠</h3>
6262

63+
* Raises a comprehensive error when using `qml.fourier.qnode_spectrum` with standard numpy
64+
arguments and `interface="auto"`.
65+
[(#6622)](https://github.com/PennyLaneAI/pennylane/pull/6622)
66+
6367
* Added support for the `wire_options` dictionary to customize wire line formatting in `qml.draw_mpl` circuit
6468
visualizations, allowing global and per-wire customization with options like `color`, `linestyle`, and `linewidth`.
6569
[(#6486)](https://github.com/PennyLaneAI/pennylane/pull/6486)
@@ -120,6 +124,11 @@
120124

121125
<h3>Breaking changes 💔</h3>
122126

127+
* `qml.fourier.qnode_spectrum` no longer automatically converts pure numpy parameters to the
128+
Autograd framework. As the function uses automatic differentiation for validation, parameters
129+
from an autodiff framework have to be used.
130+
[(#6622)](https://github.com/PennyLaneAI/pennylane/pull/6622)
131+
123132
* `qml.math.jax_argnums_to_tape_trainable` is moved and made private to avoid a qnode dependency
124133
in the math module.
125134
[(#6609)](https://github.com/PennyLaneAI/pennylane/pull/6609)
@@ -251,3 +260,4 @@ William Maxwell,
251260
Andrija Paurevic,
252261
Justin Pickering,
253262
Jay Soni,
263+
David Wierichs,

pennylane/fourier/circuit_spectrum.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,9 @@ def circuit(x, w):
122122
res = qml.fourier.circuit_spectrum(circuit)(x, w)
123123
124124
>>> print(qml.draw(circuit)(x, w))
125-
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>
126-
1: ──RX(2.00)──Rot(0.56,0.61,0.96)──RX(2.00)──Rot(0.32,0.49,0.77)───────────┤
127-
2: ──RX(3.00)──Rot(0.11,0.63,0.31)──RX(3.00)──Rot(0.52,0.46,0.83)───────────┤
125+
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>
126+
1: ──RX(2.00,"x1")──Rot(0.70,0.12,0.60)──RX(2.00,"x1")──Rot(0.04,0.03,0.88)────────────────┤
127+
2: ──RX(3.00,"x2")──Rot(0.65,0.87,0.05)──RX(3.00,"x2")──Rot(0.37,0.53,0.02)────────────────┤
128128
129129
>>> for inp, freqs in res.items():
130130
>>> print(f"{inp}: {freqs}")

pennylane/fourier/qnode_spectrum.py

+22-7
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,12 @@ def qnode_spectrum(qnode, encoding_args=None, argnum=None, decimals=8, validatio
224224
as long as this processing is *linear*. In particular, constant
225225
prefactors for the encoding arguments are allowed.
226226
227+
.. warning::
228+
229+
In order to validate the preprocessing of the QNode arguments, automatic
230+
differentiation is used by ``qnode_spectrum``. Therefore, pure Numpy parameters
231+
are not supported, but one of the machine learning frameworks has to be used.
232+
227233
**Example**
228234
229235
Consider the following example, which uses non-trainable inputs ``x``, ``y`` and ``z``
@@ -246,10 +252,11 @@ def circuit(x, y, z, w):
246252
247253
This circuit looks as follows:
248254
249-
>>> x = np.array([1., 2., 3.])
250-
>>> y = np.array([0.1, 0.3, 0.5])
251-
>>> z = -1.8
252-
>>> w = np.random.random((2, n_qubits, 3))
255+
>>> from pennylane import numpy as pnp
256+
>>> x = pnp.array([1., 2., 3.])
257+
>>> y = pnp.array([0.1, 0.3, 0.5])
258+
>>> z = pnp.array(-1.8)
259+
>>> w = pnp.random.random((2, n_qubits, 3))
253260
>>> print(qml.draw(circuit)(x, y, z, w))
254261
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>
255262
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 circuit(x, y, z):
351358
First, note that we assigned ``id`` labels to the gates for which we will use
352359
``circuit_spectrum``. This allows us to choose these gates in the computation:
353360
354-
>>> x, y, z = 0.1, 0.2, 0.3
361+
>>> x, y, z = pnp.array(0.1, 0.2, 0.3)
355362
>>> circuit_spec_fn = qml.fourier.circuit_spectrum(circuit, encoding_gates=["x","y0","y1"])
356363
>>> circuit_spec = circuit_spec_fn(x, y, z)
357364
>>> for _id, spec in circuit_spec.items():
@@ -363,7 +370,7 @@ def circuit(x, y, z):
363370
As we can see, the preprocessing in the QNode is not included in the simple spectrum.
364371
In contrast, the output of ``qnode_spectrum`` is:
365372
366-
>>> adv_spec = qml.fourier.qnode_spectrum(circuit, encoding_args={"y", "z"})
373+
>>> adv_spec = qml.fourier.qnode_spectrum(circuit, encoding_args={"y", "z"})(x, y, z)
367374
>>> for _id, spec in adv_spec.items():
368375
... print(f"{_id}: {spec}")
369376
y: {(): [-2.3, 0.0, 2.3]}
@@ -387,7 +394,15 @@ def wrapper(*args, **kwargs):
387394
old_interface = qnode.interface
388395

389396
if old_interface == "auto":
390-
qnode.interface = qml.math.get_interface(*args, *list(kwargs.values()))
397+
new_interface = qml.math.get_interface(*args, *list(kwargs.values()))
398+
interfaces = [qml.math.get_interface(arg) for arg in args]
399+
if any(interface == "numpy" for interface in interfaces):
400+
raise ValueError(
401+
"qnode_spectrum requires an automatic differentiation library to validate "
402+
"classical processing in the QNode. Only pure numpy arguments were provided:"
403+
f"\n{args}\n{kwargs}"
404+
)
405+
qnode.interface = new_interface
391406

392407
jac_fn = qml.gradients.classical_jacobian(
393408
qnode, argnum=argnum, expand_fn=qml.transforms.expand_multipar

pennylane/math/is_independent.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* PyTorch
2222
"""
2323
import warnings
24+
from functools import partial
2425

2526
import numpy as np
2627
from autograd.core import VJPNode
@@ -74,7 +75,7 @@ def _autograd_is_indep_analytic(func, *args, **kwargs):
7475
return True
7576

7677

77-
# pylint: disable=import-outside-toplevel,unnecessary-lambda-assignment,unnecessary-lambda
78+
# pylint: disable=import-outside-toplevel
7879
def _jax_is_indep_analytic(func, *args, **kwargs):
7980
"""Test analytically whether a function is independent of its arguments
8081
using JAX.
@@ -110,7 +111,8 @@ def _jax_is_indep_analytic(func, *args, **kwargs):
110111
"""
111112
import jax
112113

113-
mapped_func = lambda *_args: func(*_args, **kwargs)
114+
mapped_func = partial(func, **kwargs)
115+
114116
_vjp = jax.vjp(mapped_func, *args)[1]
115117
if _vjp.args[0].args != ((),):
116118
return False
@@ -182,7 +184,10 @@ def _get_random_args(args, interface, num, seed, bounds):
182184
tf.random.set_seed(seed)
183185
rnd_args = []
184186
for _ in range(num):
185-
_args = (tf.random.uniform(tf.shape(_arg)) * width + bounds[0] for _arg in args)
187+
_args = (
188+
tf.random.uniform(tf.shape(_arg), dtype=_arg.dtype) * width + bounds[0]
189+
for _arg in args
190+
)
186191
_args = tuple(
187192
tf.Variable(_arg) if isinstance(arg, tf.Variable) else _arg
188193
for _arg, arg in zip(_args, args)
@@ -207,6 +212,7 @@ def _get_random_args(args, interface, num, seed, bounds):
207212
return rnd_args
208213

209214

215+
# pylint:disable=too-many-arguments,too-many-positional-arguments
210216
def _is_indep_numerical(func, interface, args, kwargs, num_pos, seed, atol, rtol, bounds):
211217
"""Test whether a function returns the same output at random positions.
212218
@@ -227,8 +233,6 @@ def _is_indep_numerical(func, interface, args, kwargs, num_pos, seed, atol, rtol
227233
chosen points.
228234
"""
229235

230-
# pylint:disable=too-many-arguments
231-
232236
rnd_args = _get_random_args(args, interface, num_pos, seed, bounds)
233237
original_output = func(*args, **kwargs)
234238
is_tuple_valued = isinstance(original_output, tuple)
@@ -247,6 +251,7 @@ def _is_indep_numerical(func, interface, args, kwargs, num_pos, seed, atol, rtol
247251
return True
248252

249253

254+
# pylint:disable=too-many-arguments,too-many-positional-arguments
250255
def is_independent(
251256
func,
252257
interface,

tests/fourier/test_qnode_spectrum.py

+23-5
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def circuit(x):
261261
qml.RY(0.4, wires=i)
262262
return qml.expval(qml.PauliZ(wires=0))
263263

264-
res = qnode_spectrum(circuit)(0.1)
264+
res = qnode_spectrum(circuit)(pnp.array(0.1, requires_grad=True))
265265
expected_degree = n_qubits * n_layers
266266
assert list(res.keys()) == ["x"] and list(res["x"].keys()) == [()]
267267
assert np.allclose(res["x"][()], range(-expected_degree, expected_degree + 1))
@@ -334,13 +334,31 @@ def circuit(x, last_gate=False):
334334
qml.RX(x, wires=2)
335335
return qml.expval(qml.PauliZ(wires=0))
336336

337-
x = 0.9
337+
x = pnp.array(0.9, requires_grad=True)
338338
res_true = qnode_spectrum(circuit, argnum=[0])(x, last_gate=True)
339339
assert np.allclose(res_true["x"][()], range(-3, 4))
340340

341341
res_false = qnode_spectrum(circuit, argnum=[0])(x, last_gate=False)
342342
assert np.allclose(res_false["x"][()], range(-2, 3))
343343

344+
def test_numpy_parameters_auto_interface(self):
345+
"""Test that the spectrum computation raises an error with pure numpy inputs
346+
and the "auto" interface."""
347+
348+
dev = qml.device("default.qubit", wires=3)
349+
350+
@qml.qnode(dev)
351+
def circuit(x, y):
352+
qml.RX(x[0], wires=0)
353+
qml.RX(x[1], wires=0)
354+
qml.RX(y, wires=1)
355+
return qml.expval(qml.PauliZ(wires=0))
356+
357+
x = np.array([0.9, 0.7])
358+
y = -0.5
359+
with pytest.raises(ValueError, match="Only pure numpy arguments"):
360+
_ = qnode_spectrum(circuit, argnum=[0, 1])(x, y)
361+
344362
def test_multi_par_error(self):
345363
"""Test that an error is thrown if the spectrum of
346364
a multi-parameter gate that cannot be decomposed is requested."""
@@ -361,7 +379,7 @@ def circuit(x):
361379
with pytest.raises(
362380
RecursionError, match="Reached recursion limit trying to decompose operations."
363381
):
364-
qnode_spectrum(circuit)(1.5)
382+
qnode_spectrum(circuit)(pnp.array(1.5))
365383

366384
@pytest.mark.parametrize(
367385
"measurement",
@@ -378,7 +396,7 @@ def circuit(x):
378396
return [qml.expval(qml.PauliX(1)), qml.apply(measurement)]
379397

380398
with pytest.raises(ValueError, match=f"{measurement.__class__.__name__} is not supported"):
381-
qnode_spectrum(circuit)(1.5)
399+
qnode_spectrum(circuit)(pnp.array(1.5))
382400

383401

384402
def circuit9(x, w):
@@ -498,7 +516,7 @@ def test_integration_jax(self):
498516
import jax
499517

500518
x = jax.numpy.array([1.0, 2.0, 3.0])
501-
w = [[-1.0, -2.0, -3.0], [-4.0, -5.0, -6.0]]
519+
w = jax.numpy.array([[-1.0, -2.0, -3.0], [-4.0, -5.0, -6.0]])
502520

503521
dev = qml.device("default.qubit", wires=3)
504522
qnode = qml.QNode(circuit9, dev)

tests/math/test_is_independent.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ class TestIsIndependentTensorflow:
324324
(tf.Variable(0.2),),
325325
(tf.Variable(1.1), tf.constant(3.2), tf.Variable(0.2)),
326326
(tf.Variable(np.array([[0, 9.2], [-1.2, 3.2]])),),
327-
(tf.Variable(0.3), [1, 4, 2], tf.Variable(np.array([0.3, 9.1]))),
327+
(tf.Variable(0.3), tf.constant([1.0, 4, 2]), tf.Variable(np.array([0.3, 9.1]))),
328328
],
329329
)
330330
@pytest.mark.parametrize("bounds", [(-1, 1), (0.1, 1.0211)])
@@ -336,7 +336,8 @@ def test_get_random_args(self, args, num, bounds):
336336
tf.random.set_seed(seed)
337337
for _rnd_args in rnd_args:
338338
expected = tuple(
339-
tf.random.uniform(tf.shape(arg)) * (bounds[1] - bounds[0]) + bounds[0]
339+
tf.random.uniform(tf.shape(arg), dtype=arg.dtype) * (bounds[1] - bounds[0])
340+
+ bounds[0]
340341
for arg in args
341342
)
342343
expected = tuple(

0 commit comments

Comments
 (0)