Skip to content

Add error if differentiating diff_method=None #6770

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
* The coefficients of observables now have improved differentiability.
[(#6598)](https://github.com/PennyLaneAI/pennylane/pull/6598)

* An informative error is raised when a `QNode` with `diff_method=None` is differentiated.
[(#6770)](https://github.com/PennyLaneAI/pennylane/pull/6770)

<h3>Breaking changes 💔</h3>

* The ``tape`` and ``qtape`` properties of ``QNode`` have been removed.
Expand Down
20 changes: 20 additions & 0 deletions pennylane/workflow/jacobian_products.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,26 @@ def execute_and_compute_jacobian(self, tapes: QuantumScriptBatch) -> tuple[Resul
"""


class NoGradients(JacobianProductCalculator):
"""A jacobian product calculator that raises errors when a vjp or jvp is requested."""

error_msg = "Derivatives cannot be calculated with diff_method=None"

def compute_jacobian(self, tapes: QuantumScriptBatch) -> tuple:
raise qml.QuantumFunctionError(NoGradients.error_msg)

def compute_vjp(self, tapes: QuantumScriptBatch, dy: Sequence[Sequence[TensorLike]]) -> tuple:
raise qml.QuantumFunctionError(NoGradients.error_msg)

def execute_and_compute_jvp(
self, tapes: QuantumScriptBatch, tangents: Sequence[Sequence[TensorLike]]
) -> tuple[ResultBatch, tuple]:
raise qml.QuantumFunctionError(NoGradients.error_msg)

def execute_and_compute_jacobian(self, tapes: QuantumScriptBatch) -> tuple[ResultBatch, tuple]:
raise qml.QuantumFunctionError(NoGradients.error_msg)


class TransformJacobianProducts(JacobianProductCalculator):
"""Compute VJPs, JVPs and Jacobians via a gradient transform :class:`~.TransformDispatcher`.

Expand Down
4 changes: 1 addition & 3 deletions pennylane/workflow/qnode.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,9 +550,7 @@ def __init__(
# input arguments
self.func = func
self.device = device
self._interface = (
Interface.NUMPY if diff_method is None else get_canonical_interface_name(interface)
)
self._interface = get_canonical_interface_name(interface)
self.diff_method = diff_method
mcm_config = qml.devices.MCMConfig(mcm_method=mcm_method, postselect_mode=postselect_mode)
cache = (max_diff > 1) if cache == "auto" else cache
Expand Down
11 changes: 5 additions & 6 deletions pennylane/workflow/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
DeviceDerivatives,
DeviceJacobianProducts,
JacobianProductCalculator,
NoGradients,
TransformJacobianProducts,
)

Expand Down Expand Up @@ -128,6 +129,9 @@ class (`jpc`) required for gradient computations.

execute_fn = inner_execute

if config.gradient_method is None:
return NoGradients(), execute_fn

if config.use_device_jacobian_product:
return DeviceJacobianProducts(device, config), execute_fn

Expand Down Expand Up @@ -276,12 +280,7 @@ def run(

# Exiting early if we do not need to deal with an interface boundary
no_interface_boundary_required = (
config.interface == Interface.NUMPY
or config.gradient_method
in {
None,
"backprop",
}
config.interface == Interface.NUMPY or config.gradient_method == "backprop"
)
if no_interface_boundary_required:
results = inner_execute(tapes)
Expand Down
47 changes: 26 additions & 21 deletions tests/gradients/core/test_pulse_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,13 +896,15 @@ def test_constant_ry(self, num_split_times, t):
dev = qml.device("default.qubit", wires=1)
# Effective rotation parameter
p = params[0] * delta_t
r = qml.execute([tape], dev, None)
assert qml.math.isclose(r, jnp.cos(2 * p), atol=1e-4)
tapes, fn = stoch_pulse_grad(tape, num_split_times=num_split_times)
assert len(tapes) == num_split_times * 2

res = fn(qml.execute(tapes, dev, None))
assert qml.math.isclose(res, -2 * jnp.sin(2 * p) * delta_t)

# note that qml.execute changes trainable params
r = qml.execute([tape], dev, None)
assert qml.math.isclose(r, jnp.cos(2 * p), atol=1e-4)
jax.clear_caches()

def test_constant_ry_argnum(self):
Expand All @@ -924,14 +926,15 @@ def test_constant_ry_argnum(self):
dev = qml.device("default.qubit", wires=1)
# Effective rotation parameter
p = params[0] * t
r = qml.execute([tape], dev, None)
assert qml.math.isclose(r, jnp.cos(2 * p + y), atol=1e-4)
num_split_times = 1
tapes, fn = stoch_pulse_grad(tape, num_split_times=num_split_times, argnum=0)
assert len(tapes) == num_split_times * 2

res = fn(qml.execute(tapes, dev, None))
assert qml.math.isclose(res, -2 * jnp.sin(2 * p + y) * t)

r = qml.execute([tape], dev, None)
assert qml.math.isclose(r, jnp.cos(2 * p + y), atol=1e-4)
jax.clear_caches()

@pytest.mark.parametrize("num_split_times", [1, 3])
Expand All @@ -956,13 +959,13 @@ def test_constant_ry_rescaled(self, num_split_times, t):
prefactor = np.sqrt(0.85)
# Effective rotation parameter
p = params[0] * (delta_t := T[-1] - T[0]) * prefactor
r = qml.execute([tape], dev, None)
assert qml.math.isclose(r, jnp.cos(2 * p), atol=1e-4)
tapes, fn = stoch_pulse_grad(tape, num_split_times=num_split_times)
assert len(tapes) == num_split_times * 2

res = fn(qml.execute(tapes, dev, None))
assert qml.math.isclose(res, -2 * jnp.sin(2 * p) * delta_t * prefactor)
r = qml.execute([tape], dev, None)
assert qml.math.isclose(r, jnp.cos(2 * p), atol=1e-4)
jax.clear_caches()

@pytest.mark.parametrize("t", [0.02, (0.5, 0.6)])
Expand Down Expand Up @@ -991,8 +994,6 @@ def test_sin_envelope_rz_expval(self, t):
+ x / y * (jnp.sin(y * T[1]) * T[1] - jnp.sin(y * T[0]) * T[0]),
]
)
r = qml.execute([tape], dev, None)
assert qml.math.isclose(r, jnp.cos(2 * theta))

num_split_times = 5
tapes, fn = stoch_pulse_grad(tape, num_split_times=num_split_times)
Expand All @@ -1002,6 +1003,8 @@ def test_sin_envelope_rz_expval(self, t):
exp_grad = -2 * jnp.sin(2 * theta) * theta_jac
# classical Jacobian is being estimated with the Monte Carlo sampling -> coarse tolerance
assert qml.math.allclose(res, exp_grad, atol=0.2)
r = qml.execute([tape], dev, None)
assert qml.math.isclose(r, jnp.cos(2 * theta))
jax.clear_caches()

@pytest.mark.parametrize("t", [0.02, (0.5, 0.6)])
Expand Down Expand Up @@ -1030,9 +1033,6 @@ def test_sin_envelope_rx_probs(self, t):
+ x / y * (jnp.sin(y * T[1]) * T[1] - jnp.sin(y * T[0]) * T[0]),
]
)
r = qml.execute([tape], dev, None)
exp_probs = jnp.array([jnp.cos(theta) ** 2, jnp.sin(theta) ** 2])
assert qml.math.allclose(r, exp_probs)

num_split_times = 5
tapes, fn = stoch_pulse_grad(tape, num_split_times=num_split_times)
Expand All @@ -1043,6 +1043,9 @@ def test_sin_envelope_rx_probs(self, t):
exp_jac = jnp.tensordot(probs_jac, theta_jac, axes=0)
# classical Jacobian is being estimated with the Monte Carlo sampling -> coarse tolerance
assert qml.math.allclose(jac, exp_jac, atol=0.2)
r = qml.execute([tape], dev, None)
exp_probs = jnp.array([jnp.cos(theta) ** 2, jnp.sin(theta) ** 2])
assert qml.math.allclose(r, exp_probs)
jax.clear_caches()

@pytest.mark.parametrize("t", [0.02, (0.5, 0.6)])
Expand Down Expand Up @@ -1071,11 +1074,6 @@ def test_sin_envelope_rx_expval_probs(self, t):
+ x / y * (jnp.sin(y * T[1]) * T[1] - jnp.sin(y * T[0]) * T[0]),
]
)
r = qml.execute([tape], dev, None)[0]
exp = (jnp.cos(2 * theta), jnp.array([jnp.cos(theta) ** 2, jnp.sin(theta) ** 2]))
assert isinstance(r, tuple) and len(r) == 2
assert qml.math.allclose(r[0], exp[0])
assert qml.math.allclose(r[1], exp[1])

num_split_times = 5
tapes, fn = stoch_pulse_grad(tape, num_split_times=num_split_times)
Expand All @@ -1088,6 +1086,12 @@ def test_sin_envelope_rx_expval_probs(self, t):
# classical Jacobian is being estimated with the Monte Carlo sampling -> coarse tolerance
for j, e in zip(jac, exp_jac):
assert qml.math.allclose(j, e, atol=0.2)

r = qml.execute([tape], dev, None)[0]
exp = (jnp.cos(2 * theta), jnp.array([jnp.cos(theta) ** 2, jnp.sin(theta) ** 2]))
assert isinstance(r, tuple) and len(r) == 2
assert qml.math.allclose(r[0], exp[0])
assert qml.math.allclose(r[1], exp[1])
jax.clear_caches()

@pytest.mark.parametrize("t", [0.02, (0.5, 0.6)])
Expand All @@ -1106,8 +1110,6 @@ def test_pwc_envelope_rx(self, t, seed):

# Effective rotation parameter
p = jnp.mean(params[0]) * (T[1] - T[0])
r = qml.execute([tape], dev, None)
assert qml.math.isclose(r, jnp.cos(2 * p))
num_split_times = 5
tapes, fn = stoch_pulse_grad(tape, num_split_times=num_split_times, sampler_seed=seed)
assert len(tapes) == 2 * num_split_times
Expand All @@ -1118,6 +1120,8 @@ def test_pwc_envelope_rx(self, t, seed):
assert qml.math.allclose(
res, -2 * jnp.sin(2 * p) * (T[1] - T[0]) / len(params[0]), atol=0.01
)
r = qml.execute([tape], dev, None)
assert qml.math.isclose(r, jnp.cos(2 * p))
jax.clear_caches()

@pytest.mark.parametrize("t", [2.0, 3, (0.5, 0.6)])
Expand All @@ -1136,10 +1140,7 @@ def test_constant_commuting(self, t):
tape = qml.tape.QuantumScript([op], [qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))])

dev = qml.device("default.qubit", wires=2)
r = qml.execute([tape], dev, None)
# Effective rotation parameters
p = [_p * (T[1] - T[0]) for _p in params]
assert qml.math.isclose(r, jnp.cos(2 * p[0]) * jnp.cos(2 * p[1]))
tapes, fn = stoch_pulse_grad(tape)
assert len(tapes) == 4

Expand All @@ -1149,6 +1150,10 @@ def test_constant_commuting(self, t):
-2 * jnp.sin(2 * p[1]) * jnp.cos(2 * p[0]) * (T[1] - T[0]),
]
assert qml.math.allclose(res, exp_grad)
r = qml.execute([tape], dev, None)
# Effective rotation parameters
exp = jnp.cos(2 * p[0]) * jnp.cos(2 * p[1])
assert qml.math.isclose(r, exp)
jax.clear_caches()

@pytest.mark.slow
Expand Down
10 changes: 6 additions & 4 deletions tests/gradients/core/test_pulse_odegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,13 +1016,14 @@ def test_single_pulse_single_term(self, shots, tol, seed):
op = qml.evolve(H)([x], t=t)
tape = qml.tape.QuantumScript([op], [qml.expval(Z(0))], shots=shots)

val = qml.execute([tape], dev)
theta = integral_of_polyval(x, t)
assert qml.math.allclose(val, jnp.cos(2 * theta), atol=tol)

_tapes, fn = pulse_odegen(tape)
assert len(_tapes) == 2

val = qml.execute([tape], dev)
assert qml.math.allclose(val, jnp.cos(2 * theta), atol=tol)

grad = fn(qml.execute(_tapes, dev))
par_jac = jax.jacobian(integral_of_polyval)(x, t)
exp_grad = -2 * par_jac * jnp.sin(2 * theta)
Expand Down Expand Up @@ -1090,9 +1091,7 @@ def test_single_pulse_multi_term_argnum(self, argnum):
op = qml.evolve(H)([x, y], t=t)
tape = qml.tape.QuantumScript([op], [qml.expval(Z(0))])

val = qml.execute([tape], dev)
theta = integral_of_polyval(x, t) + y * (t[1] - t[0])
assert qml.math.allclose(val, jnp.cos(2 * theta))

# Argnum=[0] or 0
par_jac_0 = jax.jacobian(integral_of_polyval)(x, t)
Expand All @@ -1105,6 +1104,9 @@ def test_single_pulse_multi_term_argnum(self, argnum):
_tapes, fn = pulse_odegen(tape, argnum=argnum)
assert len(_tapes) == 2

val = qml.execute([tape], dev)
assert qml.math.allclose(val, jnp.cos(2 * theta))

grad = fn(qml.execute(_tapes, dev))
assert isinstance(grad, tuple) and len(grad) == 2
assert isinstance(grad[0], jnp.ndarray) and grad[0].shape == x.shape
Expand Down
8 changes: 4 additions & 4 deletions tests/logging/test_logging_autograd.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def circuit(params):
return qml.expval(qml.PauliZ(0))

circuit(params)
assert len(caplog.records) == 10
assert len(caplog.records) == 11
log_records_expected = [
(
"pennylane.workflow.qnode",
Expand All @@ -83,7 +83,7 @@ def circuit(params):
"pennylane.workflow.execution",
[
"device=<default.qubit device (wires=2)",
"diff_method=None, interface=numpy",
"diff_method=None, interface=Interface.AUTOGRAD",
],
),
(
Expand Down Expand Up @@ -169,7 +169,7 @@ def circuit(params):

circuit(params)

assert len(caplog.records) == 8
assert len(caplog.records) == 9

log_records_expected = [
(
Expand All @@ -188,7 +188,7 @@ def circuit(params):
"pennylane.workflow.execution",
[
"device=<default.qutrit.mixed device (wires=2)",
"diff_method=None, interface=None",
"diff_method=None, interface=Interface.AUTOGRAD",
],
),
]
Expand Down
27 changes: 26 additions & 1 deletion tests/test_qnode.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ def circuit(x):
qml.RX(x, wires=0)
return qml.expval(qml.PauliZ(0))

assert circuit.interface == "numpy"
assert circuit.interface == "auto"
assert circuit.device is dev

# QNode can still be executed
Expand Down Expand Up @@ -1063,6 +1063,31 @@ def circuit():
with pytest.raises(qml.QuantumFunctionError, match="device_vjp=True is not supported"):
circuit()

@pytest.mark.parametrize(
"interface",
(
pytest.param("autograd", marks=pytest.mark.autograd),
pytest.param("jax", marks=pytest.mark.jax),
pytest.param("torch", marks=pytest.mark.torch),
pytest.param("tensorflow", marks=pytest.mark.tf),
),
)
def test_error_if_differentiate_diff_method_None(self, interface):
"""Test that an error is raised if differentiating a qnode with diff_method=None"""

@qml.qnode(qml.device("reference.qubit", wires=1), diff_method=None)
def circuit(x):
qml.RX(x, 0)
return qml.expval(qml.Z(0))

x = qml.math.asarray(0.5, like=interface, requires_grad=True)

res = circuit(x) # execution works fine
assert qml.math.allclose(res, np.cos(0.5))

with pytest.raises(qml.QuantumFunctionError, match="with diff_method=None"):
qml.math.grad(circuit)(x)


class TestShots:
"""Unit tests for specifying shots per call."""
Expand Down
27 changes: 27 additions & 0 deletions tests/workflow/interfaces/test_jacobian_products.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
DeviceDerivatives,
DeviceJacobianProducts,
JacobianProductCalculator,
NoGradients,
TransformJacobianProducts,
)

Expand Down Expand Up @@ -79,6 +80,32 @@ def _tol_for_shots(shots):
return 0.05 if shots else 1e-6


def test_no_gradients():
"""Test that errors are raised when derivatives are requested from `NoGradients`."""

jpc = NoGradients()

with pytest.raises(
qml.QuantumFunctionError, match="cannot be calculated with diff_method=None"
):
jpc.compute_jacobian(())

with pytest.raises(
qml.QuantumFunctionError, match="cannot be calculated with diff_method=None"
):
jpc.compute_vjp((), ())

with pytest.raises(
qml.QuantumFunctionError, match="cannot be calculated with diff_method=None"
):
jpc.execute_and_compute_jvp((), ())

with pytest.raises(
qml.QuantumFunctionError, match="cannot be calculated with diff_method=None"
):
jpc.execute_and_compute_jacobian(())


# pylint: disable=too-few-public-methods
class TestBasics:
"""Test initialization and repr for jacobian product calculator classes."""
Expand Down
Loading