From 7af90b44ab7a81a5fcfc748c1b1d876cda09eed2 Mon Sep 17 00:00:00 2001 From: soranjh <40344468+soranjh@users.noreply.github.com> Date: Fri, 13 Oct 2023 09:27:45 -0400 Subject: [PATCH 1/8] Return qchem Hamiltonians with real dtype coeffs (#4639) --- doc/releases/changelog-dev.md | 4 ++++ pennylane/fermi/conversion.py | 28 +++++++++++++++++++++------- pennylane/qchem/dipole.py | 2 +- pennylane/qchem/observable_hf.py | 2 +- tests/fermi/test_fermi_mapping.py | 20 ++++++++++++++++++++ tests/qchem/test_dipole.py | 4 +--- tests/qchem/test_hamiltonians.py | 4 ++-- 7 files changed, 50 insertions(+), 14 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index e0ce840e73f..3d7a8d7fc24 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -191,6 +191,10 @@ still a `_qfunc_output` property on `QNode` instances. [(#4651)](https://github.com/PennyLaneAI/pennylane/pull/4651) +* The `qml.jordan_wigner` function has been modified to optionally remove the imaginary components + of the computed qubit operator, if imaginary components are smaller than a threshold. + [(#4639)](https://github.com/PennyLaneAI/pennylane/pull/4639) +

Breaking changes 💔

* The device test suite now converts device kwargs to integers or floats if they can be converted to integers or floats. diff --git a/pennylane/fermi/conversion.py b/pennylane/fermi/conversion.py index 45b109fd28a..7a7fb6aeaf9 100644 --- a/pennylane/fermi/conversion.py +++ b/pennylane/fermi/conversion.py @@ -15,14 +15,20 @@ from functools import singledispatch from typing import Union + +import pennylane as qml from pennylane.operation import Operator -from pennylane.pauli import PauliWord, PauliSentence -from .fermionic import FermiWord, FermiSentence +from pennylane.pauli import PauliSentence, PauliWord + +from .fermionic import FermiSentence, FermiWord # pylint: disable=unexpected-keyword-arg def jordan_wigner( - fermi_operator: (Union[FermiWord, FermiSentence]), ps: bool = False, wire_map: dict = None + fermi_operator: (Union[FermiWord, FermiSentence]), + ps: bool = False, + wire_map: dict = None, + tol: float = None, ) -> Union[Operator, PauliSentence]: r"""Convert a fermionic operator to a qubit operator using the Jordan-Wigner mapping. @@ -49,6 +55,7 @@ def jordan_wigner( wire_map (dict): a dictionary defining how to map the oribitals of the Fermi operator to qubit wires. If None, the integers used to order the orbitals will be used as wire labels. Defaults to None. + tol (float): tolerance for discarding the imaginary part of the coefficients Returns: Union[PauliSentence, Operator]: a linear combination of qubit operators @@ -72,17 +79,17 @@ def jordan_wigner( + (0.25+0j) * X(2) @ X(3) + 0.25j * X(2) @ Y(3) """ - return _jordan_wigner_dispatch(fermi_operator, ps, wire_map) + return _jordan_wigner_dispatch(fermi_operator, ps, wire_map, tol) @singledispatch -def _jordan_wigner_dispatch(fermi_operator, ps, wire_map): +def _jordan_wigner_dispatch(fermi_operator, ps, wire_map, tol): """Dispatches to appropriate function if fermi_operator is a FermiWord or FermiSentence.""" raise ValueError(f"fermi_operator must be a FermiWord or FermiSentence, got: {fermi_operator}") @_jordan_wigner_dispatch.register -def _(fermi_operator: FermiWord, ps=False, wire_map=None): +def _(fermi_operator: FermiWord, ps=False, wire_map=None, tol=None): wires = list(fermi_operator.wires) or [0] identity_wire = wires[0] @@ -104,6 +111,10 @@ def _(fermi_operator: FermiWord, ps=False, wire_map=None): } ) + for pw in qubit_operator: + if tol is not None and abs(qml.math.imag(qubit_operator[pw])) <= tol: + qubit_operator[pw] = qml.math.real(qubit_operator[pw]) + if not ps: # wire_order specifies wires to use for Identity (PauliWord({})) qubit_operator = qubit_operator.operation(wire_order=[identity_wire]) @@ -115,7 +126,7 @@ def _(fermi_operator: FermiWord, ps=False, wire_map=None): @_jordan_wigner_dispatch.register -def _(fermi_operator: FermiSentence, ps=False, wire_map=None): +def _(fermi_operator: FermiSentence, ps=False, wire_map=None, tol=None): wires = list(fermi_operator.wires) or [0] identity_wire = wires[0] @@ -127,6 +138,9 @@ def _(fermi_operator: FermiSentence, ps=False, wire_map=None): for pw in fermi_word_as_ps: qubit_operator[pw] = qubit_operator[pw] + fermi_word_as_ps[pw] * coeff + if tol is not None and abs(qml.math.imag(qubit_operator[pw])) <= tol: + qubit_operator[pw] = qml.math.real(qubit_operator[pw]) + if not ps: qubit_operator = qubit_operator.operation(wire_order=[identity_wire]) diff --git a/pennylane/qchem/dipole.py b/pennylane/qchem/dipole.py index 909869b8f8d..1f1fcaa78a8 100644 --- a/pennylane/qchem/dipole.py +++ b/pennylane/qchem/dipole.py @@ -228,7 +228,7 @@ def _fermionic_dipole(*args): return _fermionic_dipole -def dipole_moment(mol, cutoff=1.0e-18, core=None, active=None): +def dipole_moment(mol, cutoff=1.0e-16, core=None, active=None): r"""Return a function that computes the qubit dipole moment observable. The dipole operator in the second-quantized form is diff --git a/pennylane/qchem/observable_hf.py b/pennylane/qchem/observable_hf.py index 7005928a069..5cb96b52fe1 100644 --- a/pennylane/qchem/observable_hf.py +++ b/pennylane/qchem/observable_hf.py @@ -134,7 +134,7 @@ def qubit_observable(o_ferm, cutoff=1.0e-12): + ((0.775+0j)*(PauliX(wires=[0]) @ PauliX(wires=[1]))) + (0.775j*(PauliX(wires=[0]) @ PauliY(wires=[1]))) """ - h = qml.jordan_wigner(o_ferm, ps=True) + h = qml.jordan_wigner(o_ferm, ps=True, tol=cutoff) h.simplify(tol=cutoff) if active_new_opmath(): diff --git a/tests/fermi/test_fermi_mapping.py b/tests/fermi/test_fermi_mapping.py index 21f218eb405..ddf12ece5b7 100644 --- a/tests/fermi/test_fermi_mapping.py +++ b/tests/fermi/test_fermi_mapping.py @@ -654,3 +654,23 @@ def test_providing_wire_map_fermi_word_to_ps(wire_map, ops): op.simplify() assert ps == op + + +fs1 = FermiSentence({fw1: 1}) + + +@pytest.mark.parametrize( + "fermi_op, qubit_op_data, tol", + ( + (fw1, (-0.25j, (0.25 + 0j), (0.25 + 0j), 0.25j), None), + (fw1, (-0.25j, 0.25, 0.25, 0.25j), 0.0), + (fw1, (-0.25j, 0.25, 0.25, 0.25j), 1.0e-12), + (fs1, (-0.25j, (0.25 + 0j), (0.25 + 0j), 0.25j), None), + (fs1, (-0.25j, 0.25, 0.25, 0.25j), 0.0), + (fs1, (-0.25j, 0.25, 0.25, 0.25j), 1.0e-12), + ), +) +def test_jordan_wigner_tolerance(fermi_op, qubit_op_data, tol): + """Test that jordan_wigner properly removes negligible imaginary components""" + op = jordan_wigner(fermi_op, tol=tol) + assert isinstance(op.data[1], type(qubit_op_data[1])) diff --git a/tests/qchem/test_dipole.py b/tests/qchem/test_dipole.py index 23554cf3518..e19db8fe3f3 100644 --- a/tests/qchem/test_dipole.py +++ b/tests/qchem/test_dipole.py @@ -285,9 +285,7 @@ def test_gradient_expvalD(): mol = qchem.Molecule(symbols, geometry, charge=1, alpha=alpha) args = [mol.alpha] - # TODO: `d_qubit[0]` has coeff dtype complex, but is actually a real-valued Hamiltonian - # default.qubit.legacy casts Hamiltonian expectations to real, but default.qubit does not - dev = qml.device("default.qubit.legacy", wires=6) + dev = qml.device("default.qubit", wires=6) def dipole(mol): @qml.qnode(dev) diff --git a/tests/qchem/test_hamiltonians.py b/tests/qchem/test_hamiltonians.py index 78caa5bf68a..c6c9f3c091a 100644 --- a/tests/qchem/test_hamiltonians.py +++ b/tests/qchem/test_hamiltonians.py @@ -284,7 +284,7 @@ def test_gradient_expvalH(): mol = qchem.Molecule(symbols, geometry, alpha=alpha) args = [alpha] - dev = qml.device("default.qubit.legacy", wires=4) + dev = qml.device("default.qubit", wires=4) def energy(mol): @qml.qnode(dev) @@ -336,7 +336,7 @@ def test_gradient_expvalH(self): mol = qchem.Molecule(symbols, geometry, alpha=alpha) args = [jax.numpy.array(alpha)] - dev = qml.device("default.qubit.legacy", wires=4) + dev = qml.device("default.qubit", wires=4) def energy(mol): @qml.qnode(dev, interface="jax") From 7ce1d4f4cffc783142ee1365cc64f812ef7680aa Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Fri, 13 Oct 2023 10:31:40 -0400 Subject: [PATCH 2/8] Increase codecov coverage (#4652) **Context:** After updating to the new device API, codecov was unhappy with the lost coverage [here](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4436/indirect-changes?src=pr&el=tree-more). Coverage loss was reported in: 1. testing TF support for `compute_eigvals` for `PCPhase` and `IsingZZ` 2. one line in interfaces/torch.py 3. one line in transforms/hamiltonian_expand.py **Description of the Change:** Added tests for points 1 and 3. Reintroduced running `test_vqe.py` test `test_optimize_torch` with DQL (in addition to DQ2) to maintain code coverage from before the device swap, as its not clear if that line is reachable with DQ2. **Benefits:** Code coverage didn't decrease when switching device API --------- Co-authored-by: Matthew Silverman --- tests/ops/qubit/test_parametric_ops.py | 35 +++++++++++++++++++++ tests/test_vqe.py | 5 +-- tests/transforms/test_hamiltonian_expand.py | 19 +++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index 7e874328911..43a47f0d669 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -1247,6 +1247,22 @@ def test_pswap_eigvals_jax(self, phi): evs_expected = [1, 1, -qml.math.exp(1j * phi), qml.math.exp(1j * phi)] assert qml.math.allclose(evs, evs_expected) + @pytest.mark.tf + @pytest.mark.parametrize("phi", np.linspace(-np.pi, np.pi, 10)) + def test_pcphase_eigvals_tf(self, phi): + """Test eigenvalues computation for PCPhase using Tensorflow interface""" + import tensorflow as tf + + param_tf = tf.Variable(phi) + + op = qml.PCPhase(param_tf, dim=2, wires=[0, 1]) + evs = qml.PCPhase.compute_eigvals(*op.parameters, **op.hyperparameters) + evs_expected = np.array( + [np.exp(1j * phi), np.exp(1j * phi), np.exp(-1j * phi), np.exp(-1j * phi)] + ) + + assert qml.math.allclose(evs, evs_expected) + def test_isingxy(self, tol): """Test that the IsingXY operation is correct""" assert np.allclose(qml.IsingXY.compute_matrix(0), np.identity(4), atol=tol, rtol=0) @@ -1520,6 +1536,25 @@ def get_expected(theta): qml.IsingZZ.compute_eigvals(param), np.diagonal(get_expected(param)), atol=tol, rtol=0 ) + @pytest.mark.tf + @pytest.mark.parametrize("phi", np.linspace(-np.pi, np.pi, 10)) + def test_isingzz_eigvals_tf(self, phi): + """Test eigenvalues computation for IsingXY using Tensorflow interface""" + import tensorflow as tf + + param_tf = tf.Variable(phi) + evs = qml.IsingZZ.compute_eigvals(param_tf) + + def get_expected(theta): + neg_imag = np.exp(-1j * theta / 2) + plus_imag = np.exp(1j * theta / 2) + expected = np.array( + np.diag([neg_imag, plus_imag, plus_imag, neg_imag]), dtype=np.complex128 + ) + return expected + + assert qml.math.allclose(evs, np.diagonal(get_expected(phi))) + def test_isingzz_broadcasted(self, tol): """Test that the broadcasted IsingZZ operation is correct""" z = np.zeros(3) diff --git a/tests/test_vqe.py b/tests/test_vqe.py index 2b4c7a3d8eb..a3472d59956 100644 --- a/tests/test_vqe.py +++ b/tests/test_vqe.py @@ -314,12 +314,13 @@ def test_passing_kwargs(self, coeffs, observables, expected): # pylint: disable=protected-access @pytest.mark.torch @pytest.mark.slow + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.qubit.legacy"]) @pytest.mark.parametrize("shots", [None, [(8000, 5)], [(8000, 5), (9000, 4)]]) - def test_optimize_torch(self, shots): + def test_optimize_torch(self, dev_name, shots): """Test that an ExpvalCost with observable optimization gives the same result as another ExpvalCost without observable optimization.""" - dev = qml.device("default.qubit", wires=4, shots=shots) + dev = qml.device(dev_name, wires=4, shots=shots) hamiltonian1 = copy.copy(big_hamiltonian) hamiltonian2 = copy.copy(big_hamiltonian) diff --git a/tests/transforms/test_hamiltonian_expand.py b/tests/transforms/test_hamiltonian_expand.py index 886623740fd..c194f7a80a1 100644 --- a/tests/transforms/test_hamiltonian_expand.py +++ b/tests/transforms/test_hamiltonian_expand.py @@ -292,6 +292,25 @@ def test_hamiltonian_dif_tensorflow(self): g = gtape.gradient(res, var) assert np.allclose(list(g[0]) + list(g[1]), output2) + def test_processing_function_conditional_clause(self): + """Test the conditional logic for `len(c_group) == 1` and `len(r_group) != 1` + in the processing function returned by hamiltonian_expand, accessed when + using a shot vector and grouping if the terms don't commute with each other.""" + + dev_with_shot_vector = qml.device("default.qubit", shots=(10, 10, 10)) + + H = qml.Hamiltonian([1, 2.0], [qml.PauliZ(0), qml.PauliX(0)]) + H.compute_grouping() + + @qml.transforms.hamiltonian_expand + @qml.qnode(dev_with_shot_vector) + def circuit(): + return qml.expval(H) + + res = circuit() + + assert res.shape == (3,) + with AnnotatedQueue() as s_tape1: qml.PauliX(0) From eafabe418a4fd83e36333abab762c119ef5b80a1 Mon Sep 17 00:00:00 2001 From: Christina Lee Date: Fri, 13 Oct 2023 11:45:47 -0400 Subject: [PATCH 3/8] Fix grover operator work wires (#4668) The default to `GroverOperator` for work wires was `None`. Unfortunately this was being interpreted as `work_wires = Wires([None,])`. This PR makes it so that `work_wires=None` is interpreted as `work_wires = Wires([])`. Fixes https://github.com/PennyLaneAI/pennylane-qiskit/issues/339 --------- Co-authored-by: Matthew Silverman --- doc/releases/changelog-dev.md | 3 +++ pennylane/templates/subroutines/grover.py | 5 ++++- tests/templates/test_subroutines/test_grover.py | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 3d7a8d7fc24..d500a55edca 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -340,6 +340,9 @@

Bug fixes 🐛

+* Providing `work_wires=None` to `qml.GroverOperator` no longer interprets `None` as a wire. + [(#4668)](https://github.com/PennyLaneAI/pennylane/pull/4668) + * Fixed issue where `__copy__` method of the `qml.Select()` operator attempted to access un-initialized data. [(#4551)](https://github.com/PennyLaneAI/pennylane/pull/4551) diff --git a/pennylane/templates/subroutines/grover.py b/pennylane/templates/subroutines/grover.py index b6178e0e701..5f3cbf09461 100644 --- a/pennylane/templates/subroutines/grover.py +++ b/pennylane/templates/subroutines/grover.py @@ -113,7 +113,10 @@ def __init__(self, wires=None, work_wires=None, id=None): if (not hasattr(wires, "__len__")) or (len(wires) < 2): raise ValueError("GroverOperator must have at least two wires provided.") - self._hyperparameters = {"n_wires": len(wires), "work_wires": Wires(work_wires)} + self._hyperparameters = { + "n_wires": len(wires), + "work_wires": Wires(work_wires) if work_wires is not None else Wires([]), + } super().__init__(wires=wires, id=id) diff --git a/tests/templates/test_subroutines/test_grover.py b/tests/templates/test_subroutines/test_grover.py index bc2f0dce0fd..987ef2d5daf 100644 --- a/tests/templates/test_subroutines/test_grover.py +++ b/tests/templates/test_subroutines/test_grover.py @@ -62,6 +62,12 @@ def test_work_wires(): assert ops[2].hyperparameters["work_wires"] == work_wire +def test_work_wires_None(): + """Test that work wires of None are not inpreted as work wires.""" + op = qml.GroverOperator(wires=(0, 1, 2, 3), work_wires=None) + assert op.hyperparameters["work_wires"] == qml.wires.Wires([]) + + @pytest.mark.parametrize("bad_wires", [0, (0,), tuple()]) def test_single_wire_error(bad_wires): """Assert error raised when called with only a single wire""" From 2f1ceb30352d99648375fad5a845a9f923336aec Mon Sep 17 00:00:00 2001 From: Romain Moyard Date: Fri, 13 Oct 2023 13:54:08 -0400 Subject: [PATCH 4/8] Transform on devices and update insert (#4667) **Description of the Change:** - Transforms can now be added to the preprocessing of device. `dev = transform(dev)` - Insert is updated to the new transform system **Benefits:** **Possible Drawbacks:** Default mixed is not a new generation device, the main use cases of applying insert on a device is for default mixed (adding noise). - Add a special patch in transform dispatcher in the case of default mixed (to be removed in the future) **Related GitHub Issues:** --- doc/releases/changelog-dev.md | 3 + .../transforms/core/transform_dispatcher.py | 62 +++++++++- pennylane/transforms/insert_ops.py | 56 +++++---- pennylane/transforms/qfunc_transforms.py | 4 +- .../test_transform_dispatcher.py | 77 +++++++++++++ tests/transforms/test_insert_ops.py | 109 ++++++++++++++---- tests/transforms/test_mitigate.py | 4 +- 7 files changed, 267 insertions(+), 48 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index d500a55edca..068d357c9c0 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -60,6 +60,9 @@ to the new transform program system. [(#4573)](https://github.com/PennyLaneAI/pennylane/pull/4573) +* Transforms can be applied on devices following the new device API. + [(#4667)](https://github.com/PennyLaneAI/pennylane/pull/4667) + * All quantum functions transforms are update to the new transform program system. [(#4439)](https://github.com/PennyLaneAI/pennylane/pull/4439) diff --git a/pennylane/transforms/core/transform_dispatcher.py b/pennylane/transforms/core/transform_dispatcher.py index 8e81d499ba3..6b7660a8ad9 100644 --- a/pennylane/transforms/core/transform_dispatcher.py +++ b/pennylane/transforms/core/transform_dispatcher.py @@ -58,7 +58,7 @@ def __init__( self._qnode_transform = self.default_qnode_transform - def __call__(self, *targs, **tkwargs): + def __call__(self, *targs, **tkwargs): # pylint: disable=too-many-return-statements obj = None if targs: @@ -75,6 +75,11 @@ def __call__(self, *targs, **tkwargs): if isinstance(obj, qml.QNode): return self._qnode_transform(obj, targs, tkwargs) + # TODO: Remove with the previous device generation + if isinstance(obj, qml.Device): + return self._old_device_transform(obj, targs, tkwargs) + if isinstance(obj, qml.devices.Device): + return self._device_transform(obj, targs, tkwargs) if callable(obj): return self._qfunc_transform(obj, targs, tkwargs) @@ -229,6 +234,61 @@ def qfunc_transformed(*args, **kwargs): return qfunc_transformed + def _old_device_transform(self, original_device, targs, tkwargs): + """Apply the transform on a device""" + if self._expand_transform: + raise TransformError("Device transform does not support expand transforms.") + if self._is_informative: + raise TransformError("Device transform does not support informative transforms.") + if self._final_transform: + raise TransformError("Device transform does not support final transforms.") + new_dev = copy.deepcopy(original_device) + transform = self._transform + + @new_dev.custom_expand + def new_expand_fn(self, tape, *args, **kwargs): # pylint: disable=unused-variable + tapes, _ = transform(tape, *targs, **tkwargs) + tape = tapes[0] + return self.default_expand_fn(tape, *args, **kwargs) + + return new_dev + + def _device_transform(self, original_device, targs, tkwargs): + """Apply the transform on a device""" + if self._expand_transform: + raise TransformError("Device transform does not support expand transforms.") + if self._is_informative: + raise TransformError("Device transform does not support informative transforms.") + if self._final_transform: + raise TransformError("Device transform does not support final transforms.") + + class TransformedDevice(type(original_device)): + """A transformed device with updated preprocess method.""" + + def __init__(self, original_device, transform): + for key, value in original_device.__dict__.items(): + self.__setattr__(key, value) + self.transform = transform + self._original_device = original_device + + def __repr__(self): + return f"Transformed Device({original_device.__repr__()} with additional preprocess transform {self.transform})" + + def preprocess( + self, config: qml.devices.ExecutionConfig = qml.devices.DefaultExecutionConfig + ): + """This function updates the original device transform program to be applied.""" + program, config = self.original_device.preprocess(config) + program.push_back(TransformContainer(self.transform, targs, tkwargs)) + return program, config + + @property + def original_device(self): + """Return the original device.""" + return self._original_device + + return TransformedDevice(original_device, self._transform) + class TransformContainer: """Class to store a quantum transform with its args, kwargs and classical co-transforms. Use diff --git a/pennylane/transforms/insert_ops.py b/pennylane/transforms/insert_ops.py index 17fbd63acb9..0e8ea6a03eb 100644 --- a/pennylane/transforms/insert_ops.py +++ b/pennylane/transforms/insert_ops.py @@ -14,15 +14,13 @@ """ Provides transforms for inserting operations into quantum circuits. """ -from collections.abc import Sequence from types import FunctionType -from typing import Type, Union +from typing import Type, Union, Callable, Sequence import pennylane as qml -from pennylane import Device, apply from pennylane.operation import Operation from pennylane.tape import QuantumTape -from pennylane.transforms.qfunc_transforms import qfunc_transform +from pennylane.transforms.core import transform from pennylane.ops.op_math import Adjoint # pylint: disable=too-many-branches @@ -51,14 +49,14 @@ def _check_position(position): return not_op, req_ops -@qfunc_transform +@transform def insert( - circuit: Union[callable, QuantumTape, Device], + tape: QuantumTape, op: Union[callable, Type[Operation]], op_args: Union[tuple, float], position: Union[str, list, Type[Operation]] = "all", before: bool = False, -) -> Union[callable, QuantumTape]: +) -> (Sequence[QuantumTape], Callable): """Insert an operation into specified points in an input circuit. Circuits passed through this transform will be updated to have the operation, specified by the @@ -71,8 +69,7 @@ def insert( for more information). Args: - circuit (callable or QuantumTape or pennylane.Device): the input circuit to be transformed, or a - device + circuit (QuantumTape): the input circuit to be transformed. op (callable or Type[Operation]): the single-qubit operation, or sequence of operations acting on a single qubit, to be inserted into the circuit op_args (tuple or float): the arguments fed to the operation, either as a tuple or a single @@ -214,7 +211,7 @@ def f(w, x, y, z): # decompose templates and their adjoints (which fixes a bug in the tutorial_error_mitigation demo) # TODO: change this to be cleaner and more robust try: - circuit = circuit.expand( + tape = tape.expand( stop_at=lambda op: not hasattr(qml.templates, op.name) and not isinstance(op, Adjoint) ) except qml.QuantumFunctionError as e: @@ -235,34 +232,45 @@ def f(w, x, y, z): if not isinstance(op_args, Sequence): op_args = [op_args] - - for prep_op in circuit.operations[: circuit.num_preps]: - apply(prep_op) + new_operations = [] + for prep_op in tape.operations[: tape.num_preps]: + new_operations.append(prep_op) if position == "start": - for w in circuit.wires: - op(*op_args, wires=w) + for w in tape.wires: + sub_tape = qml.tape.make_qscript(op)(*op_args, wires=w) + new_operations.extend(sub_tape.operations) - for circuit_op in circuit.operations[circuit.num_preps :]: + for circuit_op in tape.operations[tape.num_preps :]: if not before: - apply(circuit_op) + new_operations.append(circuit_op) if position == "all": for w in circuit_op.wires: - op(*op_args, wires=w) + sub_tape = qml.tape.make_qscript(op)(*op_args, wires=w) + new_operations.extend(sub_tape.operations) if req_ops: for operation in req_ops: if operation == type(circuit_op): for w in circuit_op.wires: - op(*op_args, wires=w) + sub_tape = qml.tape.make_qscript(op)(*op_args, wires=w) + new_operations.extend(sub_tape.operations) if before: - apply(circuit_op) + new_operations.append(circuit_op) if position == "end": - for w in circuit.wires: - op(*op_args, wires=w) + for w in tape.wires: + sub_tape = qml.tape.make_qscript(op)(*op_args, wires=w) + new_operations.extend(sub_tape.operations) + + new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots) + + def null_postprocessing(results): + """A postprocesing function returned by a transform that only converts the batch of results + into a result for a single ``QuantumTape``. + """ + return results[0] - for m in circuit.measurements: - apply(m) + return [new_tape], null_postprocessing diff --git a/pennylane/transforms/qfunc_transforms.py b/pennylane/transforms/qfunc_transforms.py index fd5b671451c..0717e0215b2 100644 --- a/pennylane/transforms/qfunc_transforms.py +++ b/pennylane/transforms/qfunc_transforms.py @@ -151,7 +151,9 @@ def __call__(self, tape, *args, **kwargs): return qs -def _create_qfunc_internal_wrapper(fn, tape_transform, transform_args, transform_kwargs): +def _create_qfunc_internal_wrapper( + fn, tape_transform, transform_args, transform_kwargs +): # pragma: no cover """Convenience function to create the internal wrapper function generated by the qfunc_transform decorator""" if isinstance(fn, qml.Device): diff --git a/tests/transforms/test_experimental/test_transform_dispatcher.py b/tests/transforms/test_experimental/test_transform_dispatcher.py index bf9d18e6e64..ec3a5afb3fd 100644 --- a/tests/transforms/test_experimental/test_transform_dispatcher.py +++ b/tests/transforms/test_experimental/test_transform_dispatcher.py @@ -530,3 +530,80 @@ def qfunc(): qnode = qml.QNode(transformed_qfunc, qml.device("default.qubit")) result = qnode() assert isinstance(result, type_) + + @pytest.mark.parametrize("valid_transform", valid_transforms) + def test_device_transform(self, valid_transform): + """Test a device transform.""" + dispatched_transform = transform(valid_transform) + new_dev = dispatched_transform(dev, index=0) + + assert new_dev.original_device is dev + assert repr(new_dev).startswith("Transformed Device") + + program, _ = dev.preprocess() + new_program, _ = new_dev.preprocess() + + assert isinstance(program, qml.transforms.core.TransformProgram) + assert isinstance(new_program, qml.transforms.core.TransformProgram) + + assert len(program) == 4 + assert len(new_program) == 5 + + assert new_program[-1].transform is valid_transform + + @pytest.mark.parametrize("valid_transform", valid_transforms) + def test_old_device_transform(self, valid_transform): + """Test a device transform on old device.""" + dispatched_transform = transform(valid_transform) + device = qml.device("default.mixed", wires=2) + new_dev = dispatched_transform(device, index=0) + + assert isinstance(new_dev, type(device)) + assert new_dev.custom_expand_fn + assert repr(device).startswith(" Date: Fri, 13 Oct 2023 15:04:45 -0400 Subject: [PATCH 5/8] Decomposition using GlobalPhase has real phase (#4653) **Context:** When we added the `GlobalPhase` and updated the decompositions for `QubitUnitary` to use it, we left the datatype for the phase as complex, even though the results are real **Description of the Change:** There is one line where the angles, though per definition real, need to have a complex dtype to avoid making PyTorch angry. They are now cast to real _only_ there in that line, rather than carrying that through everywhere else. **Benefits:** Its cleaner --- doc/releases/changelog-dev.md | 6 ++++ .../decompositions/single_qubit_unitary.py | 24 ++++++++-------- tests/transforms/test_decompositions.py | 28 +++++++++---------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 068d357c9c0..cb5f4a98005 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -186,10 +186,15 @@ interface-specific scalar data, eg `[(tf.Variable(1.1), tf.Variable(2.2))]`. [(#4603)](https://github.com/PennyLaneAI/pennylane/pull/4603) +* When decomposing a unitary matrix with `one_qubit_decomposition`, and opting to include the `GlobalPhase` + in the decomposition, the phase is no longer cast to `dtype=complex`. + [(#4653)](https://github.com/PennyLaneAI/pennylane/pull/4653) + * `qml.cut_circuit` is now compatible with circuits that compute the expectation values of Hamiltonians with two or more terms. [(#4642)](https://github.com/PennyLaneAI/pennylane/pull/4642) + * `_qfunc_output` has been removed from `QuantumScript`, as it is no longer necessary. There is still a `_qfunc_output` property on `QNode` instances. [(#4651)](https://github.com/PennyLaneAI/pennylane/pull/4651) @@ -198,6 +203,7 @@ of the computed qubit operator, if imaginary components are smaller than a threshold. [(#4639)](https://github.com/PennyLaneAI/pennylane/pull/4639) +

Breaking changes 💔

* The device test suite now converts device kwargs to integers or floats if they can be converted to integers or floats. diff --git a/pennylane/transforms/decompositions/single_qubit_unitary.py b/pennylane/transforms/decompositions/single_qubit_unitary.py index b19bbc8daa9..62969e36b7a 100644 --- a/pennylane/transforms/decompositions/single_qubit_unitary.py +++ b/pennylane/transforms/decompositions/single_qubit_unitary.py @@ -15,7 +15,7 @@ operations into elementary gates. """ -import numpy +import numpy as np import pennylane as qml from pennylane import math @@ -40,9 +40,9 @@ def _convert_to_su2(U, return_global_phase=False): # Compute the determinants U = qml.math.cast(U, "complex128") dets = math.linalg.det(U) + exp_angles = math.angle(dets) / 2 + U_SU2 = math.cast_like(U, dets) * math.exp(-1j * math.cast_like(exp_angles, 1j))[:, None, None] - exp_angles = math.cast_like(math.angle(dets), 1j) / 2 - U_SU2 = math.cast_like(U, dets) * math.exp(-1j * exp_angles)[:, None, None] return (U_SU2, exp_angles) if return_global_phase else U_SU2 @@ -186,9 +186,9 @@ def _zyz_decomposition(U, wire, return_global_phase=False): phis, thetas, omegas, alphas = map(math.squeeze, [phis, thetas, omegas, alphas]) - phis = phis % (4 * numpy.pi) - thetas = thetas % (4 * numpy.pi) - omegas = omegas % (4 * numpy.pi) + phis = phis % (4 * np.pi) + thetas = thetas % (4 * np.pi) + omegas = omegas % (4 * np.pi) operations = [qml.RZ(phis, wire), qml.RY(thetas, wire), qml.RZ(omegas, wire)] if return_global_phase: @@ -250,9 +250,9 @@ def _xyx_decomposition(U, wire, return_global_phase=False): phis, thetas, lams, gammas = map(math.squeeze, [phis, thetas, lams, gammas]) - phis = phis % (4 * numpy.pi) - thetas = thetas % (4 * numpy.pi) - lams = lams % (4 * numpy.pi) + phis = phis % (4 * np.pi) + thetas = thetas % (4 * np.pi) + lams = lams % (4 * np.pi) operations = [qml.RX(lams, wire), qml.RY(thetas, wire), qml.RX(phis, wire)] if return_global_phase: @@ -316,9 +316,9 @@ def _zxz_decomposition(U, wire, return_global_phase=False): phis, thetas, psis, alphas = map(math.squeeze, [phis, thetas, psis, alphas]) - phis = phis % (4 * numpy.pi) - thetas = thetas % (4 * numpy.pi) - psis = psis % (4 * numpy.pi) + phis = phis % (4 * np.pi) + thetas = thetas % (4 * np.pi) + psis = psis % (4 * np.pi) # Return gates in the order they will be applied on the qubit operations = [qml.RZ(psis, wire), qml.RX(thetas, wire), qml.RZ(phis, wire)] diff --git a/tests/transforms/test_decompositions.py b/tests/transforms/test_decompositions.py index ccaaafd315f..b4bdda9688d 100644 --- a/tests/transforms/test_decompositions.py +++ b/tests/transforms/test_decompositions.py @@ -38,16 +38,16 @@ typeof_gates_zyz = (qml.RZ, qml.RY, qml.RZ, qml.GlobalPhase) single_qubit_decomps_zyz = [ (I, typeof_gates_zyz, [0.0, 0.0, 0.0, 0]), - (Z, typeof_gates_zyz, [np.pi / 2, 0.0, np.pi / 2, (-1.5707963267948966 + 0j)]), + (Z, typeof_gates_zyz, [np.pi / 2, 0.0, np.pi / 2, -1.5707963267948966]), ( S, typeof_gates_zyz, - [np.pi / 4, 0.0, np.pi / 4, (-0.7853981633974483 - 1.6780315470477092e-09j)], + [np.pi / 4, 0.0, np.pi / 4, -0.7853981633974483], ), ( T, typeof_gates_zyz, - [np.pi / 8, 0.0, np.pi / 8, (-0.39269908047469393 - 3.225207101387184e-09j)], + [np.pi / 8, 0.0, np.pi / 8, -0.39269908047469393], ), (qml.RZ(0.3, wires=0).matrix(), typeof_gates_zyz, [0.15, 0.0, 0.15, 0]), ( @@ -70,8 +70,8 @@ typeof_gates_zyz, [12.382273469673908, np.pi, 0.18409714468526372, 0], ), - (H, typeof_gates_zyz, [np.pi, np.pi / 2, 0.0, (-1.5707963267948966 + 0j)]), - (X, typeof_gates_zyz, [np.pi / 2, np.pi, 10.995574287564276, (-1.5707963267948966 + 0j)]), + (H, typeof_gates_zyz, [np.pi, np.pi / 2, 0.0, -1.5707963267948966]), + (X, typeof_gates_zyz, [np.pi / 2, np.pi, 10.995574287564276, -1.5707963267948966]), ( np.exp(1j * 0.02) * qml.Rot(-1.0, 2.0, -3.0, wires=0).matrix(), typeof_gates_zyz, @@ -79,7 +79,7 @@ 11.566370614359172, 2.0, 9.566370614359172, - (-0.020000000000000042 - 2.122325752640375e-17j), + -0.020000000000000042, ], ), # Add two instances of broadcasted unitaries, one coming from RZ and another from Rot @@ -196,14 +196,14 @@ def test_zyz_decomposition_jax(self, U, expected_gates, expected_params): 10.845351366405708, 1.3974974118006183, 0.45246583660683803, - (1.1759220332464762 - 4.163336342344337e-17j), + 1.1759220332464762, ), ), # Try a few specific special unitaries (I, typeof_gates_xyx, [0, 0, 0, 0]), # This triggers the if conditional trivially - (X, typeof_gates_xyx, [4.71238898038469, 0.0, 10.995574287564276, (-1.5707963267948966 + 0j)]), - (Y, typeof_gates_xyx, [1 / 2 * np.pi, np.pi, 1 / 2 * np.pi, (-1.5707963267948966 + 0j)]), - (Z, typeof_gates_xyx, [10.995574287564276, np.pi, 1 / 2 * np.pi, (-1.5707963267948966 + 0j)]), + (X, typeof_gates_xyx, [4.71238898038469, 0.0, 10.995574287564276, -1.5707963267948966]), + (Y, typeof_gates_xyx, [1 / 2 * np.pi, np.pi, 1 / 2 * np.pi, -1.5707963267948966]), + (Z, typeof_gates_xyx, [10.995574287564276, np.pi, 1 / 2 * np.pi, -1.5707963267948966]), # Add two instances of broadcasted unitaries, one coming from RZ and another from Rot ( qml.QubitUnitary(qml.RZ.compute_matrix(np.array([np.pi, np.pi / 2])), wires=0).matrix(), @@ -311,16 +311,16 @@ def test_xyx_decomposition_jax(self, U, expected_gates, expected_params): typeof_gates_zxz = (qml.RZ, qml.RX, qml.RZ, qml.GlobalPhase) single_qubit_decomps_zxz = [ (I, typeof_gates_zxz, [0.0, 0.0, 0.0, 0]), - (Z, typeof_gates_zxz, [np.pi / 2, 0.0, np.pi / 2, (-1.5707963267948966 + 0j)]), + (Z, typeof_gates_zxz, [np.pi / 2, 0.0, np.pi / 2, -1.5707963267948966]), ( S, typeof_gates_zxz, - [np.pi / 4, 0.0, np.pi / 4, (-0.7853981633974483 - 1.6780315470477092e-09j)], + [np.pi / 4, 0.0, np.pi / 4, -0.7853981633974483], ), ( T, typeof_gates_zxz, - [np.pi / 8, 0.0, np.pi / 8, (-0.39269908047469393 - 3.225207101387184e-09j)], + [np.pi / 8, 0.0, np.pi / 8, -0.39269908047469393], ), (qml.RZ(0.3, wires=0).matrix(), typeof_gates_zxz, [0.15, 0.0, 0.15, 0]), ( @@ -484,7 +484,7 @@ def test_zxz_decomposition_jax(self, U, expected_gates, expected_params): 10.845351366405708, 1.3974974118006183, 0.45246583660683803, - 1.1759220332464762 - 4.163336342344337e-17j, + 1.1759220332464762, ), ), ( From e68a56f585fd985401da4569a9b059cbc8022e85 Mon Sep 17 00:00:00 2001 From: Romain Moyard Date: Fri, 13 Oct 2023 17:42:30 -0400 Subject: [PATCH 6/8] Gradients transforms update (#4595) **Description of the Change:** - Update the gradients transforms to the new system **Benefits:** - [x] Parameter shift - [x] Parameter shift CV - [x] Finite diff - [x] Hadamard - [x] SPSA - [x] Pulse gradient - [x] Pulse ode - [x] Metric tensor - [x] Adjoint metric tensor - [x] Quantum Fisher ~Classical fisher~ no tape equivalent ~Hessian param shift~ Separate PR Major changes: - The full program is constructed in the QNode because we need to access the QNode when building classical jacobians. - Update drawer - Two private methods to construct argnums and classical jacobians in the transform program - Metric tensor raises errors instead of warnings - Remove the spy for gradient transforms ------- **Possible Drawbacks:** **Related GitHub Issues:** --------- Co-authored-by: Mudit Pandey --- doc/releases/changelog-dev.md | 3 + pennylane/_grad.py | 1 - pennylane/drawer/draw.py | 5 +- pennylane/gradients/finite_difference.py | 43 +++- pennylane/gradients/gradient_transform.py | 14 +- pennylane/gradients/hadamard_gradient.py | 42 +++- pennylane/gradients/parameter_shift.py | 43 +++- pennylane/gradients/parameter_shift_cv.py | 43 +++- pennylane/gradients/pulse_gradient.py | 31 +-- pennylane/gradients/pulse_gradient_odegen.py | 32 +-- pennylane/gradients/spsa_gradient.py | 44 +++- pennylane/interfaces/autograd.py | 2 +- pennylane/interfaces/execution.py | 37 +-- pennylane/interfaces/jax.py | 2 +- pennylane/interfaces/jax_jit.py | 2 +- pennylane/interfaces/tensorflow.py | 2 +- pennylane/interfaces/tensorflow_autograph.py | 2 +- pennylane/interfaces/torch.py | 2 +- pennylane/optimize/qnspsa.py | 21 +- pennylane/optimize/riemannian_gradient.py | 19 +- pennylane/qinfo/transforms.py | 35 ++- pennylane/qnode.py | 50 +++- pennylane/transforms/adjoint_metric_tensor.py | 231 +++++++----------- pennylane/transforms/core/transform.py | 6 +- .../transforms/core/transform_dispatcher.py | 20 +- .../transforms/core/transform_program.py | 178 +++++++++++++- pennylane/transforms/metric_tensor.py | 214 ++++++---------- pennylane/transforms/qcut/cutcircuit.py | 2 +- pennylane/transforms/specs.py | 2 +- tests/drawer/test_draw.py | 2 +- .../gradients/core/test_gradient_transform.py | 50 +--- .../gradients/core/test_hadamard_gradient.py | 98 +++----- tests/gradients/core/test_pulse_gradient.py | 6 +- .../finite_diff/test_finite_difference.py | 47 ++-- .../test_finite_difference_shot_vec.py | 37 +-- .../finite_diff/test_spsa_gradient.py | 44 ++-- .../test_spsa_gradient_shot_vec.py | 35 +-- .../parameter_shift/test_cv_gradients.py | 27 +- .../parameter_shift/test_parameter_shift.py | 96 +------- .../test_parameter_shift_cv.py | 27 +- .../test_parameter_shift_shot_vec.py | 81 ------ .../test_autograd_default_qubit_2.py | 16 +- .../test_autograd_qnode_default_qubit_2.py | 73 +----- .../test_execute_default_qubit_2.py | 12 +- .../test_jax_default_qubit_2.py | 15 +- .../test_jax_jit_qnode_default_qubit_2.py | 70 +----- .../test_jax_qnode_default_qubit_2.py | 61 +---- .../test_tensorflow_default_qubit_2.py | 15 +- .../test_tensorflow_qnode_default_qubit_2.py | 72 +----- .../test_torch_default_qubit_2.py | 15 +- .../test_torch_qnode_default_qubit_2.py | 67 +---- tests/interfaces/test_autograd_qnode.py | 66 +---- tests/interfaces/test_jacobian_products.py | 2 +- tests/interfaces/test_jax_jit_qnode.py | 73 +----- tests/interfaces/test_jax_qnode.py | 67 +---- tests/interfaces/test_tensorflow_qnode.py | 70 +----- tests/interfaces/test_torch_qnode.py | 67 +---- .../test_transform_program_integration.py | 30 +++ tests/logging/test_logging_autograd.py | 2 +- tests/qinfo/test_fisher.py | 2 +- tests/qnn/test_keras.py | 1 + tests/qnn/test_qnn_torch.py | 1 + tests/tape/test_qscript.py | 15 +- tests/tape/test_tape.py | 11 +- .../test_approx_time_evolution.py | 21 +- tests/test_qnode.py | 31 --- tests/test_qnode_legacy.py | 31 --- tests/test_return_types_dq2.py | 18 +- tests/test_vqe.py | 19 -- .../transforms/test_adjoint_metric_tensor.py | 113 +++------ tests/transforms/test_batch_transform.py | 6 +- .../test_transform_dispatcher.py | 18 +- .../test_transform_program.py | 17 +- tests/transforms/test_metric_tensor.py | 109 ++------- tests/transforms/test_specs.py | 17 +- 75 files changed, 1160 insertions(+), 1741 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index cb5f4a98005..d327f2829b3 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -63,6 +63,9 @@ * Transforms can be applied on devices following the new device API. [(#4667)](https://github.com/PennyLaneAI/pennylane/pull/4667) +* All gradient transforms are updated to the new transform program system. + [(#4595)](https://github.com/PennyLaneAI/pennylane/pull/4595) + * All quantum functions transforms are update to the new transform program system. [(#4439)](https://github.com/PennyLaneAI/pennylane/pull/4439) diff --git a/pennylane/_grad.py b/pennylane/_grad.py index cd93d8997d5..6769548d66a 100644 --- a/pennylane/_grad.py +++ b/pennylane/_grad.py @@ -327,7 +327,6 @@ def _jacobian_function(*args, **kwargs): "If this is unintended, please add trainable parameters via the " "'requires_grad' attribute or 'argnum' keyword." ) - jac = tuple(_jacobian(func, arg)(*args, **kwargs) for arg in _argnum) return jac[0] if unpack else jac diff --git a/pennylane/drawer/draw.py b/pennylane/drawer/draw.py index 812ced953d4..ce3939e2839 100644 --- a/pennylane/drawer/draw.py +++ b/pennylane/drawer/draw.py @@ -260,10 +260,13 @@ def wrapper(*args, **kwargs): _wire_order = wire_order or qnode.tape.wires else: original_expansion_strategy = getattr(qnode, "expansion_strategy", None) - try: qnode.expansion_strategy = expansion_strategy or original_expansion_strategy tapes = qnode.construct(args, kwargs) + if isinstance(qnode.device, qml.devices.Device): + program = qnode.transform_program + tapes = program([qnode.tape]) + finally: qnode.expansion_strategy = original_expansion_strategy diff --git a/pennylane/gradients/finite_difference.py b/pennylane/gradients/finite_difference.py index fa86886a4ce..d925c57853a 100644 --- a/pennylane/gradients/finite_difference.py +++ b/pennylane/gradients/finite_difference.py @@ -15,8 +15,10 @@ This module contains functions for computing the finite-difference gradient of a quantum tape. """ -# pylint: disable=protected-access,too-many-arguments,too-many-branches,too-many-statements +# pylint: disable=protected-access,too-many-arguments,too-many-branches,too-many-statements,unused-argument +from typing import Sequence, Callable import functools +from functools import partial from warnings import warn import numpy as np @@ -24,6 +26,10 @@ import pennylane as qml from pennylane.measurements import ProbabilityMP +from pennylane.transforms.core import transform +from pennylane.transforms.tape_expand import expand_invalid_trainable +from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac + from .general_shift_rules import generate_shifted_tapes from .gradient_transform import ( @@ -31,7 +37,6 @@ assert_no_tape_batching, choose_grad_methods, gradient_analysis_and_validation, - gradient_transform, _no_trainable_grad, ) @@ -167,9 +172,36 @@ def _processing_fn(results, shots, single_shot_batch_fn): return tuple(grads_tuple) -@gradient_transform +def _expand_transform_finite_diff( + tape: qml.tape.QuantumTape, + argnum=None, + h=1e-7, + approx_order=1, + n=1, + strategy="forward", + f0=None, + validate_params=True, +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Expand function to be applied before finite difference.""" + expanded_tape = expand_invalid_trainable(tape) + + def null_postprocessing(results): + """A postprocesing function returned by a transform that only converts the batch of results + into a result for a single ``QuantumTape``. + """ + return results[0] + + return [expanded_tape], null_postprocessing + + +@partial( + transform, + expand_transform=_expand_transform_finite_diff, + classical_cotransform=_contract_qjac_with_cjac, + final_transform=True, +) def finite_diff( - tape, + tape: qml.tape.QuantumTape, argnum=None, h=1e-7, approx_order=1, @@ -177,7 +209,7 @@ def finite_diff( strategy="forward", f0=None, validate_params=True, -): +) -> (Sequence[qml.tape.QuantumTape], Callable): r"""Transform a QNode to compute the finite-difference gradient of all gate parameters with respect to its inputs. Args: @@ -318,6 +350,7 @@ def finite_diff( The outermost tuple contains results corresponding to each element of the shot vector. """ + transform_name = "finite difference" assert_no_tape_batching(tape, transform_name) diff --git a/pennylane/gradients/gradient_transform.py b/pennylane/gradients/gradient_transform.py index 2a128f92a16..218a9226087 100644 --- a/pennylane/gradients/gradient_transform.py +++ b/pennylane/gradients/gradient_transform.py @@ -391,13 +391,19 @@ def reorder_grads(grads, tape_specs): # pylint: disable=too-many-return-statements,too-many-branches -def _contract_qjac_with_cjac(qjac, cjac, num_measurements, has_partitioned_shots): +def _contract_qjac_with_cjac(qjac, cjac, tape): """Contract a quantum Jacobian with a classical preprocessing Jacobian. Essentially, this function computes the generalized version of ``tensordot(qjac, cjac)`` over the tape parameter axis, adapted to the new return type system. This function takes the measurement shapes and different QNode arguments into account. """ + num_measurements = len(tape.measurements) + has_partitioned_shots = tape.shots.has_partitioned_shots + + if isinstance(qjac, tuple) and len(qjac) == 1: + qjac = qjac[0] + if isinstance(cjac, tuple) and len(cjac) == 1: cjac = cjac[0] @@ -453,7 +459,7 @@ def _reshape(x): return tuple(tuple(tdot(qml.math.stack(q), c) for c in cjac if c is not None) for q in qjac) -class gradient_transform(qml.batch_transform): +class gradient_transform(qml.batch_transform): # pragma: no cover """Decorator for defining quantum gradient transforms. Quantum gradient transforms are a specific case of :class:`~.batch_transform`. @@ -601,8 +607,6 @@ def jacobian_wrapper( qnode, argnum=argnum_cjac, expand_fn=self.expand_fn )(*args, **kwargs) - num_measurements = len(qnode.tape.measurements) - has_partitioned_shots = qnode.tape.shots.has_partitioned_shots - return _contract_qjac_with_cjac(qjac, cjac, num_measurements, has_partitioned_shots) + return _contract_qjac_with_cjac(qjac, cjac, qnode.tape) # pragma: no cover return jacobian_wrapper diff --git a/pennylane/gradients/hadamard_gradient.py b/pennylane/gradients/hadamard_gradient.py index e37df1a393d..dbc99e35bb6 100644 --- a/pennylane/gradients/hadamard_gradient.py +++ b/pennylane/gradients/hadamard_gradient.py @@ -15,9 +15,14 @@ This module contains functions for computing the Hadamard-test gradient of a qubit-based quantum tape. """ +# pylint: disable=unused-argument +from typing import Sequence, Callable +from functools import partial import pennylane as qml import pennylane.numpy as np from pennylane.transforms.metric_tensor import _get_aux_wire +from pennylane.transforms.core import transform +from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac from pennylane.transforms.tape_expand import expand_invalid_trainable_hadamard_gradient from .gradient_transform import ( @@ -27,17 +32,40 @@ assert_no_variance, choose_grad_methods, gradient_analysis_and_validation, - gradient_transform, _no_trainable_grad, ) -def _hadamard_grad( - tape, +def _expand_transform_hadamard( + tape: qml.tape.QuantumTape, argnum=None, aux_wire=None, device_wires=None, -): +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Expand function to be applied before hadamard gradient.""" + expanded_tape = expand_invalid_trainable_hadamard_gradient(tape) + + def null_postprocessing(results): + """A postprocesing function returned by a transform that only converts the batch of results + into a result for a single ``QuantumTape``. + """ + return results[0] + + return [expanded_tape], null_postprocessing + + +@partial( + transform, + expand_transform=_expand_transform_hadamard, + classical_cotransform=_contract_qjac_with_cjac, + final_transform=True, +) +def hadamard_grad( + tape: qml.tape.QuantumTape, + argnum=None, + aux_wire=None, + device_wires=None, +) -> (Sequence[qml.tape.QuantumTape], Callable): r"""Transform a QNode to compute the Hadamard test gradient of all gates with respect to their inputs. Args: @@ -174,6 +202,7 @@ def _hadamard_grad( The number of trainable parameters may increase due to the decomposition. """ + transform_name = "Hadamard test" assert_no_state_returns(tape.measurements, transform_name) assert_no_variance(tape.measurements, transform_name) @@ -421,8 +450,3 @@ def _get_generators(trainable_op): coeffs = trainable_op.generator().coeffs return coeffs, generators - - -hadamard_grad = gradient_transform( - _hadamard_grad, expand_fn=expand_invalid_trainable_hadamard_gradient -) diff --git a/pennylane/gradients/parameter_shift.py b/pennylane/gradients/parameter_shift.py index 03dd00c3643..d7877bb890a 100644 --- a/pennylane/gradients/parameter_shift.py +++ b/pennylane/gradients/parameter_shift.py @@ -15,11 +15,17 @@ This module contains functions for computing the parameter-shift gradient of a qubit-based quantum tape. """ -# pylint: disable=protected-access,too-many-arguments,too-many-statements +# pylint: disable=protected-access,too-many-arguments,too-many-statements,unused-argument +from typing import Sequence, Callable +from functools import partial + import numpy as np import pennylane as qml from pennylane.measurements import VarianceMP +from pennylane.transforms.core import transform +from pennylane.transforms.tape_expand import expand_invalid_trainable +from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac from .finite_difference import finite_diff from .general_shift_rules import ( @@ -35,7 +41,6 @@ assert_multimeasure_not_broadcasted, choose_grad_methods, gradient_analysis_and_validation, - gradient_transform, _no_trainable_grad, reorder_grads, ) @@ -155,7 +160,6 @@ def _single_meas_grad(result, coeffs, unshifted_coeff, r0): ) # pragma: no cover # return the unshifted term, which is the only contribution return qml.math.array(unshifted_coeff * r0) - result = qml.math.stack(result) coeffs = qml.math.convert_like(coeffs, result) g = qml.math.tensordot(result, coeffs, [[0], [0]]) @@ -719,16 +723,42 @@ def var_param_shift(tape, argnum, shifts=None, gradient_recipes=None, f0=None, b return gradient_tapes, processing_fn -@gradient_transform +def _expand_transform_param_shift( + tape: qml.tape.QuantumTape, + argnum=None, + shifts=None, + gradient_recipes=None, + fallback_fn=finite_diff, + f0=None, + broadcast=False, +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Expand function to be applied before parameter shift.""" + expanded_tape = expand_invalid_trainable(tape) + + def null_postprocessing(results): + """A postprocesing function returned by a transform that only converts the batch of results + into a result for a single ``QuantumTape``. + """ + return results[0] + + return [expanded_tape], null_postprocessing + + +@partial( + transform, + expand_transform=_expand_transform_param_shift, + classical_cotransform=_contract_qjac_with_cjac, + final_transform=True, +) def param_shift( - tape, + tape: qml.tape.QuantumTape, argnum=None, shifts=None, gradient_recipes=None, fallback_fn=finite_diff, f0=None, broadcast=False, -): +) -> (Sequence[qml.tape.QuantumTape], Callable): r"""Transform a QNode to compute the parameter-shift gradient of all gate parameters with respect to its inputs. @@ -1004,6 +1034,7 @@ def param_shift( Note that ``broadcast=True`` requires additional memory by a factor of the largest batch_size of the created tapes. """ + transform_name = "parameter-shift rule" assert_no_state_returns(tape.measurements, transform_name) assert_multimeasure_not_broadcasted(tape.measurements, broadcast) diff --git a/pennylane/gradients/parameter_shift_cv.py b/pennylane/gradients/parameter_shift_cv.py index 7aeec09ad4b..06b3282094d 100644 --- a/pennylane/gradients/parameter_shift_cv.py +++ b/pennylane/gradients/parameter_shift_cv.py @@ -15,21 +15,25 @@ This module contains functions for computing the parameter-shift gradient of a CV-based quantum tape. """ -# pylint: disable=protected-access,too-many-arguments,too-many-statements,too-many-branches +# pylint: disable=protected-access,too-many-arguments,too-many-statements,too-many-branches,unused-argument +from typing import Sequence, Callable import itertools +from functools import partial import warnings import numpy as np import pennylane as qml from pennylane.measurements import ExpectationMP, ProbabilityMP, StateMP, VarianceMP +from pennylane.transforms.core import transform +from pennylane.transforms.tape_expand import expand_invalid_trainable +from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac from .finite_difference import finite_diff from .general_shift_rules import generate_shifted_tapes, process_shifts from .gradient_transform import ( choose_grad_methods, _grad_method_validation, - gradient_transform, _no_trainable_grad, ) from .parameter_shift import _get_operation_recipe, expval_param_shift @@ -478,11 +482,36 @@ def processing_fn(results): return gradient_tapes, processing_fn -# TODO: integration of CV devices with new return types -# pylint: disable=unused-argument -@gradient_transform +def _expand_transform_param_shift_cv( + tape: qml.tape.QuantumTape, + dev, + argnum=None, + shifts=None, + gradient_recipes=None, + fallback_fn=finite_diff, + f0=None, + force_order2=False, +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Expand function to be applied before parameter shift CV.""" + expanded_tape = expand_invalid_trainable(tape) + + def null_postprocessing(results): + """A postprocesing function returned by a transform that only converts the batch of results + into a result for a single ``QuantumTape``. + """ + return results[0] + + return [expanded_tape], null_postprocessing + + +@partial( + transform, + expand_transform=_expand_transform_param_shift_cv, + classical_cotransform=_contract_qjac_with_cjac, + final_transform=True, +) def param_shift_cv( - tape, + tape: qml.tape.QuantumTape, dev, argnum=None, shifts=None, @@ -490,7 +519,7 @@ def param_shift_cv( fallback_fn=finite_diff, f0=None, force_order2=False, -): +) -> (Sequence[qml.tape.QuantumTape], Callable): r"""Transform a continuous-variable QNode to compute the parameter-shift gradient of all gate parameters with respect to its inputs. diff --git a/pennylane/gradients/pulse_gradient.py b/pennylane/gradients/pulse_gradient.py index 47cc189fd32..2693d10b675 100644 --- a/pennylane/gradients/pulse_gradient.py +++ b/pennylane/gradients/pulse_gradient.py @@ -15,11 +15,14 @@ This module contains functions for computing the stochastic parameter-shift gradient of pulse sequences in a qubit-based quantum tape. """ +from typing import Sequence, Callable +from functools import partial import warnings import numpy as np import pennylane as qml from pennylane.pulse import ParametrizedEvolution, HardwareHamiltonian +from pennylane.transforms.core import transform from .parameter_shift import _make_zero_rep from .general_shift_rules import eigvals_to_frequencies, generate_shift_rule @@ -30,7 +33,6 @@ assert_no_variance, choose_grad_methods, gradient_analysis_and_validation, - gradient_transform, _no_trainable_grad, reorder_grads, ) @@ -281,9 +283,14 @@ def _psr_and_contract(res_list, cjacs, int_prefactor): # pylint: disable=too-many-arguments -def _stoch_pulse_grad( - tape, argnum=None, num_split_times=1, sampler_seed=None, use_broadcasting=False -): +@partial(transform, final_transform=True) +def stoch_pulse_grad( + tape: qml.tape.QuantumTape, + argnum=None, + num_split_times=1, + sampler_seed=None, + use_broadcasting=False, +) -> (Sequence[qml.tape.QuantumTape], Callable): r"""Compute the gradient of a quantum circuit composed of pulse sequences by applying the stochastic parameter shift rule. @@ -854,21 +861,7 @@ def processing_fn(results): return tapes, processing_fn -def expand_invalid_trainable_stoch_pulse_grad(x, *args, **kwargs): - r"""Do not expand any operation. We expect the ``stoch_pulse_grad`` to be used - on pulse programs and we do not expect decomposition pipelines between pulses - and gate-based circuits yet. - """ - # pylint:disable=unused-argument - return x - - -stoch_pulse_grad = gradient_transform( - _stoch_pulse_grad, expand_fn=expand_invalid_trainable_stoch_pulse_grad -) - - -@stoch_pulse_grad.custom_qnode_wrapper +@stoch_pulse_grad.custom_qnode_transform def stoch_pulse_grad_qnode_wrapper(self, qnode, targs, tkwargs): """A custom QNode wrapper for the gradient transform :func:`~.stoch_pulse_grad`. It raises an error, so that applying ``stoch_pulse_grad`` to a ``QNode`` directly diff --git a/pennylane/gradients/pulse_gradient_odegen.py b/pennylane/gradients/pulse_gradient_odegen.py index 927ec620d39..39dc815f566 100644 --- a/pennylane/gradients/pulse_gradient_odegen.py +++ b/pennylane/gradients/pulse_gradient_odegen.py @@ -15,6 +15,7 @@ This module contains functions for computing the pulse generator parameter-shift gradient of pulse sequences in a qubit-based quantum tape. """ +from typing import Callable, Sequence import warnings from functools import partial import numpy as np @@ -23,6 +24,7 @@ from pennylane.pulse import ParametrizedEvolution from pennylane.ops.qubit.special_unitary import pauli_basis_strings, _pauli_decompose +from pennylane.transforms.core import transform from .parameter_shift import _make_zero_rep from .pulse_gradient import _assert_has_jax, raise_pulse_diff_on_qnode @@ -33,7 +35,6 @@ assert_no_variance, choose_grad_methods, gradient_analysis_and_validation, - gradient_transform, _no_trainable_grad, reorder_grads, ) @@ -398,7 +399,10 @@ def processing_fn(results): return gradient_tapes, processing_fn -def _pulse_odegen(tape, argnum=None, atol=1e-7): +@partial(transform, final_transform=True) +def pulse_odegen( + tape: qml.tape.QuantumTape, argnum=None, atol=1e-7 +) -> (Sequence[qml.tape.QuantumTape], Callable): r"""Transform a QNode to compute the pulse generator parameter-shift gradient of pulses in a pulse program with respect to their inputs. This method combines automatic differentiation of few-qubit operations with @@ -699,31 +703,19 @@ def circuit(params): return _expval_pulse_odegen(tape, argnum, atol) -def expand_invalid_trainable_pulse_odegen(x, *args, **kwargs): - r"""Do not expand any operation. We expect the ``pulse_odegen`` to be used - on pulse programs and we do not expect decomposition pipelines between pulses - and gate-based circuits yet. - """ - # pylint:disable=unused-argument - return x - - -pulse_odegen = gradient_transform(_pulse_odegen, expand_fn=expand_invalid_trainable_pulse_odegen) - - -def _legacy_pulse_generator_wrapper(*args, **kwargs): +def _legacy_pulse_generator_wrapper( + tape: qml.tape.QuantumTape, argnum=None, atol=1e-7 +) -> (Sequence[qml.tape.QuantumTape], Callable): warnings.warn( "pulse_generator for gradient computation has been renamed to pulse_odegen and will not be available in pennylane v0.34 onwards" ) - return _pulse_odegen(*args, **kwargs) + return pulse_odegen(tape, argnum, atol) -pulse_generator = gradient_transform( - _legacy_pulse_generator_wrapper, expand_fn=expand_invalid_trainable_pulse_odegen -) +pulse_generator = transform(_legacy_pulse_generator_wrapper, final_transform=True) -@pulse_odegen.custom_qnode_wrapper +@pulse_odegen.custom_qnode_transform def pulse_odegen_qnode_wrapper(self, qnode, targs, tkwargs): """A custom QNode wrapper for the gradient transform :func:`~.pulse_odegen`. It raises an error, so that applying ``pulse_odegen`` to a ``QNode`` directly diff --git a/pennylane/gradients/spsa_gradient.py b/pennylane/gradients/spsa_gradient.py index 70239178d1c..f49454fe4b7 100644 --- a/pennylane/gradients/spsa_gradient.py +++ b/pennylane/gradients/spsa_gradient.py @@ -15,18 +15,21 @@ This module contains functions for computing the SPSA gradient of a quantum tape. """ -# pylint: disable=protected-access,too-many-arguments,too-many-branches,too-many-statements +# pylint: disable=protected-access,too-many-arguments,too-many-branches,too-many-statements,unused-argument +from typing import Sequence, Callable from functools import partial import numpy as np import pennylane as qml +from pennylane.transforms.core import transform +from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac +from pennylane.transforms.tape_expand import expand_invalid_trainable from .finite_difference import _processing_fn, finite_diff_coeffs from .gradient_transform import ( _all_zero_grad, assert_no_tape_batching, - gradient_transform, choose_grad_methods, gradient_analysis_and_validation, _no_trainable_grad, @@ -56,9 +59,39 @@ def _rademacher_sampler(indices, num_params, *args, rng): return direction -@gradient_transform +def _expand_transform_spsa( + tape: qml.tape.QuantumTape, + argnum=None, + h=1e-5, + approx_order=2, + n=1, + strategy="center", + f0=None, + validate_params=True, + num_directions=1, + sampler=_rademacher_sampler, + sampler_rng=None, +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Expand function to be applied before spsa gradient.""" + expanded_tape = expand_invalid_trainable(tape) + + def null_postprocessing(results): + """A postprocesing function returned by a transform that only converts the batch of results + into a result for a single ``QuantumTape``. + """ + return results[0] + + return [expanded_tape], null_postprocessing + + +@partial( + transform, + expand_transform=_expand_transform_spsa, + classical_cotransform=_contract_qjac_with_cjac, + final_transform=True, +) def spsa_grad( - tape, + tape: qml.tape.QuantumTape, argnum=None, h=1e-5, approx_order=2, @@ -69,7 +102,7 @@ def spsa_grad( num_directions=1, sampler=_rademacher_sampler, sampler_rng=None, -): +) -> (Sequence[qml.tape.QuantumTape], Callable): r"""Transform a QNode to compute the SPSA gradient of all gate parameters with respect to its inputs. This estimator shifts all parameters simultaneously and approximates the gradient based on these shifts and a @@ -250,6 +283,7 @@ def spsa_grad( Note that the stochastic approximation and the fluctuations from the shot noise of the device accumulate, leading to a very coarse-grained estimate for the gradient. """ + transform_name = "SPSA" assert_no_tape_batching(tape, transform_name) diff --git a/pennylane/interfaces/autograd.py b/pennylane/interfaces/autograd.py index 62d7b384221..6ca0e2a7d76 100644 --- a/pennylane/interfaces/autograd.py +++ b/pennylane/interfaces/autograd.py @@ -241,7 +241,7 @@ def grad_fn(dy): else: # Need to compute the Jacobians on the backward pass (accumulation="backward") - if isinstance(gradient_fn, qml.gradients.gradient_transform): + if isinstance(gradient_fn, qml.transforms.core.TransformDispatcher): # Gradient function is a gradient transform. # Generate and execute the required gradient tapes diff --git a/pennylane/interfaces/execution.py b/pennylane/interfaces/execution.py index a5d07e979ae..202ed42488a 100644 --- a/pennylane/interfaces/execution.py +++ b/pennylane/interfaces/execution.py @@ -399,6 +399,7 @@ def execute( gradient_fn: Optional[Union[Callable, str]] = None, interface="auto", transform_program=None, + config=None, grad_on_execution="best", gradient_kwargs=None, cache: Union[bool, dict, Cache] = True, @@ -423,6 +424,8 @@ def execute( interface (str): The interface that will be used for classical autodifferentiation. This affects the types of parameters that can exist on the input tapes. Available options include ``autograd``, ``torch``, ``tf``, ``jax`` and ``auto``. + transform_program(qml.transforms.core.TransformProgram): A transform program to be applied to the initial tape. + config (qml.devices.ExecutionConfig): A datastructure describing the parameters needed to fully describe the execution. grad_on_execution (bool, str): Whether the gradients should be computed on the execution or not. Only applies if the device is queried for the gradient; gradient transform functions available in ``qml.gradients`` are only supported on the backward @@ -552,18 +555,8 @@ def cost_fn(params, x): interface = get_jax_interface_name(tapes) - if gradient_fn is None: - _gradient_method = None - elif isinstance(gradient_fn, str): - _gradient_method = gradient_fn - else: - _gradient_method = "gradient-transform" - config = qml.devices.ExecutionConfig( - interface=interface, - gradient_method=_gradient_method, - grad_on_execution=None if grad_on_execution == "best" else grad_on_execution, - ) gradient_kwargs = gradient_kwargs or {} + config = config or _get_execution_config(gradient_fn, grad_on_execution, interface, device) if isinstance(cache, bool) and cache: # cache=True: create a LRUCache object @@ -604,9 +597,7 @@ def inner_execute_with_empty_jac(tapes, **_): "device batch transforms cannot be turned off with the new device interface.", UserWarning, ) - device_transform_program, config = device.preprocess(config) - full_transform_program = transform_program + device_transform_program - tapes, post_processing = full_transform_program(tapes) + tapes, post_processing = transform_program(tapes) else: # TODO: Remove once old device are removed tapes, program_post_processing = transform_program(tapes) @@ -747,3 +738,21 @@ def device_gradient_fn(inner_tapes, **gradient_kwargs): ) return post_processing(results) + + +def _get_execution_config(gradient_fn, grad_on_execution, interface, device): + """Helper function to get the execution config.""" + if gradient_fn is None: + _gradient_method = None + elif isinstance(gradient_fn, str): + _gradient_method = gradient_fn + else: + _gradient_method = "gradient-transform" + config = qml.devices.ExecutionConfig( + interface=interface, + gradient_method=_gradient_method, + grad_on_execution=None if grad_on_execution == "best" else grad_on_execution, + ) + if isinstance(device, qml.devices.Device): + _, config = device.preprocess(config) + return config diff --git a/pennylane/interfaces/jax.py b/pennylane/interfaces/jax.py index 632be8a51a7..4e1325063cf 100644 --- a/pennylane/interfaces/jax.py +++ b/pennylane/interfaces/jax.py @@ -176,7 +176,7 @@ def execute_wrapper(params): @execute_wrapper.defjvp def execute_wrapper_jvp(primals, tangents): """Primals[0] are parameters as Jax tracers and tangents[0] is a list of tangent vectors as Jax tracers.""" - if isinstance(gradient_fn, qml.gradients.gradient_transform): + if isinstance(gradient_fn, qml.transforms.core.TransformDispatcher): at_max_diff = _n == max_diff new_tapes = set_parameters_on_copy_and_unwrap(tapes, primals[0], unwrap=False) _args = ( diff --git a/pennylane/interfaces/jax_jit.py b/pennylane/interfaces/jax_jit.py index 902128e787a..1b182a993f7 100644 --- a/pennylane/interfaces/jax_jit.py +++ b/pennylane/interfaces/jax_jit.py @@ -340,7 +340,7 @@ def execute_wrapper_jvp(primals, tangents): idx for idx, t in enumerate(tangent) if not isinstance(t, Zero) ) - if not isinstance(gradient_fn, qml.gradients.gradient_transform): + if not isinstance(gradient_fn, qml.transforms.core.TransformDispatcher): jacobians_func = _device_method_jac_via_callback elif _n == max_diff: jacobians_func = _grad_transform_jac_via_callback diff --git a/pennylane/interfaces/tensorflow.py b/pennylane/interfaces/tensorflow.py index 8947d84084b..a8e2b18388f 100644 --- a/pennylane/interfaces/tensorflow.py +++ b/pennylane/interfaces/tensorflow.py @@ -243,7 +243,7 @@ def grad_fn(*dy, **tfkwargs): else: # Need to compute the Jacobians on the backward pass (accumulation="backward") - if isinstance(gradient_fn, qml.gradients.gradient_transform): + if isinstance(gradient_fn, qml.transforms.core.TransformDispatcher): # Gradient function is a gradient transform. # Generate and execute the required gradient tapes diff --git a/pennylane/interfaces/tensorflow_autograph.py b/pennylane/interfaces/tensorflow_autograph.py index 0be8b64f33c..8ad073b0d55 100644 --- a/pennylane/interfaces/tensorflow_autograph.py +++ b/pennylane/interfaces/tensorflow_autograph.py @@ -197,7 +197,7 @@ def _backward(*args): else: # Need to compute the Jacobians on the backward pass (accumulation="backward") - if isinstance(gradient_fn, qml.gradients.gradient_transform): + if isinstance(gradient_fn, qml.transforms.core.TransformDispatcher): # Gradient function is a gradient transform. # Generate and execute the required gradient tapes diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py index 0ba68c71de1..2a4172078b7 100644 --- a/pennylane/interfaces/torch.py +++ b/pennylane/interfaces/torch.py @@ -183,7 +183,7 @@ def backward(ctx, *dy): else: # Need to compute the Jacobians on the backward pass (accumulation="backward") - if isinstance(ctx.gradient_fn, qml.gradients.gradient_transform): + if isinstance(ctx.gradient_fn, qml.transforms.core.TransformDispatcher): # Gradient function is a gradient transform. # Generate and execute the required gradient tapes diff --git a/pennylane/optimize/qnspsa.py b/pennylane/optimize/qnspsa.py index 29a379d1e54..e1c08453172 100644 --- a/pennylane/optimize/qnspsa.py +++ b/pennylane/optimize/qnspsa.py @@ -218,7 +218,20 @@ def _step_core(self, cost, args, kwargs): all_grad_dirs.append(grad_dirs) all_tensor_dirs.append(tensor_dirs) - raw_results = qml.execute(all_grad_tapes + all_metric_tapes, cost.device, None) + if isinstance(cost.device, qml.devices.Device): + program, config = cost.device.preprocess() + + raw_results = qml.execute( + all_grad_tapes + all_metric_tapes, + cost.device, + None, + transform_program=program, + config=config, + ) + else: + raw_results = qml.execute( + all_grad_tapes + all_metric_tapes, cost.device, None + ) # pragma: no cover grads = [ self._post_process_grad(raw_results[2 * i : 2 * i + 2], all_grad_dirs[i]) for i in range(self.resamplings) @@ -425,8 +438,10 @@ def _apply_blocking(self, cost, args, kwargs, params_next): cost.construct(params_next, kwargs) tape_loss_next = cost.tape.copy(copy_operations=True) - - loss_curr, loss_next = qml.execute([tape_loss_curr, tape_loss_next], cost.device, None) + program, _ = cost.device.preprocess() + loss_curr, loss_next = qml.execute( + [tape_loss_curr, tape_loss_next], cost.device, None, transform_program=program + ) # self.k has been updated earlier ind = (self.k - 2) % self.last_n_steps.size diff --git a/pennylane/optimize/riemannian_gradient.py b/pennylane/optimize/riemannian_gradient.py index ac9371769c1..4f69c8fdca7 100644 --- a/pennylane/optimize/riemannian_gradient.py +++ b/pennylane/optimize/riemannian_gradient.py @@ -390,7 +390,24 @@ def get_omegas(self): self.lie_algebra_basis_names, self.nqubits, ) - circuits = qml.execute(circuits, self.circuit.device, gradient_fn=None) + + if isinstance(self.circuit.device, qml.devices.Device): + program, config = self.circuit.device.preprocess() + + circuits = qml.execute( + circuits, + self.circuit.device, + transform_program=program, + config=config, + gradient_fn=None, + ) + else: + circuits = qml.execute( + circuits, self.circuit.device, gradient_fn=None + ) # pragma: no cover + + program, _ = self.circuit.device.preprocess() + circuits_plus = np.array(circuits[: len(circuits) // 2]).reshape( len(self.coeffs), len(self.lie_algebra_basis_names) ) diff --git a/pennylane/qinfo/transforms.py b/pennylane/qinfo/transforms.py index 1ab0c0a8c6a..3e6f83c0bbd 100644 --- a/pennylane/qinfo/transforms.py +++ b/pennylane/qinfo/transforms.py @@ -646,7 +646,10 @@ def wrapper(*args, **kwargs): return wrapper -def quantum_fisher(qnode, *args, **kwargs): +@partial(transform, is_informative=True) +def quantum_fisher( + tape: qml.tape.QuantumTape, device, *args, **kwargs +) -> (Sequence[qml.tape.QuantumTape], Callable): r"""Returns a function that computes the quantum fisher information matrix (QFIM) of a given :class:`.QNode`. Given a parametrized quantum state :math:`|\psi(\bm{\theta})\rangle`, the quantum fisher information matrix (QFIM) quantifies how changes to the parameters :math:`\bm{\theta}` @@ -731,17 +734,33 @@ def circ(params): """ - if qnode.device.shots and isinstance(qnode.device, (DefaultQubitLegacy, DefaultQubit)): + if device.shots and isinstance(device, (DefaultQubitLegacy, DefaultQubit)): + tapes, processing_fn = metric_tensor(tape, *args, **kwargs) - def wrapper(*args0, **kwargs0): - return 4 * metric_tensor(qnode, *args, **kwargs)(*args0, **kwargs0) + def processing_fn_multiply(res): + res = qml.execute(res, device=device) + return 4 * processing_fn(res) - else: + return tapes, processing_fn_multiply - def wrapper(*args0, **kwargs0): - return 4 * adjoint_metric_tensor(qnode, *args, **kwargs)(*args0, **kwargs0) + res = adjoint_metric_tensor(tape, *args, **kwargs) - return wrapper + def processing_fn_multiply(r): # pylint: disable=function-redefined + r = qml.math.stack(r) + return 4 * r + + return res, processing_fn_multiply + + +@quantum_fisher.custom_qnode_transform +def qnode_execution_wrapper(self, qnode, targs, tkwargs): + """Here, we overwrite the QNode execution wrapper in order + to take into account that classical processing may be present + inside the QNode.""" + + tkwargs["device"] = qnode.device + + return self.default_qnode_transform(qnode, targs, tkwargs) def fidelity(qnode0, qnode1, wires0, wires1): diff --git a/pennylane/qnode.py b/pennylane/qnode.py index 3b79f2cce01..3cac738e5ee 100644 --- a/pennylane/qnode.py +++ b/pennylane/qnode.py @@ -615,7 +615,7 @@ def get_gradient_fn(device, interface, diff_method="best", shots=None): "'device', 'adjoint', 'spsa', 'hadamard')." ) - if isinstance(diff_method, qml.gradients.gradient_transform): + if isinstance(diff_method, qml.transforms.core.TransformDispatcher): return diff_method, {}, device raise qml.QuantumFunctionError( @@ -918,11 +918,6 @@ def construct(self, args, kwargs): # pylint: disable=too-many-branches else: self._tape = self.device.expand_fn(self.tape, max_expansion=self.max_expansion) - # If the gradient function is a transform, expand the tape so that - # all operations are supported by the transform. - if isinstance(self.gradient_fn, qml.gradients.gradient_transform): - self._tape = self.gradient_fn.expand_fn(self._tape) - if old_interface == "auto": self.interface = "auto" @@ -969,13 +964,54 @@ def __call__(self, *args, **kwargs) -> qml.typing.Result: ) self._tape_cached = using_custom_cache and self.tape.hash in cache + config = None + # Add the device program to the QNode program + if isinstance(self.device, qml.devices.Device): + if self.gradient_fn is None: + _gradient_method = None + elif isinstance(self.gradient_fn, str): + _gradient_method = self.gradient_fn + else: + _gradient_method = "gradient-transform" + grad_on_execution = self.execute_kwargs.get("grad_on_execution") + config = qml.devices.ExecutionConfig( + interface=self.interface, + gradient_method=_gradient_method, + grad_on_execution=None if grad_on_execution == "best" else grad_on_execution, + ) + device_transform_program, config = self.device.preprocess(execution_config=config) + full_transform_program = self.transform_program + device_transform_program + else: + full_transform_program = self.transform_program + # Add the gradient expand to the porgram if necessary + if ( + isinstance(self.gradient_fn, qml.transforms.core.TransformDispatcher) + and self.gradient_fn.expand_transform + ): + full_transform_program.insert_front_transform( + qml.transforms.core.TransformDispatcher(self.gradient_fn.expand_transform), + **self.gradient_kwargs, + ) + # Calculate the classical jacobians if necessary + if full_transform_program.has_classical_cotransform(): + argnums = full_transform_program[-1]._kwargs.pop( + "argnums", None + ) # pylint: disable=protected-access + full_transform_program._set_all_classical_jacobians( + self, args, kwargs, argnums + ) # pylint: disable=protected-access + full_transform_program._set_all_argnums( + self, args, kwargs, argnums + ) # pylint: disable=protected-access + # pylint: disable=unexpected-keyword-arg res = qml.execute( (self._tape,), device=self.device, gradient_fn=self.gradient_fn, interface=self.interface, - transform_program=self.transform_program, + transform_program=full_transform_program, + config=config, gradient_kwargs=self.gradient_kwargs, override_shots=override_shots, **self.execute_kwargs, diff --git a/pennylane/transforms/adjoint_metric_tensor.py b/pennylane/transforms/adjoint_metric_tensor.py index 6eaf0189736..0339c4d112c 100644 --- a/pennylane/transforms/adjoint_metric_tensor.py +++ b/pennylane/transforms/adjoint_metric_tensor.py @@ -14,14 +14,16 @@ """ Contains the adjoint_metric_tensor. """ -import warnings +from typing import Sequence, Callable from itertools import chain +from functools import partial import pennylane as qml from pennylane import numpy as np -# pylint: disable=too-many-statements +# pylint: disable=too-many-statements,unused-argument from pennylane.transforms.metric_tensor import _contract_metric_tensor_with_cjac +from pennylane.transforms.core import transform def _reshape_real_imag(state, dim): @@ -52,7 +54,14 @@ def _group_operations(tape): return trainable_operations, group_after_trainable_op -def adjoint_metric_tensor(circuit, device=None, hybrid=True): +@partial( + transform, + classical_cotransform=_contract_metric_tensor_with_cjac, + is_informative=True, +) +def adjoint_metric_tensor( + tape: qml.tape.QuantumTape, +) -> (Sequence[qml.tape.QuantumTape], Callable): r"""Implements the adjoint method outlined in `Jones `__ to compute the metric tensor. @@ -71,10 +80,7 @@ def adjoint_metric_tensor(circuit, device=None, hybrid=True): Note also that this makes the metric tensor strictly real-valued. Args: - circuit (.QuantumTape or .QNode): Circuit to compute the metric tensor of - device (.Device): Device to use for the adjoint method - hybrid (bool): Whether to take classical preprocessing into account. Ignored if - ``circuit`` is a tape. + tape (.QuantumTape): Circuit to compute the metric tensor of Returns: array: the metric tensor of the tape with respect to its trainable parameters. @@ -128,147 +134,100 @@ def circuit(weights): The drawback of the adjoint method is that it is only available on simulators and without shot simulations. """ - if isinstance(circuit, qml.tape.QuantumScript): - return _adjoint_metric_tensor_tape(circuit) - if isinstance(circuit, (qml.QNode, qml.ExpvalCost)): - return _adjoint_metric_tensor_qnode(circuit, device, hybrid) - - raise qml.QuantumFunctionError("The passed object is not a QuantumTape or QNode.") - - -def _adjoint_metric_tensor_tape(tape): - """Computes the metric tensor of a tape using the adjoint method and a given device.""" - # pylint: disable=protected-access - if tape.shots: - raise ValueError( - "The adjoint method for the metric tensor is only implemented for shots=None" - ) - if set(tape.wires) != set(range(tape.num_wires)): - wire_map = {w: i for i, w in enumerate(tape.wires)} - tape = qml.map_wires(tape, wire_map) - tape = qml.transforms.expand_trainable_multipar(tape) - - # Divide all operations of a tape into trainable operations and blocks - # of untrainable operations after each trainable one. - trainable_operations, group_after_trainable_op = _group_operations(tape) - - dim = 2**tape.num_wires - # generate and extract initial state - prep = tape[0] if len(tape) > 0 and isinstance(tape[0], qml.operation.StatePrep) else None - - interface = qml.math.get_interface(*tape.get_parameters(trainable_only=False)) - psi = qml.devices.qubit.create_initial_state(tape.wires, prep, like=interface) - - # initialize metric tensor components (which all will be real-valued) - like_real = qml.math.real(psi[0]) - L = qml.math.convert_like(qml.math.zeros((tape.num_params, tape.num_params)), like_real) - T = qml.math.convert_like(qml.math.zeros((tape.num_params,)), like_real) - - for op in group_after_trainable_op[-1]: - psi = qml.devices.qubit.apply_operation(op, psi) - - for j, outer_op in enumerate(trainable_operations): - generator_1, prefactor_1 = qml.generator(outer_op) - - # the state vector phi is missing a factor of 1j * prefactor_1 - phi = qml.devices.qubit.apply_operation(generator_1, psi) - - phi_real, phi_imag = _reshape_real_imag(phi, dim) - diag_value = prefactor_1**2 * ( - qml.math.dot(phi_real, phi_real) + qml.math.dot(phi_imag, phi_imag) - ) - L = qml.math.scatter_element_add(L, (j, j), diag_value) - - lam = psi * 1.0 - lam_real, lam_imag = _reshape_real_imag(lam, dim) - - # this entry is missing a factor of 1j - value = prefactor_1 * (qml.math.dot(lam_real, phi_real) + qml.math.dot(lam_imag, phi_imag)) - T = qml.math.scatter_element_add(T, (j,), value) - - for i in range(j - 1, -1, -1): - # after first iteration of inner loop: apply U_{i+1}^\dagger - if i < j - 1: - phi = qml.devices.qubit.apply_operation( - qml.adjoint(trainable_operations[i + 1], lazy=False), phi - ) - # apply V_{i}^\dagger - for op in reversed(group_after_trainable_op[i]): - adj_op = qml.adjoint(op, lazy=False) - phi = qml.devices.qubit.apply_operation(adj_op, phi) - lam = qml.devices.qubit.apply_operation(adj_op, lam) - - inner_op = trainable_operations[i] - # extract and apply G_i - generator_2, prefactor_2 = qml.generator(inner_op) - # this state vector is missing a factor of 1j * prefactor_2 - mu = qml.devices.qubit.apply_operation(generator_2, lam) - phi_real, phi_imag = _reshape_real_imag(phi, dim) - mu_real, mu_imag = _reshape_real_imag(mu, dim) - # this entry is missing a factor of 1j * (-1j) = 1, i.e. none - value = ( - prefactor_1 - * prefactor_2 - * (qml.math.dot(mu_real, phi_real) + qml.math.dot(mu_imag, phi_imag)) + def processing_fn(tapes): + tape = tapes[0] + if tape.shots: + raise ValueError( + "The adjoint method for the metric tensor is only implemented for shots=None" ) - L = qml.math.scatter_element_add( - L, [(i, j), (j, i)], value * qml.math.convert_like(qml.math.ones((2,)), value) - ) - # apply U_i^\dagger - lam = qml.devices.qubit.apply_operation(qml.adjoint(inner_op, lazy=False), lam) + if set(tape.wires) != set(range(tape.num_wires)): + wire_map = {w: i for i, w in enumerate(tape.wires)} + tape = qml.map_wires(tape, wire_map) + tape = qml.transforms.expand_trainable_multipar(tape) - # apply U_j and V_j - psi = qml.devices.qubit.apply_operation(outer_op, psi) - for op in group_after_trainable_op[j]: - psi = qml.devices.qubit.apply_operation(op, psi) + # Divide all operations of a tape into trainable operations and blocks + # of untrainable operations after each trainable one. + trainable_operations, group_after_trainable_op = _group_operations(tape) - # postprocessing: combine L and T into the metric tensor. - # We require outer(conj(T), T) here, but as we skipped the factor 1j above, - # the stored T is real-valued. Thus we have -1j*1j*outer(T, T) = outer(T, T) - metric_tensor = L - qml.math.tensordot(T, T, 0) + dim = 2**tape.num_wires + # generate and extract initial state + prep = tape[0] if len(tape) > 0 and isinstance(tape[0], qml.operation.StatePrep) else None - return metric_tensor + interface = qml.math.get_interface(*tape.get_parameters(trainable_only=False)) + psi = qml.devices.qubit.create_initial_state(tape.wires, prep, like=interface) + # initialize metric tensor components (which all will be real-valued) + like_real = qml.math.real(psi[0]) + L = qml.math.convert_like(qml.math.zeros((tape.num_params, tape.num_params)), like_real) + T = qml.math.convert_like(qml.math.zeros((tape.num_params,)), like_real) -def _adjoint_metric_tensor_qnode(qnode, device, hybrid): - """Computes the metric tensor of a qnode using the adjoint method and its device. - For ``hybrid==True`` this wrapper accounts for classical preprocessing within the - QNode. - """ - if device is None: - if isinstance(qnode, qml.ExpvalCost): - if qnode._multiple_devices: # pylint: disable=protected-access - warnings.warn( - "ExpvalCost was instantiated with multiple devices. Only the first device " - "will be used to evaluate the metric tensor with the adjoint method.", - UserWarning, - ) - qnode = qnode.qnodes[0] - device = qnode.device + for op in group_after_trainable_op[-1]: + psi = qml.devices.qubit.apply_operation(op, psi) - def wrapper(*args, **kwargs): - old_interface = qnode.interface - if old_interface == "auto": - qnode.interface = qml.math.get_interface(*args, *list(kwargs.values())) + for j, outer_op in enumerate(trainable_operations): + generator_1, prefactor_1 = qml.generator(outer_op) - cjac_fn = qml.transforms.classical_jacobian( - qnode, expand_fn=qml.transforms.expand_trainable_multipar - ) + # the state vector phi is missing a factor of 1j * prefactor_1 + phi = qml.devices.qubit.apply_operation(generator_1, psi) - qnode.construct(args, kwargs) - program, _ = qml.devices.qubit.preprocess() - tapes, _ = program((qnode.tape,)) - mt = _adjoint_metric_tensor_tape(tapes[0]) + phi_real, phi_imag = _reshape_real_imag(phi, dim) + diag_value = prefactor_1**2 * ( + qml.math.dot(phi_real, phi_real) + qml.math.dot(phi_imag, phi_imag) + ) + L = qml.math.scatter_element_add(L, (j, j), diag_value) + + lam = psi * 1.0 + lam_real, lam_imag = _reshape_real_imag(lam, dim) - if old_interface == "auto": - qnode.interface = "auto" + # this entry is missing a factor of 1j + value = prefactor_1 * ( + qml.math.dot(lam_real, phi_real) + qml.math.dot(lam_imag, phi_imag) + ) + T = qml.math.scatter_element_add(T, (j,), value) + + for i in range(j - 1, -1, -1): + # after first iteration of inner loop: apply U_{i+1}^\dagger + if i < j - 1: + phi = qml.devices.qubit.apply_operation( + qml.adjoint(trainable_operations[i + 1], lazy=False), phi + ) + # apply V_{i}^\dagger + for op in reversed(group_after_trainable_op[i]): + adj_op = qml.adjoint(op, lazy=False) + phi = qml.devices.qubit.apply_operation(adj_op, phi) + lam = qml.devices.qubit.apply_operation(adj_op, lam) + + inner_op = trainable_operations[i] + # extract and apply G_i + generator_2, prefactor_2 = qml.generator(inner_op) + # this state vector is missing a factor of 1j * prefactor_2 + mu = qml.devices.qubit.apply_operation(generator_2, lam) + + phi_real, phi_imag = _reshape_real_imag(phi, dim) + mu_real, mu_imag = _reshape_real_imag(mu, dim) + # this entry is missing a factor of 1j * (-1j) = 1, i.e. none + value = ( + prefactor_1 + * prefactor_2 + * (qml.math.dot(mu_real, phi_real) + qml.math.dot(mu_imag, phi_imag)) + ) + L = qml.math.scatter_element_add( + L, [(i, j), (j, i)], value * qml.math.convert_like(qml.math.ones((2,)), value) + ) + # apply U_i^\dagger + lam = qml.devices.qubit.apply_operation(qml.adjoint(inner_op, lazy=False), lam) - if not hybrid: - return mt + # apply U_j and V_j + psi = qml.devices.qubit.apply_operation(outer_op, psi) + for op in group_after_trainable_op[j]: + psi = qml.devices.qubit.apply_operation(op, psi) - cjac = cjac_fn(*args, **kwargs) + # postprocessing: combine L and T into the metric tensor. + # We require outer(conj(T), T) here, but as we skipped the factor 1j above, + # the stored T is real-valued. Thus we have -1j*1j*outer(T, T) = outer(T, T) + metric_tensor = L - qml.math.tensordot(T, T, 0) - return _contract_metric_tensor_with_cjac(mt, cjac) + return metric_tensor - return wrapper + return [tape], processing_fn diff --git a/pennylane/transforms/core/transform.py b/pennylane/transforms/core/transform.py index aabe860d5a4..0feeb1ca9e3 100644 --- a/pennylane/transforms/core/transform.py +++ b/pennylane/transforms/core/transform.py @@ -130,10 +130,8 @@ def qnode_circuit(a): # 3: CHeck the classical co-transform if classical_cotransform is not None: - raise NotImplementedError("Classical cotransforms are not yet integrated.") - # TODO: Add more verification in a future PR - # if not callable(classical_cotransform): - # raise TransformError("The classical co-transform must be a valid Python function.") + if not callable(classical_cotransform): + raise TransformError("The classical co-transform must be a valid Python function.") return TransformDispatcher( quantum_transform, diff --git a/pennylane/transforms/core/transform_dispatcher.py b/pennylane/transforms/core/transform_dispatcher.py index 6b7660a8ad9..93ae8ed9a4e 100644 --- a/pennylane/transforms/core/transform_dispatcher.py +++ b/pennylane/transforms/core/transform_dispatcher.py @@ -67,7 +67,17 @@ def __call__(self, *targs, **tkwargs): # pylint: disable=too-many-return-statem obj, *targs = targs if isinstance(obj, qml.tape.QuantumScript): - transformed_tapes, processing_fn = self._transform(obj, *targs, **tkwargs) + if self._expand_transform: + transformed_tapes, _ = self._expand_transform(obj, *targs, **tkwargs) + transformed_tapes, transform_processing_fn = self._transform( + transformed_tapes[0], *targs, **tkwargs + ) + + def processing_fn(results): + return transform_processing_fn(results) + + else: + transformed_tapes, processing_fn = self._transform(obj, *targs, **tkwargs) if self.is_informative: return processing_fn(transformed_tapes) @@ -108,6 +118,14 @@ def wrapper(obj): return wrapper + def __repr__(self): + return f"" + + @property + def __name__(self): + """Return the quantum transform name.""" + return self._transform.__name__ + @property def transform(self): """Return the quantum transform.""" diff --git a/pennylane/transforms/core/transform_program.py b/pennylane/transforms/core/transform_program.py index 73d716243de..9889ac1b2e0 100644 --- a/pennylane/transforms/core/transform_program.py +++ b/pennylane/transforms/core/transform_program.py @@ -17,6 +17,7 @@ from functools import partial from typing import Callable, List, Tuple, Optional, Sequence +import pennylane as qml from pennylane.typing import Result, ResultBatch from pennylane.tape import QuantumTape @@ -113,6 +114,8 @@ class TransformProgram: def __init__(self, initial_program: Optional[Sequence] = None): self._transform_program = list(initial_program) if initial_program else [] + self._classical_jacobians = None + self._argnums = None def __iter__(self): """list[TransformContainer]: Return an iterator to the underlying transform program.""" @@ -276,36 +279,189 @@ def has_final_transform(self) -> bool: """Check if the transform program has a terminal transform or not.""" return self[-1].final_transform if self else False + def has_classical_cotransform(self) -> bool: + """Check if the transform program has some classical cotransforms. + + Returns: + bool: Boolean + """ + return any(t.classical_cotransform is not None for t in self) + + def _set_all_classical_jacobians( + self, qnode, args, kwargs, argnums + ): # pylint: disable=too-many-statements + """It can be called inside the QNode to get all the classical Jacobians for a gradient transform.""" + + def classical_preprocessing(program, *args, **kwargs): + """Returns the trainable gate parameters for a given QNode input.""" + kwargs.pop("shots", None) + kwargs.pop("argnums", None) + qnode.construct(args, kwargs) + tape = qnode.qtape + tapes, _ = program((tape,)) + res = tuple(qml.math.stack(tape.get_parameters(trainable_only=True)) for tape in tapes) + if len(tapes) == 1: + return res[0] + return res + + def jacobian(classical_function, program, argnums, *args, **kwargs): + indices = qml.math.get_trainable_indices(args) + + if qnode.interface in ["jax", "jax-jit"]: + import jax # pylint: disable=import-outside-toplevel + + if isinstance(args[0], jax.numpy.ndarray): + argnums = 0 if argnums is None else argnums + + if not indices and argnums is None: + raise qml.QuantumFunctionError("No trainable parameters.") + + classical_function = partial(classical_function, program) + + if qnode.interface == "autograd": + jac = qml.jacobian(classical_function, argnum=argnums)(*args, **kwargs) + + if qnode.interface == "tf": + import tensorflow as tf # pylint: disable=import-outside-toplevel + + def _jacobian(*args, **kwargs): + with tf.GradientTape() as tape: + gate_params = classical_function(*args, **kwargs) + + jac = tape.jacobian(gate_params, args) + return jac + + jac = _jacobian(*args, **kwargs) + + if qnode.interface == "torch": + import torch # pylint: disable=import-outside-toplevel + + def _jacobian(*args, **kwargs): # pylint: disable=unused-argument + jac = torch.autograd.functional.jacobian(classical_function, args) + return jac + + jac = _jacobian(*args, **kwargs) + + if qnode.interface in ["jax", "jax-jit"]: + import jax # pylint: disable=import-outside-toplevel + + argnums = 0 if argnums is None else argnums + + def _jacobian(*args, **kwargs): + return jax.jacobian(classical_function, argnums=argnums)(*args, **kwargs) + + jac = _jacobian(*args, **kwargs) + + return jac + + classical_jacobians = [] + for index, transform in enumerate(self): + if transform.classical_cotransform: + argnum = transform._kwargs.get("argnum", None) # pylint: disable=protected-access + if qnode.interface == "jax" and argnum: + raise qml.QuantumFunctionError( + "argnum does not work with the Jax interface. You should use argnums instead." + ) + sub_program = TransformProgram(self[0:index]) + classical_jacobian = jacobian( + classical_preprocessing, sub_program, argnums, *args, **kwargs + ) + qnode.construct(args, kwargs) + tapes, _ = sub_program((qnode.tape,)) + multi_tapes = len(tapes) > 1 + if not multi_tapes: + classical_jacobian = [classical_jacobian] + classical_jacobians.append(classical_jacobian) + else: + classical_jacobians.append(None) + self._classical_jacobians = classical_jacobians + # Reset the initial tape + qnode.construct(args, kwargs) + + def _set_all_argnums(self, qnode, args, kwargs, argnums): + """It can be used inside the QNode to set all argnums (tape level) using argnums from the argnums at the QNode + level. + """ + + def jax_argnums_to_tape_trainable(program, argnums, args, kwargs): + import jax # pylint: disable=import-outside-toplevel + + with jax.core.new_main(jax.interpreters.ad.JVPTrace) as main: + trace = jax.interpreters.ad.JVPTrace(main, 0) + + args_jvp = [ + jax.interpreters.ad.JVPTracer(trace, arg, jax.numpy.zeros(arg.shape)) + if i in argnums + else arg + for i, arg in enumerate(args) + ] + + qnode.construct(args_jvp, kwargs) + tape = qnode.qtape + tapes, _ = program((tape,)) + del trace + return tuple(tape.get_parameters(trainable_only=False) for tape in tapes) + + argnums_list = [] + for index, transform in enumerate(self): + argnums = [0] if qnode.interface in ["jax", "jax-jit"] and argnums is None else argnums + if transform.classical_cotransform and argnums: + params = jax_argnums_to_tape_trainable( + TransformProgram(self[0:index]), argnums, args, kwargs + ) + argnums_list.append([qml.math.get_trainable_indices(param) for param in params]) + else: + argnums_list.append(None) + + self._argnums = argnums_list + + qnode.construct(args, kwargs) + def __call__(self, tapes: Tuple[QuantumTape]) -> Tuple[ResultBatch, BatchPostProcessingFn]: if not self: return tapes, null_postprocessing processing_fns_stack = [] - for transform_container in self: - transform, args, kwargs, cotransform, _, _ = transform_container - - if cotransform: - raise NotImplementedError( - "cotransforms are not yet integrated with TransformProgram" - ) + for i, transform_container in enumerate(self): + transform, targs, tkwargs, cotransform, _, _ = transform_container execution_tapes = [] fns = [] slices = [] + classical_fns = [] + slices_classical = [] + start = 0 - for tape in tapes: - new_tapes, fn = transform(tape, *args, **kwargs) + start_classical = 0 + for j, tape in enumerate(tapes): + if self._argnums is not None and self._argnums[i] is not None: + tape.trainable_params = self._argnums[i][j] + new_tapes, fn = transform(tape, *targs, **tkwargs) execution_tapes.extend(new_tapes) + fns.append(fn) end = start + len(new_tapes) slices.append(slice(start, end)) start = end + if cotransform and self._classical_jacobians: + classical_fns.append( + partial(cotransform, cjac=self._classical_jacobians[i][j], tape=tape) + ) + slices_classical.append(slice(start_classical, start_classical + 1)) + start_classical += 1 + + if cotransform: + batch_postprocessing_classical = partial( + _batch_postprocessing, individual_fns=classical_fns, slices=slices_classical + ) + batch_postprocessing_classical.__doc__ = _batch_postprocessing.__doc__ + processing_fns_stack.append(batch_postprocessing_classical) + batch_postprocessing = partial(_batch_postprocessing, individual_fns=fns, slices=slices) batch_postprocessing.__doc__ = _batch_postprocessing.__doc__ - processing_fns_stack.append(batch_postprocessing) # set input tapes for next iteration. @@ -318,4 +474,6 @@ def __call__(self, tapes: Tuple[QuantumTape]) -> Tuple[ResultBatch, BatchPostPro postprocessing_fn.__doc__ = _apply_postprocessing_stack.__doc__ + # Reset classical jacobians + self._classical_jacobians = [] return tuple(tapes), postprocessing_fn diff --git a/pennylane/transforms/metric_tensor.py b/pennylane/transforms/metric_tensor.py index 639f28c94dc..479e21d7e04 100644 --- a/pennylane/transforms/metric_tensor.py +++ b/pennylane/transforms/metric_tensor.py @@ -15,36 +15,87 @@ Contains the metric_tensor batch_transform which wraps multiple methods of computing the metric tensor. """ +from typing import Sequence, Callable import functools +from functools import partial import warnings import numpy as np import pennylane as qml from pennylane.circuit_graph import LayerData from pennylane.queuing import WrappedObj +from pennylane.transforms.core import transform -from .batch_transform import batch_transform + +def _contract_metric_tensor_with_cjac(mt, cjac, tape): # pylint: disable=unused-argument + """Execute the contraction of pre-computed classical Jacobian(s) + and the metric tensor of a tape in order to obtain the hybrid + metric tensor of a QNode. + + Args: + mt (array): Metric tensor of a tape (2-dimensional) + cjac (array or tuple[array]): The classical Jacobian of a QNode + + Returns: + array or tuple[array]: Hybrid metric tensor(s) of the QNode. + The number of metric tensors depends on the number of QNode arguments + for which the classical Jacobian was computed, the tensor shape(s) + depend on the shape of these QNode arguments. + """ + if isinstance(mt, tuple) and len(mt) == 1: + mt = mt[0] + if isinstance(cjac, tuple): + # Classical processing of multiple arguments is present. Return cjac.T @ mt @ cjac + # as a tuple of contractions. + metric_tensors = tuple( + qml.math.tensordot(c, qml.math.tensordot(mt, c, axes=[[-1], [0]]), axes=[[0], [0]]) + for c in cjac + if c is not None + ) + return metric_tensors[0] if len(metric_tensors) == 1 else metric_tensors + + is_square = cjac.shape == (1,) or (cjac.ndim == 2 and cjac.shape[0] == cjac.shape[1]) + + if is_square and qml.math.allclose(cjac, qml.numpy.eye(cjac.shape[0])): + # Classical Jacobian is the identity. No classical processing + # is present inside the QNode. + return mt + mt_cjac = qml.math.tensordot(mt, cjac, axes=[[-1], [0]]) + mt = qml.math.tensordot(cjac, mt_cjac, axes=[[0], [0]]) + + return mt -def expand_fn( - tape, argnum=None, approx=None, allow_nonunitary=True, aux_wire=None, device_wires=None -): +def _expand_metric_tensor( + tape: qml.tape.QuantumTape, + argnum=None, + approx=None, + allow_nonunitary=True, + aux_wire=None, + device_wires=None, +) -> (Sequence[qml.tape.QuantumTape], Callable): # pylint: disable=too-many-arguments """Set the metric tensor based on whether non-unitary gates are allowed.""" # pylint: disable=unused-argument,too-many-arguments - if not allow_nonunitary and approx is None: # pragma: no cover - return qml.transforms.expand_nonunitary_gen(tape) - return qml.transforms.expand_multipar(tape) + + if not allow_nonunitary and approx is None: + return [qml.transforms.expand_nonunitary_gen(tape)], lambda x: x[0] + return [qml.transforms.expand_multipar(tape)], lambda x: x[0] -@functools.partial(batch_transform, expand_fn=expand_fn) -def metric_tensor( - tape, +@partial( + transform, + expand_transform=_expand_metric_tensor, + classical_cotransform=_contract_metric_tensor_with_cjac, + final_transform=True, +) +def metric_tensor( # pylint:disable=too-many-arguments + tape: qml.tape.QuantumTape, argnum=None, approx=None, allow_nonunitary=True, aux_wire=None, device_wires=None, -): # pylint: disable=too-many-arguments +) -> (Sequence[qml.tape.QuantumTape], Callable): r"""Returns a function that computes the metric tensor of a given QNode or quantum tape. The metric tensor convention we employ here has the following form: @@ -67,7 +118,7 @@ def metric_tensor( This is the case for unitary single-parameter operations. Args: - tape (pennylane.QNode or .QuantumTape): quantum tape or QNode to find the metric tensor of + tape (QuantumTape): quantum tape to find the metric tensor of argnum (int or Sequence[int] or None): Trainable tape-parameter indices with respect to which the metric tensor is computed. If ``argnum=None``, the metric tensor with respect to all trainable parameters is returned. Excluding tape-parameter indices from this list reduces @@ -323,10 +374,14 @@ def circuit(weights): if approx in {"diag", "block-diag"}: # Only require covariance matrix based transform diag_approx = approx == "diag" - return _metric_tensor_cov_matrix(tape, argnum, diag_approx)[:2] + tapes, processing_fn = _metric_tensor_cov_matrix(tape, argnum, diag_approx)[:2] + return tapes, processing_fn if approx is None: - return _metric_tensor_hadamard(tape, argnum, allow_nonunitary, aux_wire, device_wires) + tapes, processing_fn = _metric_tensor_hadamard( + tape, argnum, allow_nonunitary, aux_wire, device_wires + ) + return tapes, processing_fn raise ValueError( f"Unknown value {approx} for keyword argument approx. " @@ -334,142 +389,17 @@ def circuit(weights): ) -def _contract_metric_tensor_with_cjac(mt, cjac): - """Execute the contraction of pre-computed classical Jacobian(s) - and the metric tensor of a tape in order to obtain the hybrid - metric tensor of a QNode. - - Args: - mt (array): Metric tensor of a tape (2-dimensional) - cjac (array or tuple[array]): The classical Jacobian of a QNode - - Returns: - array or tuple[array]: Hybrid metric tensor(s) of the QNode. - The number of metric tensors depends on the number of QNode arguments - for which the classical Jacobian was computed, the tensor shape(s) - depend on the shape of these QNode arguments. - """ - if isinstance(cjac, tuple): - # Classical processing of multiple arguments is present. Return cjac.T @ mt @ cjac - # as a tuple of contractions. - metric_tensors = tuple( - qml.math.tensordot(c, qml.math.tensordot(mt, c, axes=[[-1], [0]]), axes=[[0], [0]]) - for c in cjac - if c is not None - ) - return metric_tensors[0] if len(metric_tensors) == 1 else metric_tensors - - is_square = cjac.shape == (1,) or (cjac.ndim == 2 and cjac.shape[0] == cjac.shape[1]) - - if is_square and qml.math.allclose(cjac, qml.numpy.eye(cjac.shape[0])): - # Classical Jacobian is the identity. No classical processing - # is present inside the QNode. - return mt - - mt = qml.math.tensordot(cjac, qml.math.tensordot(mt, cjac, axes=[[-1], [0]]), axes=[[0], [0]]) - - return mt - - -@metric_tensor.custom_qnode_wrapper +@metric_tensor.custom_qnode_transform def qnode_execution_wrapper(self, qnode, targs, tkwargs): """Here, we overwrite the QNode execution wrapper in order to take into account that classical processing may be present inside the QNode.""" - hybrid = tkwargs.pop("hybrid", True) - - if isinstance(qnode, qml.ExpvalCost): - if qnode._multiple_devices: # pylint: disable=protected-access - warnings.warn( - "ExpvalCost was instantiated with multiple devices. Only the first device " - "will be used to evaluate the metric tensor.", - UserWarning, - ) - - qnode = qnode.qnodes[0] tkwargs.setdefault("device_wires", qnode.device.wires) - mt_fn = self.default_qnode_wrapper(qnode, targs, tkwargs) - - def wrapper(*args, **kwargs): # pylint: disable=too-many-branches - argnum = tkwargs.get("argnum", None) - argnums = tkwargs.get("argnums", None) - - interface = qml.math.get_interface(*args) - trainable_params = qml.math.get_trainable_indices(args) - - if interface == "jax" and argnum is not None: - raise qml.QuantumFunctionError( - "argnum does not work with the Jax interface. You should use argnums instead." - ) - if interface == "jax" and not trainable_params: - if argnums is None: - argnums_ = [0] - - else: - argnums_ = [argnums] if isinstance(argnums, int) else argnums - - params = qml.math.jax_argnums_to_tape_trainable( - qnode, argnums_, self.expand_fn, args, kwargs - ) - argnums_ = qml.math.get_trainable_indices(params) - kwargs["argnums"] = argnums_ - - elif not trainable_params: - warnings.warn( - "Attempted to compute the metric tensor of a QNode with no trainable parameters. " - "If this is unintended, please add trainable parameters in accordance with the " - "chosen auto differentiation framework." - ) - return () - - try: - mt = mt_fn(*args, **kwargs) - except qml.wires.WireError as e: - revert_text = ( - "\n\nReverting to the block-diagonal approximation. It will often be " - "much more efficient to request the block-diagonal approximation directly!" - ) - other_mt_errors = [ - "The requested auxiliary wire is already in use by the circuit.", - "The requested auxiliary wire does not exist on the used device.", - ] - - if str(e) == "The device has no free wire for the auxiliary wire.": - warnings.warn( - "The device does not have a wire that is not used by the circuit." + revert_text - ) - elif str(e) in other_mt_errors: - warnings.warn( - "An auxiliary wire is not available." - "\n\nThis can occur when computing the full metric tensor via the " - "Hadamard test, and the device does not provide an " - "additional wire or the requested auxiliary wire does not exist " - "on the device." + revert_text - ) - else: - raise e - tkwargs["approx"] = "block-diag" - return self(qnode, *targs, **tkwargs)(*args, **kwargs) - - if not hybrid: - return mt - - kwargs.pop("shots", False) - # Special case where we apply a Jax transform (jacobian e.g.) on the gradient transform and argnums are - # defined on the outer transform and therefore on the args. - if interface == "jax": - argnum_cjac = trainable_params or argnums - else: - argnum_cjac = None - - cjac = qml.transforms.classical_jacobian( - qnode, argnum=argnum_cjac, expand_fn=self.expand_fn - )(*args, **kwargs) - return _contract_metric_tensor_with_cjac(mt, cjac) + mt_fn = self.default_qnode_transform(qnode, targs, tkwargs) - return wrapper + return mt_fn def _metric_tensor_cov_matrix(tape, argnum, diag_approx): # pylint: disable=too-many-statements diff --git a/pennylane/transforms/qcut/cutcircuit.py b/pennylane/transforms/qcut/cutcircuit.py index 288eab7612a..10bc2d18972 100644 --- a/pennylane/transforms/qcut/cutcircuit.py +++ b/pennylane/transforms/qcut/cutcircuit.py @@ -50,7 +50,7 @@ def processing_fn(res): # Expand the tapes for handling Hamiltonian with two or more terms tape_meas_ops = tape.measurements - if isinstance(tape_meas_ops[0].obs, qml.Hamiltonian): + if tape_meas_ops and isinstance(tape_meas_ops[0].obs, qml.Hamiltonian): if len(tape_meas_ops) > 1: raise NotImplementedError( "Hamiltonian expansion is supported only with a single Hamiltonian" diff --git a/pennylane/transforms/specs.py b/pennylane/transforms/specs.py index 1bda5cbd805..ee6c2b6fc93 100644 --- a/pennylane/transforms/specs.py +++ b/pennylane/transforms/specs.py @@ -138,7 +138,7 @@ def specs_qnode(*args, **kwargs): else qnode.diff_method ) - if isinstance(qnode.gradient_fn, qml.gradients.gradient_transform): + if isinstance(qnode.gradient_fn, qml.transforms.core.TransformDispatcher): info["gradient_fn"] = _get_absolute_import_path(qnode.gradient_fn) try: diff --git a/tests/drawer/test_draw.py b/tests/drawer/test_draw.py index d6686d62209..a574ceac7c1 100644 --- a/tests/drawer/test_draw.py +++ b/tests/drawer/test_draw.py @@ -173,9 +173,9 @@ def matrices_circuit(): expected2 = ( "0: ─╭|Ψ⟩──U(M0)─┤ <𝓗(M0)>\n" "1: ─╰|Ψ⟩────────┤ \n" + "\n" "M0 = \n[[1. 0.]\n [0. 1.]]" ) - assert draw(matrices_circuit)() == expected2 def test_matrix_parameters_batch_transform(self): diff --git a/tests/gradients/core/test_gradient_transform.py b/tests/gradients/core/test_gradient_transform.py index 7ab8eb1f885..596c0060e6f 100644 --- a/tests/gradients/core/test_gradient_transform.py +++ b/tests/gradients/core/test_gradient_transform.py @@ -25,9 +25,9 @@ def test_repr(): """Test the repr method of gradient transforms.""" - assert repr(qml.gradients.param_shift) == "" - assert repr(qml.gradients.spsa_grad) == "" - assert repr(qml.gradients.finite_diff) == "" + assert repr(qml.gradients.param_shift) == "" + assert repr(qml.gradients.spsa_grad) == "" + assert repr(qml.gradients.finite_diff) == "" class TestGradAnalysis: @@ -401,7 +401,7 @@ def circuit(x, y): expected = qml.jacobian(circuit)(x, y) # pylint:disable=unexpected-keyword-arg - res = qml.gradients.param_shift(circuit, hybrid=True)(x, y) + res = qml.gradients.param_shift(circuit)(x, y) assert isinstance(res, tuple) and len(res) == 2 assert all(np.allclose(_r, _e, atol=tol, rtol=0) for _r, _e in zip(res, expected)) @@ -496,11 +496,10 @@ def circuit(x, y): assert np.allclose(res, expected, atol=tol, rtol=0) - def test_classical_processing_arguments(self, mocker, tol): + def test_classical_processing_arguments(self, tol): """Test that a gradient transform acts on QNodes correctly when the QNode arguments are classically processed""" dev = qml.device("default.qubit", wires=2) - spy = mocker.spy(qml.transforms, "classical_jacobian") @qml.qnode(dev) def circuit(weights): @@ -512,19 +511,14 @@ def circuit(weights): w = np.array([0.543, -0.654], requires_grad=True) res = qml.gradients.param_shift(circuit)(w) - classical_jac = spy.spy_return(w) - assert isinstance(classical_jac, np.ndarray) - assert np.allclose(classical_jac, np.array([[2 * w[0], 0], [0, 1]])) - x, _ = w expected = [-2 * x * np.sin(x**2), 0] assert np.allclose(res, expected, atol=tol, rtol=0) - def test_classical_processing_multiple_arguments(self, mocker, tol): + def test_classical_processing_multiple_arguments(self, tol): """Test that a gradient transform acts on QNodes correctly when multiple QNode arguments are classically processed""" dev = qml.device("default.qubit", wires=2) - spy = mocker.spy(qml.transforms, "classical_jacobian") @qml.qnode(dev) def circuit(data, weights): @@ -540,8 +534,6 @@ def circuit(data, weights): x, _ = w res = qml.gradients.param_shift(circuit)(d, w) - classical_jac = spy.spy_return(d, w) - assert np.allclose(classical_jac, np.array([[2 * w[0], 0], [0, 1]]).T) expected = np.array([-2 * x * np.cos(np.cos(d)) * np.sin(x**2), 0]) assert np.allclose(res, expected, atol=tol, rtol=0) @@ -551,10 +543,6 @@ def circuit(data, weights): w = np.array([0.543, -0.654], requires_grad=True) res = qml.gradients.param_shift(circuit)(d, w) - classical_jac = spy.spy_return(d, w) - assert isinstance(classical_jac, tuple) - assert np.allclose(classical_jac[0], [-np.sin(d), 0, 0]) - assert np.allclose(classical_jac[1], np.array([[0, 2 * w[0], 0], [0, 0, 1]]).T) expected_dd = np.cos(x**2) * np.sin(d) * np.sin(np.cos(d)) expected_dw = np.array([-2 * x * np.cos(np.cos(d)) * np.sin(x**2), 0]) @@ -581,12 +569,6 @@ def circuit(weights): expected = qml.jacobian(circuit)(w) assert np.allclose(res, expected, atol=tol, rtol=0) - # when executed with hybrid=False, only the quantum jacobian is returned - # pylint:disable=unexpected-keyword-arg - res = qml.gradients.param_shift(circuit, hybrid=False)(w) - assert res[0].shape == (4,) - assert res[1].shape == (4,) - @qml.qnode(dev) def circuit1(weights): qml.RX(weights[0], wires=[0]) @@ -597,8 +579,8 @@ def circuit1(weights): w = np.array([0.543**2, -0.654], requires_grad=True) expected = qml.jacobian(circuit1)(w) - assert np.allclose(res[0], expected.T[0], atol=tol, rtol=0) - assert np.allclose(res[1], expected.T[1], atol=tol, rtol=0) + assert np.allclose(res[0][0], expected[0], atol=10e-2, rtol=0) + assert np.allclose(res[1][0], expected[1], atol=10e-2, rtol=0) @pytest.mark.parametrize("strategy", ["gradient", "device"]) def test_template_integration(self, strategy, tol): @@ -641,22 +623,6 @@ def circuit(x): assert circuit(x).shape == tuple() assert circuit(x, shots=1000).shape == tuple() - def test_shots_error(self): - """Raise an exception if shots is used within the QNode""" - dev = qml.device("default.qubit", wires=1, shots=1000) - - def circuit(x, shots): - """A quantum circuit that takes `shots` as an argument.""" - # pylint: disable=unused-argument - qml.RX(x, wires=0) - return qml.expval(qml.PauliZ(0)) - - with pytest.warns(UserWarning, match="Detected 'shots' as an argument to the given"): - qnode = qml.QNode(circuit, dev) - - with pytest.raises(ValueError, match="Detected 'shots' as an argument of the quantum"): - qml.gradients.param_shift(qnode)(0.2, shots=100) - class TestInterfaceIntegration: """Test that the gradient transforms are differentiable diff --git a/tests/gradients/core/test_hadamard_gradient.py b/tests/gradients/core/test_hadamard_gradient.py index 3b485b7aaab..1c0ba0e2db6 100644 --- a/tests/gradients/core/test_hadamard_gradient.py +++ b/tests/gradients/core/test_hadamard_gradient.py @@ -464,7 +464,10 @@ def test_output_shape_matches_qnode_expval(self, cost, expected_shape): circuit = qml.QNode(cost, dev) res_hadamard = qml.gradients.hadamard_grad(circuit)(x) - assert isinstance(res_hadamard, tuple) + + assert isinstance(res_hadamard, (tuple, list)) + if len(res_hadamard) == 1: + res_hadamard = res_hadamard[0] assert len(res_hadamard) == expected_shape[0] if len(expected_shape) > 1: @@ -487,7 +490,9 @@ def test_output_shape_matches_qnode_probs(self, cost, expected_shape): circuit = qml.QNode(cost, dev) res_hadamard = qml.gradients.hadamard_grad(circuit)(x) - assert isinstance(res_hadamard, tuple) + assert isinstance(res_hadamard, (tuple, list)) + if len(res_hadamard) == 1: + res_hadamard = res_hadamard[0] assert len(res_hadamard) == expected_shape[0] if len(expected_shape) > 2: @@ -723,11 +728,10 @@ def test_independent_parameter(self, mocker): assert spy.call_args[0][0:2] == (tape, [0]) @pytest.mark.autograd - def test_no_trainable_params_qnode_autograd(self, mocker): + def test_no_trainable_params_qnode_autograd(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit", wires=2) - spy = mocker.spy(qml.devices.qubit, "measure") @qml.qnode(dev, interface="autograd") def circuit(weights): @@ -736,18 +740,14 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res_hadamard = qml.gradients.hadamard_grad(circuit)(weights) - - assert res_hadamard == () - spy.assert_not_called() + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.torch - def test_no_trainable_params_qnode_torch(self, mocker): + def test_no_trainable_params_qnode_torch(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit", wires=2) - spy = mocker.spy(qml.devices.qubit, "measure") @qml.qnode(dev, interface="torch") def circuit(weights): @@ -756,18 +756,14 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res_hadamard = qml.gradients.hadamard_grad(circuit)(weights) - - assert res_hadamard == () - spy.assert_not_called() + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.tf - def test_no_trainable_params_qnode_tf(self, mocker): + def test_no_trainable_params_qnode_tf(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit", wires=2) - spy = mocker.spy(qml.devices.qubit, "measure") @qml.qnode(dev, interface="tf") def circuit(weights): @@ -776,18 +772,14 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res_hadamard = qml.gradients.hadamard_grad(circuit)(weights) - - assert res_hadamard == () - spy.assert_not_called() + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.jax - def test_no_trainable_params_qnode_jax(self, mocker): + def test_no_trainable_params_qnode_jax(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit", wires=2) - spy = mocker.spy(qml.devices.qubit, "measure") @qml.qnode(dev, interface="jax") def circuit(weights): @@ -796,18 +788,14 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res_hadamard = qml.gradients.hadamard_grad(circuit)(weights) - - assert res_hadamard == () - spy.assert_not_called() + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.autograd - def test_no_trainable_params_qnode_autograd_legacy(self, mocker): + def test_no_trainable_params_qnode_autograd_legacy(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit.autograd", wires=2) - spy = mocker.spy(dev, "expval") @qml.qnode(dev, interface="autograd") def circuit(weights): @@ -816,18 +804,14 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res_hadamard = qml.gradients.hadamard_grad(circuit)(weights) - - assert res_hadamard == () - spy.assert_not_called() + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.torch - def test_no_trainable_params_qnode_torch_legacy(self, mocker): + def test_no_trainable_params_qnode_torch_legacy(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit.torch", wires=2) - spy = mocker.spy(dev, "expval") @qml.qnode(dev, interface="torch") def circuit(weights): @@ -836,18 +820,14 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res_hadamard = qml.gradients.hadamard_grad(circuit)(weights) - - assert res_hadamard == () - spy.assert_not_called() + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.tf - def test_no_trainable_params_qnode_tf_legacy(self, mocker): + def test_no_trainable_params_qnode_tf_legacy(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit.tf", wires=2) - spy = mocker.spy(dev, "expval") @qml.qnode(dev, interface="tf") def circuit(weights): @@ -856,18 +836,14 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res_hadamard = qml.gradients.hadamard_grad(circuit)(weights) - - assert res_hadamard == () - spy.assert_not_called() + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.jax - def test_no_trainable_params_qnode_jax_legacy(self, mocker): + def test_no_trainable_params_qnode_jax_legacy(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit.jax", wires=2) - spy = mocker.spy(dev, "expval") @qml.qnode(dev, interface="jax") def circuit(weights): @@ -876,11 +852,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res_hadamard = qml.gradients.hadamard_grad(circuit)(weights) - - assert res_hadamard == () - spy.assert_not_called() + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.hadamard_grad(circuit)(weights) def test_no_trainable_params_tape(self): """Test that the correct ouput and warning is generated in the absence of any trainable @@ -1018,7 +991,7 @@ def test_all_zero_diff_methods_multiple_returns_tape(self): def test_all_zero_diff_methods(self): """Test that the transform works correctly when the diff method for every parameter is identified to be 0, and that no tapes were generated.""" - dev = qml.device("default.qubit", wires=3) + dev = qml.device("default.qubit", wires=4) @qml.qnode(dev) def circuit(params): @@ -1045,9 +1018,6 @@ def circuit(params): assert result[2].shape == (4,) assert np.allclose(result[2], 0) - tapes, _ = qml.gradients.hadamard_grad(circuit.tape) - assert tapes == [] - class TestHadamardTestGradDiff: """Test that the transform is differentiable""" @@ -1333,7 +1303,9 @@ def circuit(x, y): x = jax.numpy.array([0.543, -0.654]) y = jax.numpy.array(-0.123) - res = jax.jacobian(qml.gradients.hadamard_grad(circuit), argnums=argnums)(x, y) + res = jax.jacobian(qml.gradients.hadamard_grad(circuit, argnums=argnums), argnums=argnums)( + x, y + ) res_expected = jax.hessian(circuit, argnums=argnums)(x, y) if argnums == [0]: diff --git a/tests/gradients/core/test_pulse_gradient.py b/tests/gradients/core/test_pulse_gradient.py index 9f8ba10a9d3..8271996b4a8 100644 --- a/tests/gradients/core/test_pulse_gradient.py +++ b/tests/gradients/core/test_pulse_gradient.py @@ -1170,9 +1170,9 @@ def qnode(params): qnode.construct((params,), {}) num_split_times = 5 - tapes, fn = stoch_pulse_grad( - qnode.tape, argnums=[0, 1, 2], num_split_times=num_split_times, sampler_seed=7123 - ) + qnode.tape.trainable_params = [0, 1, 2] + + tapes, fn = stoch_pulse_grad(qnode.tape, num_split_times=num_split_times, sampler_seed=7123) # Two generating terms with two shifts (X_0 and Z_0), one with eight shifts # (Y_0Y_1+0.4 X_1 has eigenvalues [-1.4, -0.6, 0.6, 1.4] yielding frequencies # [0.8, 1.2, 2.0, 2.8] and hence 2 * 4 = 8 shifts) diff --git a/tests/gradients/finite_diff/test_finite_difference.py b/tests/gradients/finite_diff/test_finite_difference.py index 9d27996d802..0179a3036f6 100644 --- a/tests/gradients/finite_diff/test_finite_difference.py +++ b/tests/gradients/finite_diff/test_finite_difference.py @@ -123,7 +123,7 @@ def test_non_differentiable_error(self): with pytest.raises( ValueError, match=r"Cannot differentiate with respect to parameter\(s\) {0}" ): - finite_diff(tape, _expand=False) + finite_diff(tape) # setting trainable parameters avoids this tape.trainable_params = {1, 2} @@ -221,10 +221,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.finite_diff(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.finite_diff(circuit)(weights) @pytest.mark.torch def test_no_trainable_params_qnode_torch(self): @@ -239,10 +237,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.finite_diff(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.finite_diff(circuit)(weights) @pytest.mark.tf def test_no_trainable_params_qnode_tf(self): @@ -257,10 +253,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.finite_diff(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.finite_diff(circuit)(weights) @pytest.mark.jax def test_no_trainable_params_qnode_jax(self): @@ -275,10 +269,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.finite_diff(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.finite_diff(circuit)(weights) def test_all_zero_diff_methods(self): """Test that the transform works correctly when the diff method for every parameter is @@ -310,9 +302,6 @@ def circuit(params): assert result[2].shape == (4,) assert np.allclose(result[2], 0) - tapes, _ = qml.gradients.finite_diff(circuit.tape) - assert tapes == [] - def test_all_zero_diff_methods_multiple_returns(self): """Test that the transform works correctly when the diff method for every parameter is identified to be 0, and that no tapes were generated.""" @@ -362,9 +351,6 @@ def circuit(params): assert result[1][2].shape == (4,) assert np.allclose(result[1][2], 0) - tapes, _ = qml.gradients.finite_diff(circuit.tape) - assert tapes == [] - def test_y0(self): """Test that if first order finite differences is used, then the tape is executed only once using the current parameter @@ -460,8 +446,17 @@ def cost6(x): transform = [qml.math.shape(qml.gradients.finite_diff(c)(x)) for c in circuits] - expected = [(3,), (3,), (2, 3), (3, 4), (3, 4), (2, 3, 4)] - + expected = [ + (3,), + ( + 1, + 3, + ), + (2, 3), + (3, 4), + (1, 3, 4), + (2, 3, 4), + ] assert all(t == q for t, q in zip(transform, expected)) def test_special_observable_qnode_differentiation(self): @@ -1166,7 +1161,7 @@ def circuit(x, y): res = jax.jacobian( qml.gradients.finite_diff( - circuit, approx_order=approx_order, strategy=strategy, h=1e-5 + circuit, approx_order=approx_order, strategy=strategy, h=1e-5, argnums=argnums ), argnums=argnums, )(x, y) diff --git a/tests/gradients/finite_diff/test_finite_difference_shot_vec.py b/tests/gradients/finite_diff/test_finite_difference_shot_vec.py index 0e4dae14f70..39644a174cb 100644 --- a/tests/gradients/finite_diff/test_finite_difference_shot_vec.py +++ b/tests/gradients/finite_diff/test_finite_difference_shot_vec.py @@ -53,7 +53,7 @@ def test_non_differentiable_error(self): with pytest.raises( ValueError, match=r"Cannot differentiate with respect to parameter\(s\) {0}" ): - finite_diff(tape, _expand=False) + finite_diff(tape) # setting trainable parameters avoids this tape.trainable_params = {1, 2} @@ -162,10 +162,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.finite_diff(circuit, h=h_val)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.finite_diff(circuit, h=h_val)(weights) @pytest.mark.torch def test_no_trainable_params_qnode_torch(self): @@ -180,10 +178,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.finite_diff(circuit, h=h_val)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.finite_diff(circuit, h=h_val)(weights) @pytest.mark.tf def test_no_trainable_params_qnode_tf(self): @@ -198,10 +194,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.finite_diff(circuit, h=h_val)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.finite_diff(circuit, h=h_val)(weights) @pytest.mark.jax def test_no_trainable_params_qnode_jax(self): @@ -216,10 +210,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.finite_diff(circuit, h=h_val)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.finite_diff(circuit, h=h_val)(weights) def test_all_zero_diff_methods(self): """Test that the transform works correctly when the diff method for every parameter is @@ -253,9 +245,6 @@ def circuit(params): assert result[2].shape == (4,) assert np.allclose(result[2], 0) - tapes, _ = qml.gradients.finite_diff(circuit.tape, h=h_val) - assert tapes == [] - def test_all_zero_diff_methods_multiple_returns(self): """Test that the transform works correctly when the diff method for every parameter is identified to be 0, and that no tapes were generated.""" @@ -307,9 +296,6 @@ def circuit(params): assert result[1][2].shape == (4,) assert np.allclose(result[1][2], 0) - tapes, _ = qml.gradients.finite_diff(circuit.tape, h=h_val) - assert tapes == [] - def test_y0(self): """Test that if first order finite differences is used, then the tape is executed only once using the current parameter @@ -410,10 +396,7 @@ def cost6(x): circuits = [qml.QNode(cost, dev) for cost in (cost1, cost2, cost3, cost4, cost5, cost6)] transform = [qml.math.shape(qml.gradients.finite_diff(c, h=h_val)(x)) for c in circuits] - - expected = [(3,), (3,), (2, 3), (3, 4), (3, 4), (2, 3, 4)] - expected = [(len(many_shots_shot_vector),) + e for e in expected] - + expected = [(3, 3), (1, 3, 3), (3, 2, 3), (3, 3, 4), (1, 3, 3, 4), (3, 2, 3, 4)] assert all(t == q for t, q in zip(transform, expected)) def test_special_observable_qnode_differentiation(self): diff --git a/tests/gradients/finite_diff/test_spsa_gradient.py b/tests/gradients/finite_diff/test_spsa_gradient.py index ae73f414782..40ef891b840 100644 --- a/tests/gradients/finite_diff/test_spsa_gradient.py +++ b/tests/gradients/finite_diff/test_spsa_gradient.py @@ -194,7 +194,7 @@ def test_non_differentiable_error(self): with pytest.raises( ValueError, match=r"Cannot differentiate with respect to parameter\(s\) {0}" ): - spsa_grad(tape, _expand=False) + spsa_grad(tape) # setting trainable parameters avoids this tape.trainable_params = {1, 2} @@ -300,10 +300,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = spsa_grad(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + spsa_grad(circuit)(weights) @pytest.mark.torch def test_no_trainable_params_qnode_torch(self): @@ -318,10 +316,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = spsa_grad(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + spsa_grad(circuit)(weights) @pytest.mark.tf def test_no_trainable_params_qnode_tf(self): @@ -336,10 +332,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = spsa_grad(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + spsa_grad(circuit)(weights) @pytest.mark.jax def test_no_trainable_params_qnode_jax(self): @@ -354,10 +348,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = spsa_grad(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + spsa_grad(circuit)(weights) def test_all_zero_diff_methods(self): """Test that the transform works correctly when the diff method for every parameter is @@ -389,9 +381,6 @@ def circuit(params): assert result[2].shape == (4,) assert np.allclose(result[2], 0) - tapes, _ = spsa_grad(circuit.tape) - assert tapes == [] - def test_all_zero_diff_methods_multiple_returns(self): """Test that the transform works correctly when the diff method for every parameter is identified to be 0, and that no tapes were generated, with multiple return values.""" @@ -441,9 +430,6 @@ def circuit(params): assert result[1][2].shape == (4,) assert np.allclose(result[1][2], 0) - tapes, _ = spsa_grad(circuit.tape) - assert tapes == [] - def test_y0(self): """Test that if first order finite differences is underlying the SPSA, then the tape is executed only once using the current parameter values.""" @@ -549,7 +535,17 @@ def cost6(x): transform = [qml.math.shape(spsa_grad(c)(x)) for c in circuits] - expected = [(3,), (3,), (2, 3), (3, 4), (3, 4), (2, 3, 4)] + expected = [ + (3,), + ( + 1, + 3, + ), + (2, 3), + (3, 4), + (1, 3, 4), + (2, 3, 4), + ] assert all(t == q for t, q in zip(transform, expected)) diff --git a/tests/gradients/finite_diff/test_spsa_gradient_shot_vec.py b/tests/gradients/finite_diff/test_spsa_gradient_shot_vec.py index 02488c47709..920895d859d 100644 --- a/tests/gradients/finite_diff/test_spsa_gradient_shot_vec.py +++ b/tests/gradients/finite_diff/test_spsa_gradient_shot_vec.py @@ -64,7 +64,7 @@ def test_non_differentiable_error(self): with pytest.raises( ValueError, match=r"Cannot differentiate with respect to parameter\(s\) {0}" ): - spsa_grad(tape, _expand=False) + spsa_grad(tape) # setting trainable parameters avoids this tape.trainable_params = {1, 2} @@ -183,10 +183,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = spsa_grad(circuit, h=h_val)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + spsa_grad(circuit, h=h_val)(weights) @pytest.mark.torch def test_no_trainable_params_qnode_torch(self): @@ -201,10 +199,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = spsa_grad(circuit, h=h_val)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + spsa_grad(circuit, h=h_val)(weights) @pytest.mark.tf def test_no_trainable_params_qnode_tf(self): @@ -219,10 +215,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = spsa_grad(circuit, h=h_val)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + spsa_grad(circuit, h=h_val)(weights) @pytest.mark.jax def test_no_trainable_params_qnode_jax(self): @@ -237,10 +231,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = spsa_grad(circuit, h=h_val)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + spsa_grad(circuit, h=h_val)(weights) def test_all_zero_diff_methods(self): """Test that the transform works correctly when the diff method for every parameter is @@ -277,9 +269,6 @@ def circuit(params): assert result[2].shape == (4,) assert np.allclose(result[2], 0) - tapes, _ = spsa_grad(circuit.tape, h=h_val) - assert tapes == [] - def test_all_zero_diff_methods_multiple_returns(self): """Test that the transform works correctly when the diff method for every parameter is identified to be 0, and that no tapes were generated.""" @@ -334,9 +323,6 @@ def circuit(params): assert result[1][2].shape == (4,) assert np.allclose(result[1][2], 0) - tapes, _ = spsa_grad(circuit.tape, h=h_val) - assert tapes == [] - def test_y0(self): """Test that if first order finite differences is underlying the SPSA, then the tape is executed only once using the current parameter @@ -473,8 +459,7 @@ def cost6(x): transform = [qml.math.shape(spsa_grad(c, h=h_val)(x)) for c in circuits] - expected = [(3,), (3,), (2, 3), (3, 4), (3, 4), (2, 3, 4)] - expected = [(len(many_shots_shot_vector),) + e for e in expected] + expected = [(3, 3), (1, 3, 3), (3, 2, 3), (3, 3, 4), (1, 3, 3, 4), (3, 2, 3, 4)] assert all(t == q for t, q in zip(transform, expected)) diff --git a/tests/gradients/parameter_shift/test_cv_gradients.py b/tests/gradients/parameter_shift/test_cv_gradients.py index bb136886926..17363621898 100644 --- a/tests/gradients/parameter_shift/test_cv_gradients.py +++ b/tests/gradients/parameter_shift/test_cv_gradients.py @@ -22,7 +22,6 @@ import pennylane.numpy as anp # only to be used inside classical computational nodes import pennylane as qml - alpha = 0.5 # displacement in tests hbar = 2 mag_alphas = np.linspace(0, 1.5, 5) @@ -249,11 +248,14 @@ def qf(x, y): assert qml.math.allclose(grad_A, grad_F, atol=tol, rtol=0) assert qml.math.allclose(grad_A2, grad_F, atol=tol, rtol=0) - @pytest.mark.autograd + @pytest.mark.jax def test_cv_gradients_parameters_inside_array(self, gaussian_dev, tol): "Tests that free parameters inside an array passed to an Operation yield correct gradients." - par = anp.array([0.4, 1.3], requires_grad=True) + import jax + par = jax.numpy.array([0.4, 1.3]) + + @qml.qnode(device=gaussian_dev, diff_method="finite-diff") def qf(x, y): qml.Displacement(0.5, 0, wires=[0]) qml.Squeezing(x, 0, wires=[0]) @@ -263,12 +265,19 @@ def qf(x, y): M[2, 1] = 1.0 return qml.expval(qml.PolyXP(M, [0, 1])) - q = qml.QNode(qf, gaussian_dev) - q(*par) - grad_F = qml.gradients.finite_diff(q, hybrid=False)(*par) - grad_A2 = qml.gradients.param_shift_cv( - q, dev=gaussian_dev, force_order2=True, hybrid=False - )(*par) + grad_F = jax.grad(qf)(*par) + + @qml.qnode(device=gaussian_dev, diff_method="parameter-shift", force_order2=True) + def qf2(x, y): + qml.Displacement(0.5, 0, wires=[0]) + qml.Squeezing(x, 0, wires=[0]) + M = np.zeros((5, 5)) + M[1, 1] = y + M[1, 2] = 1.0 + M[2, 1] = 1.0 + return qml.expval(qml.PolyXP(M, [0, 1])) + + grad_A2 = jax.grad(qf2)(*par) # the different methods agree assert grad_A2 == pytest.approx(grad_F, abs=tol) diff --git a/tests/gradients/parameter_shift/test_parameter_shift.py b/tests/gradients/parameter_shift/test_parameter_shift.py index 6a499d828b7..d4feab4d48f 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift.py +++ b/tests/gradients/parameter_shift/test_parameter_shift.py @@ -379,87 +379,6 @@ def test_independent_parameter(self, mocker): # only called for parameter 0 assert spy.call_args[0][0:2] == (tape, [0]) - # TODO: uncomment when QNode decorator uses new qml.execute pipeline - # @pytest.mark.autograd - # def test_no_trainable_params_qnode_autograd(self, mocker): - # """Test that the correct ouput and warning is generated in the absence of any trainable - # parameters""" - # dev = qml.device("default.qubit", wires=2) - # spy = mocker.spy(dev, "expval") - - # @qml.qnode(dev, interface="autograd") - # def circuit(weights): - # qml.RX(weights[0], wires=0) - # qml.RY(weights[1], wires=0) - # return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - # weights = [0.1, 0.2] - # with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - # res = qml.gradients.param_shift(circuit)(weights) - - # assert res == () - # spy.assert_not_called() - - # @pytest.mark.torch - # def test_no_trainable_params_qnode_torch(self, mocker): - # """Test that the correct ouput and warning is generated in the absence of any trainable - # parameters""" - # dev = qml.device("default.qubit", wires=2) - # spy = mocker.spy(dev, "expval") - - # @qml.qnode(dev, interface="torch") - # def circuit(weights): - # qml.RX(weights[0], wires=0) - # qml.RY(weights[1], wires=0) - # return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - # weights = [0.1, 0.2] - # with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - # res = qml.gradients.param_shift(circuit)(weights) - - # assert res == () - # spy.assert_not_called() - - # @pytest.mark.tf - # def test_no_trainable_params_qnode_tf(self, mocker): - # """Test that the correct ouput and warning is generated in the absence of any trainable - # parameters""" - # dev = qml.device("default.qubit", wires=2) - # spy = mocker.spy(dev, "expval") - - # @qml.qnode(dev, interface="tf") - # def circuit(weights): - # qml.RX(weights[0], wires=0) - # qml.RY(weights[1], wires=0) - # return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - # weights = [0.1, 0.2] - # with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - # res = qml.gradients.param_shift(circuit)(weights) - - # assert res == () - # spy.assert_not_called() - - # @pytest.mark.jax - # def test_no_trainable_params_qnode_jax(self, mocker): - # """Test that the correct ouput and warning is generated in the absence of any trainable - # parameters""" - # dev = qml.device("default.qubit", wires=2) - # spy = mocker.spy(dev, "expval") - - # @qml.qnode(dev, interface="jax") - # def circuit(weights): - # qml.RX(weights[0], wires=0) - # qml.RY(weights[1], wires=0) - # return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - # weights = [0.1, 0.2] - # with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - # res = qml.gradients.param_shift(circuit)(weights) - - # assert res == () - # spy.assert_not_called() - @pytest.mark.parametrize("broadcast", [True, False]) def test_no_trainable_params_tape(self, broadcast): """Test that the correct ouput and warning is generated in the absence of any trainable @@ -2293,7 +2212,7 @@ def cost6(x): costs_and_expected_expval = [ (cost1, [3]), - (cost2, [3]), + (cost2, [1, 3]), (cost3, [2, 3]), ] @@ -2306,7 +2225,7 @@ def test_output_shape_matches_qnode_expval(self, cost, expected_shape): circuit = qml.QNode(cost, dev) res = qml.gradients.param_shift(circuit)(x) - assert isinstance(res, tuple) + assert len(res) == expected_shape[0] if len(expected_shape) > 1: @@ -2316,7 +2235,7 @@ def test_output_shape_matches_qnode_expval(self, cost, expected_shape): costs_and_expected_probs = [ (cost4, [3, 4]), - (cost5, [3, 4]), + (cost5, [1, 3, 4]), (cost6, [2, 3, 4]), ] @@ -2329,7 +2248,7 @@ def test_output_shape_matches_qnode_probs(self, cost, expected_shape): circuit = qml.QNode(cost, dev) res = qml.gradients.param_shift(circuit)(x) - assert isinstance(res, tuple) + assert len(res) == expected_shape[0] if len(expected_shape) > 2: @@ -3106,7 +3025,7 @@ def cost6(x): single_measure_circuits = [qml.QNode(cost, dev) for cost in (cost1, cost2, cost4, cost5)] multi_measure_circuits = [qml.QNode(cost, dev) for cost in (cost3, cost6)] - for c, exp_shape in zip(single_measure_circuits, [(3,), (3,), (3, 4), (3, 4)]): + for c, exp_shape in zip(single_measure_circuits, [(3,), (1, 3), (3, 4), (1, 3, 4)]): grad = qml.gradients.param_shift(c, broadcast=True)(x) assert qml.math.shape(grad) == exp_shape @@ -4072,7 +3991,6 @@ def circuit(x): res = qml.gradients.param_shift(circuit)(x) res_expected = jax.jacobian(circuit)(x) - assert res.shape == res_expected.shape assert np.allclose(res, res_expected) @@ -4656,7 +4574,9 @@ def circuit(x, y): x = jax.numpy.array([0.543, -0.654]) y = jax.numpy.array(-0.123) - res = jax.jacobian(qml.gradients.param_shift(circuit), argnums=argnums)(x, y) + res = jax.jacobian(qml.gradients.param_shift(circuit, argnums=argnums), argnums=argnums)( + x, y + ) res_expected = jax.hessian(circuit, argnums=argnums)(x, y) if argnums == [0]: diff --git a/tests/gradients/parameter_shift/test_parameter_shift_cv.py b/tests/gradients/parameter_shift/test_parameter_shift_cv.py index 1c622a61133..cc76c318fbd 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift_cv.py +++ b/tests/gradients/parameter_shift/test_parameter_shift_cv.py @@ -298,10 +298,8 @@ def circuit(weights): return qml.expval(qml.QuadX(0)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.param_shift_cv(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.param_shift_cv(circuit)(weights) @pytest.mark.torch def test_no_trainable_params_qnode_torch(self): @@ -317,10 +315,8 @@ def circuit(weights): return qml.expval(qml.QuadX(0)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.param_shift_cv(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.param_shift_cv(circuit)(weights) @pytest.mark.tf def test_no_trainable_params_qnode_tf(self): @@ -336,10 +332,8 @@ def circuit(weights): return qml.expval(qml.QuadX(0)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.param_shift_cv(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.param_shift_cv(circuit)(weights) @pytest.mark.jax def test_no_trainable_params_qnode_jax(self): @@ -355,10 +349,8 @@ def circuit(weights): return qml.expval(qml.QuadX(0)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - res = qml.gradients.param_shift_cv(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.gradients.param_shift_cv(circuit)(weights) def test_no_trainable_params_tape(self): """Test that the correct ouput and warning is generated in the absence of any trainable @@ -397,9 +389,6 @@ def circuit(params): result = qml.gradients.param_shift_cv(circuit, dev)(params) assert np.allclose(result, np.zeros((2, 3)), atol=0, rtol=0) - tapes, _ = qml.gradients.param_shift_cv(circuit.tape, dev) - assert tapes == [] - def test_state_non_differentiable_error(self): """Test error raised if attempting to differentiate with respect to a state""" diff --git a/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py b/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py index ed9900ca8a2..f9ab005ca03 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py +++ b/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py @@ -100,87 +100,6 @@ def test_independent_parameter(self, mocker): # only called for parameter 0 assert spy.call_args[0][0:2] == (tape, [0]) - # TODO: uncomment and port to shot-vectors when QNode decorator uses new qml.execute pipeline - # @pytest.mark.autograd - # def test_no_trainable_params_qnode_autograd(self, mocker): - # """Test that the correct ouput and warning is generated in the absence of any trainable - # parameters""" - # dev = qml.device("default.qubit", wires=2, shots=default_shot_vector) - # spy = mocker.spy(dev, "expval") - - # @qml.qnode(dev, interface="autograd") - # def circuit(weights): - # qml.RX(weights[0], wires=0) - # qml.RY(weights[1], wires=0) - # return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - # weights = [0.1, 0.2] - # with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - # res = qml.gradients.param_shift(circuit)(weights) - - # assert res == () - # spy.assert_not_called() - - # @pytest.mark.torch - # def test_no_trainable_params_qnode_torch(self, mocker): - # """Test that the correct ouput and warning is generated in the absence of any trainable - # parameters""" - # dev = qml.device("default.qubit", wires=2, shots=default_shot_vector) - # spy = mocker.spy(dev, "expval") - - # @qml.qnode(dev, interface="torch") - # def circuit(weights): - # qml.RX(weights[0], wires=0) - # qml.RY(weights[1], wires=0) - # return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - # weights = [0.1, 0.2] - # with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - # res = qml.gradients.param_shift(circuit)(weights) - - # assert res == () - # spy.assert_not_called() - - # @pytest.mark.tf - # def test_no_trainable_params_qnode_tf(self, mocker): - # """Test that the correct ouput and warning is generated in the absence of any trainable - # parameters""" - # dev = qml.device("default.qubit", wires=2, shots=default_shot_vector) - # spy = mocker.spy(dev, "expval") - - # @qml.qnode(dev, interface="tf") - # def circuit(weights): - # qml.RX(weights[0], wires=0) - # qml.RY(weights[1], wires=0) - # return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - # weights = [0.1, 0.2] - # with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - # res = qml.gradients.param_shift(circuit)(weights) - - # assert res == () - # spy.assert_not_called() - - # @pytest.mark.jax - # def test_no_trainable_params_qnode_jax(self, mocker): - # """Test that the correct ouput and warning is generated in the absence of any trainable - # parameters""" - # dev = qml.device("default.qubit", wires=2, shots=default_shot_vector) - # spy = mocker.spy(dev, "expval") - - # @qml.qnode(dev, interface="jax") - # def circuit(weights): - # qml.RX(weights[0], wires=0) - # qml.RY(weights[1], wires=0) - # return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - # weights = [0.1, 0.2] - # with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"): - # res = qml.gradients.param_shift(circuit)(weights) - - # assert res == () - # spy.assert_not_called() - @pytest.mark.parametrize("broadcast", [True, False]) def test_no_trainable_params_tape(self, broadcast): """Test that the correct ouput and warning is generated in the absence of any trainable diff --git a/tests/interfaces/default_qubit_2_integration/test_autograd_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_autograd_default_qubit_2.py index 16b9aa77c2a..09d20d1b028 100644 --- a/tests/interfaces/default_qubit_2_integration/test_autograd_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_autograd_default_qubit_2.py @@ -442,7 +442,21 @@ def cost_fn(a, p): tape = qml.tape.QuantumScript( [qml.RX(a, wires=0), U3(*p, wires=0)], [qml.expval(qml.PauliX(0))] ) - return execute([tape], device, **execute_kwargs)[0] + gradient_fn = execute_kwargs["gradient_fn"] + + if gradient_fn is None: + _gradient_method = None + elif isinstance(gradient_fn, str): + _gradient_method = gradient_fn + else: + _gradient_method = "gradient-transform" + config = qml.devices.ExecutionConfig( + interface="autograd", + gradient_method=_gradient_method, + grad_on_execution=execute_kwargs.get("grad_on_execution", None), + ) + program, _ = device.preprocess(execution_config=config) + return execute([tape], device, **execute_kwargs, transform_program=program)[0] a = np.array(0.1, requires_grad=False) p = np.array([0.1, 0.2, 0.3], requires_grad=True) diff --git a/tests/interfaces/default_qubit_2_integration/test_autograd_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_autograd_qnode_default_qubit_2.py index 3463e581096..33e26ea1c77 100644 --- a/tests/interfaces/default_qubit_2_integration/test_autograd_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_autograd_qnode_default_qubit_2.py @@ -106,22 +106,15 @@ def circuit(a): assert isinstance(grad, float) assert grad.shape == tuple() - def test_jacobian(self, interface, dev, diff_method, grad_on_execution, mocker, tol): + def test_jacobian(self, interface, dev, diff_method, grad_on_execution, tol): """Test jacobian calculation""" kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution ) - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) tol = TOL_FOR_SPSA - elif diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") a = np.array(0.1, requires_grad=True) b = np.array(0.2, requires_grad=True) @@ -156,27 +149,14 @@ def cost(x, y): assert res[1].shape == (2,) assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - - def test_jacobian_no_evaluate( - self, interface, dev, diff_method, grad_on_execution, mocker, tol - ): + def test_jacobian_no_evaluate(self, interface, dev, diff_method, grad_on_execution, tol): """Test jacobian calculation when no prior circuit evaluation has been performed""" kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution ) - - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) tol = TOL_FOR_SPSA - elif diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") a = np.array(0.1, requires_grad=True) b = np.array(0.2, requires_grad=True) @@ -197,26 +177,20 @@ def cost(x, y): assert np.allclose(res[0], expected[0], atol=tol, rtol=0) assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - # call the Jacobian with new parameters a = np.array(0.6, requires_grad=True) b = np.array(0.832, requires_grad=True) res = jac_fn(a, b) expected = ([-np.sin(a), np.sin(a) * np.sin(b)], [0, -np.cos(a) * np.cos(b)]) - expected = ([-np.sin(a), np.sin(a) * np.sin(b)], [0, -np.cos(a) * np.cos(b)]) assert np.allclose(res[0], expected[0], atol=tol, rtol=0) assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - def test_jacobian_options(self, interface, dev, diff_method, grad_on_execution, mocker): + def test_jacobian_options(self, interface, dev, diff_method, grad_on_execution): """Test setting jacobian options""" if diff_method == "backprop": pytest.skip("Test does not support backprop") - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - a = np.array([0.1, 0.2], requires_grad=True) @qnode(dev, interface=interface, h=1e-8, order=2, grad_on_execution=grad_on_execution) @@ -227,13 +201,7 @@ def circuit(a): qml.jacobian(circuit)(a) - for args in spy.call_args_list: - assert args[1]["order"] == 2 - assert args[1]["h"] == 1e-8 - - def test_changing_trainability( - self, interface, dev, diff_method, grad_on_execution, mocker, tol - ): + def test_changing_trainability(self, interface, dev, diff_method, grad_on_execution, tol): """Test changing the trainability of parameters changes the number of differentiation requests made""" if diff_method != "parameter-shift": @@ -258,7 +226,6 @@ def loss(a, b): return np.sum(autograd.numpy.hstack(circuit(a, b))) grad_fn = qml.grad(loss) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") res = grad_fn(a, b) # the tape has reported both arguments as trainable @@ -267,9 +234,6 @@ def loss(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called for each argument - assert len(spy.spy_return[0]) == 4 - # make the second QNode argument a constant a = np.array(0.54, requires_grad=True) b = np.array(0.8, requires_grad=False) @@ -282,9 +246,6 @@ def loss(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called only once - assert len(spy.spy_return[0]) == 2 - # trainability also updates on evaluation a = np.array(0.54, requires_grad=False) b = np.array(0.8, requires_grad=True) @@ -1124,7 +1085,7 @@ def cost_fn(x): assert np.allclose(hess, expected_hess, atol=tol, rtol=0) def test_hessian_vector_valued_separate_args( - self, interface, dev, diff_method, grad_on_execution, mocker, tol + self, interface, dev, diff_method, grad_on_execution, tol ): """Test hessian calculation of a vector valued QNode that has separate input arguments""" if diff_method not in {"parameter-shift", "backprop"}: @@ -1165,8 +1126,6 @@ def circuit(a, b): assert g[1].shape == (2,) assert np.allclose(g[1], expected_g[1], atol=tol, rtol=0) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - def jac_fn_a(*args): return jac_fn(*args)[0] @@ -1178,11 +1137,6 @@ def jac_fn_b(*args): assert isinstance(hess_a, tuple) and len(hess_a) == 2 assert isinstance(hess_b, tuple) and len(hess_b) == 2 - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called() - exp_hess_a = ( [-0.5 * np.cos(a) * np.cos(b), 0.5 * np.cos(a) * np.cos(b)], [0.5 * np.sin(a) * np.sin(b), -0.5 * np.sin(a) * np.sin(b)], @@ -1355,9 +1309,7 @@ class TestTapeExpansion: with the Autograd interface""" @pytest.mark.parametrize("max_diff", [1, 2]) - def test_gradient_expansion_trainable_only( - self, dev, diff_method, grad_on_execution, max_diff, mocker - ): + def test_gradient_expansion_trainable_only(self, dev, diff_method, grad_on_execution, max_diff): """Test that a *supported* operation with no gradient recipe is only expanded for parameter-shift and finite-differences when it is trainable.""" if diff_method not in ("parameter-shift", "finite-diff", "spsa", "hadamard"): @@ -1385,16 +1337,8 @@ def circuit(x, y): y = np.array(0.7, requires_grad=False) circuit(x, y) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") _ = qml.grad(circuit)(x, y) - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 3 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - assert input_tape.operations[2].name == "PhaseShift" - assert input_tape.operations[2].grad_method is None - @pytest.mark.parametrize("max_diff", [1, 2]) def test_hamiltonian_expansion_analytic( self, dev, diff_method, grad_on_execution, max_diff, tol @@ -1878,7 +1822,6 @@ def cost(x, y): return anp.hstack(qml.grad(circuit)(x, y)) hess = qml.jacobian(cost)(par_0, par_1) - print(hess) assert isinstance(hess, tuple) assert len(hess) == 2 diff --git a/tests/interfaces/default_qubit_2_integration/test_execute_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_execute_default_qubit_2.py index d0cde46aaf2..72578467152 100644 --- a/tests/interfaces/default_qubit_2_integration/test_execute_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_execute_default_qubit_2.py @@ -64,7 +64,10 @@ def decomposition(self): qs = qml.tape.QuantumScript([CustomOp(0)], [qml.expval(qml.PauliZ(0))]) with pytest.warns(UserWarning, match="device batch transforms cannot be turned off"): - qml.execute((qs, qs), device=dev, device_batch_transform=False) + program, _ = dev.preprocess() + qml.execute( + (qs, qs), device=dev, device_batch_transform=False, transform_program=program + ) def test_split_and_expand_performed(self): """Test that preprocess returns the correct tapes when splitting and expanding @@ -134,7 +137,8 @@ def decomposition(self): qs = qml.tape.QuantumScript([CustomOp(0)], [qml.expval(qml.PauliZ(0))]) with pytest.warns(UserWarning, match="device batch transforms cannot be turned off"): - results = qml.execute([qs], dev, device_batch_transform=False) + program, _ = dev.preprocess() + results = qml.execute([qs], dev, device_batch_transform=False, transform_program=program) assert len(results) == 1 assert qml.math.allclose(results[0], -1) @@ -157,8 +161,8 @@ def test_caching(gradient_fn): assert len(cache) == 1 assert cache[qs.hash] == -1.0 - assert results == (-1.0, -1.0) - assert results2 == (-1.0, -1.0) + assert results == [-1.0, -1.0] + assert results2 == [-1.0, -1.0] assert tracker.totals["batches"] == 1 assert tracker.totals["executions"] == 1 diff --git a/tests/interfaces/default_qubit_2_integration/test_jax_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_jax_default_qubit_2.py index 249794ee193..5d92ea5b92f 100644 --- a/tests/interfaces/default_qubit_2_integration/test_jax_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_jax_default_qubit_2.py @@ -416,7 +416,20 @@ def cost_fn(a, p): [qml.expval(qml.PauliX(0))], shots=shots, ) - return execute([tape], device, **execute_kwargs)[0] + gradient_fn = execute_kwargs["gradient_fn"] + if gradient_fn is None: + _gradient_method = None + elif isinstance(gradient_fn, str): + _gradient_method = gradient_fn + else: + _gradient_method = "gradient-transform" + conf = qml.devices.ExecutionConfig( + interface="autograd", + gradient_method=_gradient_method, + grad_on_execution=execute_kwargs.get("grad_on_execution", None), + ) + program, _ = device.preprocess(execution_config=conf) + return execute([tape], device, **execute_kwargs, transform_program=program)[0] a = jnp.array(0.1) p = jnp.array([0.1, 0.2, 0.3]) diff --git a/tests/interfaces/default_qubit_2_integration/test_jax_jit_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_jax_jit_qnode_default_qubit_2.py index 22b3bb88f37..2dd5b63f3f5 100644 --- a/tests/interfaces/default_qubit_2_integration/test_jax_jit_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_jax_jit_qnode_default_qubit_2.py @@ -78,9 +78,7 @@ def circuit(a): assert isinstance(grad, jax.Array) assert grad.shape == () - def test_changing_trainability( - self, dev, diff_method, grad_on_execution, interface, mocker, tol - ): + def test_changing_trainability(self, dev, diff_method, grad_on_execution, interface, tol): """Test changing the trainability of parameters changes the number of differentiation requests made""" if diff_method != "parameter-shift": @@ -102,7 +100,6 @@ def circuit(a, b): return qml.expval(qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliY(1)])) grad_fn = jax.jit(jax.grad(circuit, argnums=[0, 1])) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") res = grad_fn(a, b) # the tape has reported both arguments as trainable @@ -111,9 +108,6 @@ def circuit(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called for each argument - assert len(spy.spy_return[0]) == 4 - # make the second QNode argument a constant grad_fn = jax.grad(circuit, argnums=0) res = grad_fn(a, b) @@ -124,9 +118,6 @@ def circuit(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called only once - assert len(spy.spy_return[0]) == 2 - # trainability also updates on evaluation a = np.array(0.54, requires_grad=False) b = np.array(0.8, requires_grad=True) @@ -228,13 +219,10 @@ def circuit(a, p): ) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_jacobian_options(self, dev, diff_method, grad_on_execution, interface, mocker): + def test_jacobian_options(self, dev, diff_method, grad_on_execution, interface): """Test setting jacobian options""" if diff_method != "finite-diff": pytest.skip("Test only applies to finite diff.") - - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - a = np.array([0.1, 0.2], requires_grad=True) @qnode( @@ -256,10 +244,6 @@ def circuit(a): jax.jit(jax.jacobian(circuit))(a) - for args in spy.call_args_list: - assert args[1]["approx_order"] == 2 - assert args[1]["h"] == 1e-8 - @pytest.mark.parametrize( "interface,dev,diff_method,grad_on_execution", interface_and_qubit_device_and_diff_method @@ -268,21 +252,15 @@ class TestVectorValuedQNode: """Test that using vector-valued QNodes with JAX integrate with the PennyLane stack""" - def test_diff_expval_expval(self, dev, diff_method, grad_on_execution, interface, mocker, tol): + def test_diff_expval_expval(self, dev, diff_method, grad_on_execution, interface, tol): """Test jacobian calculation""" gradient_kwargs = {} - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + + if diff_method == "spsa": gradient_kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) gradient_kwargs["num_directions"] = 20 tol = TOL_FOR_SPSA - elif diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") a = np.array(0.1, requires_grad=True) b = np.array(0.2, requires_grad=True) @@ -333,26 +311,15 @@ def circuit(a, b): assert res[1][1].shape == () assert np.allclose(res[1][1], expected[1][1], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff"): - spy.assert_called() - - def test_jacobian_no_evaluate( - self, dev, diff_method, grad_on_execution, interface, mocker, tol - ): + def test_jacobian_no_evaluate(self, dev, diff_method, grad_on_execution, interface, tol): """Test jacobian calculation when no prior circuit evaluation has been performed""" gradient_kwargs = {} - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + + if diff_method == "spsa": gradient_kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) gradient_kwargs["num_directions"] = 20 tol = TOL_FOR_SPSA - elif diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") a = jax.numpy.array(0.1) b = jax.numpy.array(0.2) @@ -385,9 +352,6 @@ def circuit(a, b): assert r.shape == () assert np.allclose(r, e, atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - # call the Jacobian with new parameters a = jax.numpy.array(0.6) b = jax.numpy.array(0.832) @@ -1146,7 +1110,7 @@ def cost_fn(x): assert np.allclose(hess, expected_hess, atol=tol, rtol=0) def test_hessian_vector_valued_separate_args( - self, dev, diff_method, grad_on_execution, interface, mocker, tol + self, dev, diff_method, grad_on_execution, interface, tol ): """Test hessian calculation of a vector valued QNode that has separate input arguments""" gradient_kwargs = {} @@ -1191,14 +1155,8 @@ def circuit(a, b): ) assert np.allclose(g, expected_g.T, atol=tol, rtol=0) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") hess = jax.jit(jax.jacobian(jac_fn, argnums=[0, 1]))(a, b) - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called() - expected_hess = np.array( [ [ @@ -1301,7 +1259,7 @@ class TestTapeExpansion: @pytest.mark.parametrize("max_diff", [1, 2]) def test_gradient_expansion_trainable_only( - self, dev, diff_method, grad_on_execution, max_diff, interface, mocker + self, dev, diff_method, grad_on_execution, max_diff, interface ): """Test that a *supported* operation with no gradient recipe is only expanded for parameter-shift and finite-differences when it is trainable.""" @@ -1331,16 +1289,8 @@ def circuit(x, y): y = jax.numpy.array(0.7) circuit(x, y) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") jax.grad(circuit, argnums=[0])(x, y) - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 3 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - assert input_tape.operations[2].name == "PhaseShift" - assert input_tape.operations[2].grad_method is None - @pytest.mark.parametrize("max_diff", [1, 2]) def test_hamiltonian_expansion_analytic( self, dev, diff_method, grad_on_execution, max_diff, interface, mocker, tol diff --git a/tests/interfaces/default_qubit_2_integration/test_jax_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_jax_qnode_default_qubit_2.py index 0a9500c6b9d..fb8de4444a6 100644 --- a/tests/interfaces/default_qubit_2_integration/test_jax_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_jax_qnode_default_qubit_2.py @@ -86,7 +86,7 @@ def circuit(a): assert grad.shape == () def test_changing_trainability( - self, dev, diff_method, grad_on_execution, interface, mocker, tol + self, dev, diff_method, grad_on_execution, interface, tol ): # pylint:disable=unused-argument """Test changing the trainability of parameters changes the number of differentiation requests made""" @@ -104,7 +104,6 @@ def circuit(a, b): return qml.expval(qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliY(1)])) grad_fn = jax.grad(circuit, argnums=[0, 1]) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") res = grad_fn(a, b) # the tape has reported both arguments as trainable @@ -113,9 +112,6 @@ def circuit(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called for each argument - assert len(spy.spy_return[0]) == 4 - # make the second QNode argument a constant grad_fn = jax.grad(circuit, argnums=0) res = grad_fn(a, b) @@ -126,9 +122,6 @@ def circuit(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called only once - assert len(spy.spy_return[0]) == 2 - def test_classical_processing(self, dev, diff_method, grad_on_execution, interface): """Test classical processing within the quantum tape""" a = jax.numpy.array(0.1) @@ -221,14 +214,12 @@ def circuit(a, p): assert np.allclose(res, expected, atol=tol, rtol=0) def test_jacobian_options( - self, dev, diff_method, grad_on_execution, interface, mocker + self, dev, diff_method, grad_on_execution, interface ): # pylint:disable=unused-argument """Test setting jacobian options""" if diff_method != "finite-diff": pytest.skip("Test only applies to finite diff.") - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - a = jax.numpy.array([0.1, 0.2]) @qnode(dev, interface=interface, diff_method="finite-diff", h=1e-8, approx_order=2) @@ -239,10 +230,6 @@ def circuit(a): jax.jacobian(circuit)(a) - for args in spy.call_args_list: - assert args[1]["approx_order"] == 2 - assert args[1]["h"] == 1e-8 - @pytest.mark.parametrize( "interface,dev,diff_method,grad_on_execution", interface_and_device_and_diff_method @@ -251,18 +238,13 @@ class TestVectorValuedQNode: """Test that using vector-valued QNodes with JAX integrate with the PennyLane stack""" - def test_diff_expval_expval(self, dev, diff_method, grad_on_execution, interface, mocker, tol): + def test_diff_expval_expval(self, dev, diff_method, grad_on_execution, interface, tol): """Test jacobian calculation""" kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution ) - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) tol = TOL_FOR_SPSA @@ -308,23 +290,13 @@ def circuit(a, b): assert res[1][1].shape == () assert np.allclose(res[1][1], expected[1][1], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff"): - spy.assert_called() - - def test_jacobian_no_evaluate( - self, dev, diff_method, grad_on_execution, interface, mocker, tol - ): + def test_jacobian_no_evaluate(self, dev, diff_method, grad_on_execution, interface, tol): """Test jacobian calculation when no prior circuit evaluation has been performed""" kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution ) - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) tol = TOL_FOR_SPSA @@ -351,9 +323,6 @@ def circuit(a, b): assert res[i][j].shape == () assert np.allclose(res[i][j], expected[i][j], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - # call the Jacobian with new parameters a = jax.numpy.array(0.6) b = jax.numpy.array(0.832) @@ -1068,7 +1037,7 @@ def cost_fn(x): assert np.allclose(hess, expected_hess, atol=tol, rtol=0) def test_hessian_vector_valued_separate_args( - self, dev, diff_method, grad_on_execution, interface, mocker, tol + self, dev, diff_method, grad_on_execution, interface, tol ): """Test hessian calculation of a vector valued QNode that has separate input arguments""" gradient_kwargs = {} @@ -1113,14 +1082,8 @@ def circuit(a, b): ) assert np.allclose(g, expected_g.T, atol=tol, rtol=0) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") hess = jax.jacobian(jac_fn, argnums=[0, 1])(a, b) - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called() - expected_hess = np.array( [ [ @@ -1221,7 +1184,7 @@ class TestTapeExpansion: @pytest.mark.parametrize("max_diff", [1, 2]) def test_gradient_expansion_trainable_only( - self, dev, diff_method, grad_on_execution, max_diff, interface, mocker + self, dev, diff_method, grad_on_execution, max_diff, interface ): """Test that a *supported* operation with no gradient recipe is only expanded for parameter-shift and finite-differences when it is trainable.""" @@ -1251,16 +1214,8 @@ def circuit(x, y): y = jax.numpy.array(0.7) circuit(x, y) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") jax.grad(circuit, argnums=[0])(x, y) - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 3 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - assert input_tape.operations[2].name == "PhaseShift" - assert input_tape.operations[2].grad_method is None - @pytest.mark.parametrize("max_diff", [1, 2]) def test_hamiltonian_expansion_analytic( self, dev, diff_method, grad_on_execution, max_diff, interface, mocker, tol diff --git a/tests/interfaces/default_qubit_2_integration/test_tensorflow_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_tensorflow_default_qubit_2.py index 0312d5875ef..5e3eb211b25 100644 --- a/tests/interfaces/default_qubit_2_integration/test_tensorflow_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_tensorflow_default_qubit_2.py @@ -456,7 +456,20 @@ def cost_fn(a, p): tape = qml.tape.QuantumScript( [qml.RX(a, wires=0), U3(*p, wires=0)], [qml.expval(qml.PauliX(0))] ) - return execute([tape], device, **execute_kwargs)[0] + gradient_fn = execute_kwargs["gradient_fn"] + if gradient_fn is None: + _gradient_method = None + elif isinstance(gradient_fn, str): + _gradient_method = gradient_fn + else: + _gradient_method = "gradient-transform" + config = qml.devices.ExecutionConfig( + interface="autograd", + gradient_method=_gradient_method, + grad_on_execution=execute_kwargs.get("grad_on_execution", None), + ) + program, _ = device.preprocess(execution_config=config) + return execute([tape], device, **execute_kwargs, transform_program=program)[0] a = tf.constant(0.1) p = tf.Variable([0.1, 0.2, 0.3]) diff --git a/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py index b894251fa5f..6a8ea43db89 100644 --- a/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py @@ -141,23 +141,15 @@ def circuit(p1, p2=y, **kwargs): expected = "0: ──RX(0.10)──RX(0.40)─╭●─┤ State\n1: ──RY(0.06)───────────╰X─┤ State" assert result == expected - def test_jacobian(self, dev, diff_method, grad_on_execution, mocker, tol, interface): + def test_jacobian(self, dev, diff_method, grad_on_execution, tol, interface): """Test jacobian calculation""" kwargs = dict( diff_method=diff_method, grad_on_execution=grad_on_execution, interface=interface ) - spy = None - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) kwargs["num_directions"] = 20 tol = TOL_FOR_SPSA - if diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") a = tf.Variable(0.1, dtype=tf.float64) b = tf.Variable(0.2, dtype=tf.float64) @@ -185,16 +177,11 @@ def circuit(a, b): expected = [[-tf.sin(a), tf.sin(a) * tf.sin(b)], [0, -tf.cos(a) * tf.cos(b)]] assert np.allclose(res, expected, atol=tol, rtol=0) - if spy is not None: - spy.assert_called() - - def test_jacobian_options(self, dev, diff_method, grad_on_execution, mocker, interface): + def test_jacobian_options(self, dev, diff_method, grad_on_execution, interface): """Test setting finite-difference jacobian options""" if diff_method not in {"finite-diff", "spsa"}: pytest.skip("Test only works with finite diff and spsa.") - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - a = tf.Variable([0.1, 0.2]) @qnode( @@ -215,13 +202,7 @@ def circuit(a): tape.jacobian(res, a) - for args in spy.call_args_list: - assert args[1]["approx_order"] == 2 - assert args[1]["h"] == 1e-8 - - def test_changing_trainability( - self, dev, diff_method, grad_on_execution, mocker, tol, interface - ): + def test_changing_trainability(self, dev, diff_method, grad_on_execution, tol, interface): """Test changing the trainability of parameters changes the number of differentiation requests made""" if diff_method in ["backprop", "adjoint", "spsa"]: @@ -230,12 +211,8 @@ def test_changing_trainability( a = tf.Variable(0.1, dtype=tf.float64) b = tf.Variable(0.2, dtype=tf.float64) - exp_num_calls = 4 # typically two shifted circuits per parameter - diff_kwargs = {} - if diff_method == "hadamard": - exp_num_calls = 2 # only one circuit per parameter - elif diff_method == "finite-diff": + if diff_method == "finite-diff": diff_kwargs = {"approx_order": 2, "strategy": "center"} @qnode( @@ -261,8 +238,6 @@ def circuit(a, b): expected = [tf.cos(a), -tf.cos(a) * tf.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") - jac = tape.jacobian(res, [a, b]) expected = [ [-tf.sin(a), tf.sin(a) * tf.sin(b)], @@ -270,9 +245,6 @@ def circuit(a, b): ] assert np.allclose(jac, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called for each argument - assert len(spy.spy_return[0]) == exp_num_calls - # make the second QNode argument a constant a = tf.Variable(0.54, dtype=tf.float64) b = tf.constant(0.8, dtype=tf.float64) @@ -287,14 +259,10 @@ def circuit(a, b): expected = [tf.cos(a), -tf.cos(a) * tf.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - spy.call_args_list = [] jac = tape.jacobian(res, a) expected = [-tf.sin(a), tf.sin(a) * tf.sin(b)] assert np.allclose(jac, expected, atol=tol, rtol=0) - # the gradient transform has only been called once - assert len(spy.call_args_list) == 1 - def test_classical_processing(self, dev, diff_method, grad_on_execution, interface): """Test classical processing within the quantum tape""" a = tf.Variable(0.1, dtype=tf.float64) @@ -956,7 +924,7 @@ class TestTapeExpansion: """Test that tape expansion within the QNode integrates correctly with the TF interface""" - def test_gradient_expansion(self, dev, diff_method, grad_on_execution, mocker, interface): + def test_gradient_expansion(self, dev, diff_method, grad_on_execution, interface): """Test that a *supported* operation with no gradient recipe is expanded for both parameter-shift and finite-differences, but not for execution.""" if diff_method not in ("parameter-shift", "finite-diff", "spsa", "hadamard"): @@ -985,24 +953,8 @@ def circuit(x): with tf.GradientTape() as t2: with tf.GradientTape() as t1: loss = circuit(x) - - spy = mocker.spy(circuit.gradient_fn, "transform_fn") res = t1.gradient(loss, x) - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 2 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - - if diff_method != "hadamard": - shifted_tape1, shifted_tape2 = spy.spy_return[0] - - assert len(shifted_tape1.operations) == 2 - assert shifted_tape1.operations[1].name == "RY" - - assert len(shifted_tape2.operations) == 2 - assert shifted_tape2.operations[1].name == "RY" - assert np.allclose(res, -3 * np.sin(3 * x)) if diff_method == "parameter-shift": @@ -1012,7 +964,7 @@ def circuit(x): @pytest.mark.parametrize("max_diff", [1, 2]) def test_gradient_expansion_trainable_only( - self, dev, diff_method, grad_on_execution, max_diff, mocker, interface + self, dev, diff_method, grad_on_execution, max_diff, interface ): """Test that a *supported* operation with no gradient recipe is only expanded for parameter-shift and finite-differences when it is trainable.""" @@ -1044,15 +996,7 @@ def circuit(x, y): with tf.GradientTape() as t: res = circuit(x, y) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") - res = t.gradient(res, [x, y]) - - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 3 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - assert input_tape.operations[2].name == "PhaseShift" - assert input_tape.operations[2].grad_method is None + t.gradient(res, [x, y]) @pytest.mark.parametrize("max_diff", [1, 2]) def test_hamiltonian_expansion_analytic( diff --git a/tests/interfaces/default_qubit_2_integration/test_torch_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_torch_default_qubit_2.py index 34d9f82505c..d4ae61f39b2 100644 --- a/tests/interfaces/default_qubit_2_integration/test_torch_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_torch_default_qubit_2.py @@ -448,7 +448,20 @@ def cost_fn(a, p): tape = qml.tape.QuantumScript( [qml.RX(a, wires=0), U3(*p, wires=0)], [qml.expval(qml.PauliX(0))] ) - return execute([tape], device, **execute_kwargs)[0] + gradient_fn = execute_kwargs["gradient_fn"] + if gradient_fn is None: + _gradient_method = None + elif isinstance(gradient_fn, str): + _gradient_method = gradient_fn + else: + _gradient_method = "gradient-transform" + config = qml.devices.ExecutionConfig( + interface="autograd", + gradient_method=_gradient_method, + grad_on_execution=execute_kwargs.get("grad_on_execution", None), + ) + program, _ = device.preprocess(execution_config=config) + return execute([tape], device, **execute_kwargs, transform_program=program)[0] a = torch.tensor(0.1, requires_grad=False) p = torch.tensor([0.1, 0.2, 0.3], requires_grad=True) diff --git a/tests/interfaces/default_qubit_2_integration/test_torch_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_torch_qnode_default_qubit_2.py index a3f23a6a67c..9250186f58b 100644 --- a/tests/interfaces/default_qubit_2_integration/test_torch_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_torch_qnode_default_qubit_2.py @@ -139,22 +139,15 @@ def circuit(p1, p2=y, **kwargs): assert result == expected - def test_jacobian(self, interface, dev, diff_method, grad_on_execution, mocker, tol): + def test_jacobian(self, interface, dev, diff_method, grad_on_execution, tol): """Test jacobian calculation""" kwargs = dict( diff_method=diff_method, grad_on_execution=grad_on_execution, interface=interface ) - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) kwargs["num_directions"] = 20 tol = TOL_FOR_SPSA - elif diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") a_val = 0.1 b_val = 0.2 @@ -196,9 +189,6 @@ def circuit(a, b): assert np.allclose(a.grad, expected[0], atol=tol, rtol=0) assert np.allclose(b.grad, expected[1], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - # TODO: fix this behavior with float: already present before return type. @pytest.mark.xfail def test_jacobian_dtype(self, interface, dev, diff_method, grad_on_execution): @@ -234,13 +224,11 @@ def circuit(a, b): assert a.grad.dtype is torch.float32 assert b.grad.dtype is torch.float32 - def test_jacobian_options(self, interface, dev, diff_method, grad_on_execution, mocker): + def test_jacobian_options(self, interface, dev, diff_method, grad_on_execution): """Test setting jacobian options""" if diff_method not in {"finite-diff", "spsa"}: pytest.skip("Test only works with finite-diff and spsa") - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - a = torch.tensor([0.1, 0.2], requires_grad=True) @qnode( @@ -259,13 +247,7 @@ def circuit(a): res = circuit(a) res.backward() - for args in spy.call_args_list: - assert args[1]["approx_order"] == 2 - assert args[1]["h"] == 1e-8 - - def test_changing_trainability( - self, interface, dev, diff_method, grad_on_execution, mocker, tol - ): + def test_changing_trainability(self, interface, dev, diff_method, grad_on_execution, tol): """Test that changing the trainability of parameters changes the number of differentiation requests made""" if diff_method != "parameter-shift": @@ -296,8 +278,6 @@ def circuit(a, b): assert np.allclose(res[0].detach().numpy(), expected[0], atol=tol, rtol=0) assert np.allclose(res[1].detach().numpy(), expected[1], atol=tol, rtol=0) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - loss = res[0] + res[1] loss.backward() @@ -307,9 +287,6 @@ def circuit(a, b): ] assert np.allclose([a.grad, b.grad], expected, atol=tol, rtol=0) - # The parameter-shift rule has been called for each argument - assert len(spy.spy_return[0]) == 4 - # make the second QNode argument a constant a_val = 0.54 b_val = 0.8 @@ -327,15 +304,11 @@ def circuit(a, b): assert np.allclose(res[0].detach().numpy(), expected[0], atol=tol, rtol=0) assert np.allclose(res[1].detach().numpy(), expected[1], atol=tol, rtol=0) - spy.call_args_list = [] loss = res[0] + res[1] loss.backward() expected = -np.sin(a_val) + np.sin(a_val) * np.sin(b_val) assert np.allclose(a.grad, expected, atol=tol, rtol=0) - # the gradient transform has only been called once - assert len(spy.call_args_list) == 1 - def test_classical_processing(self, interface, dev, diff_method, grad_on_execution): """Test classical processing within the quantum tape""" a = torch.tensor(0.1, dtype=torch.float64, requires_grad=True) @@ -1061,7 +1034,7 @@ class TestTapeExpansion: """Test that tape expansion within the QNode integrates correctly with the Torch interface""" - def test_gradient_expansion(self, dev, diff_method, grad_on_execution, mocker): + def test_gradient_expansion(self, dev, diff_method, grad_on_execution): """Test that a *supported* operation with no gradient recipe is expanded for both parameter-shift and finite-differences, but not for execution.""" if diff_method not in ("parameter-shift", "finite-diff", "spsa", "hadamard"): @@ -1089,24 +1062,9 @@ def circuit(x): loss = circuit(x) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") loss.backward() res = x.grad - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 2 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - - if diff_method != "hadamard": - shifted_tape1, shifted_tape2 = spy.spy_return[0] - - assert len(shifted_tape1.operations) == 2 - assert shifted_tape1.operations[1].name == "RY" - - assert len(shifted_tape2.operations) == 2 - assert shifted_tape2.operations[1].name == "RY" - assert torch.allclose(res, -3 * torch.sin(3 * x)) if diff_method == "parameter-shift": @@ -1116,7 +1074,11 @@ def circuit(x): @pytest.mark.parametrize("max_diff", [1, 2]) def test_gradient_expansion_trainable_only( - self, dev, diff_method, grad_on_execution, max_diff, mocker + self, + dev, + diff_method, + grad_on_execution, + max_diff, ): """Test that a *supported* operation with no gradient recipe is only expanded for parameter-shift and finite-differences when it is trainable.""" @@ -1146,17 +1108,8 @@ def circuit(x, y): y = torch.tensor(0.7, requires_grad=False) loss = circuit(x, y) - - spy = mocker.spy(circuit.gradient_fn, "transform_fn") loss.backward() - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 3 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - assert input_tape.operations[2].name == "PhaseShift" - assert input_tape.operations[2].grad_method is None - @pytest.mark.parametrize("max_diff", [1, 2]) def test_hamiltonian_expansion_analytic( self, dev, diff_method, grad_on_execution, max_diff, tol diff --git a/tests/interfaces/test_autograd_qnode.py b/tests/interfaces/test_autograd_qnode.py index fdaf469980c..73ad1da9e39 100644 --- a/tests/interfaces/test_autograd_qnode.py +++ b/tests/interfaces/test_autograd_qnode.py @@ -172,23 +172,17 @@ def circuit(a): assert isinstance(grad, float) assert grad.shape == tuple() - def test_jacobian(self, interface, dev_name, diff_method, grad_on_execution, mocker, tol): + def test_jacobian(self, interface, dev_name, diff_method, grad_on_execution, tol): """Test jacobian calculation""" num_wires = 2 kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution ) - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": spsa_kwargs = dict(sampler_rng=np.random.default_rng(SEED_FOR_SPSA), num_directions=10) kwargs = {**kwargs, **spsa_kwargs} tol = TOL_FOR_SPSA elif diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") num_wires = 3 a = np.array(0.1, requires_grad=True) @@ -226,28 +220,17 @@ def cost(x, y): assert res[1].shape == (2,) assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - - def test_jacobian_no_evaluate( - self, interface, dev_name, diff_method, grad_on_execution, mocker, tol - ): + def test_jacobian_no_evaluate(self, interface, dev_name, diff_method, grad_on_execution, tol): """Test jacobian calculation when no prior circuit evaluation has been performed""" num_wires = 2 kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution ) - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) tol = TOL_FOR_SPSA elif diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") num_wires = 3 a = np.array(0.1, requires_grad=True) @@ -271,20 +254,16 @@ def cost(x, y): assert np.allclose(res[0], expected[0], atol=tol, rtol=0) assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - # call the Jacobian with new parameters a = np.array(0.6, requires_grad=True) b = np.array(0.832, requires_grad=True) res = jac_fn(a, b) expected = ([-np.sin(a), np.sin(a) * np.sin(b)], [0, -np.cos(a) * np.cos(b)]) - expected = ([-np.sin(a), np.sin(a) * np.sin(b)], [0, -np.cos(a) * np.cos(b)]) assert np.allclose(res[0], expected[0], atol=tol, rtol=0) assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - def test_jacobian_options(self, interface, dev_name, diff_method, grad_on_execution, mocker): + def test_jacobian_options(self, interface, dev_name, diff_method, grad_on_execution): """Test setting jacobian options""" wires = [0] if diff_method in ["backprop", "adjoint"]: @@ -310,17 +289,10 @@ def circuit(a): return qml.expval(qml.PauliZ(0)) circuit(a) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") qml.jacobian(circuit)(a) - for args in spy.call_args_list: - for key, val in kwargs.items(): - assert args[1][key] == val - - def test_changing_trainability( - self, interface, dev_name, diff_method, grad_on_execution, mocker, tol - ): + def test_changing_trainability(self, interface, dev_name, diff_method, grad_on_execution, tol): """Test changing the trainability of parameters changes the number of differentiation requests made""" if diff_method != "parameter-shift": @@ -342,7 +314,6 @@ def loss(a, b): return np.sum(autograd.numpy.hstack(circuit(a, b))) grad_fn = qml.grad(loss) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") res = grad_fn(a, b) # the tape has reported both arguments as trainable @@ -351,9 +322,6 @@ def loss(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called for each argument - assert len(spy.spy_return[0]) == 4 - # make the second QNode argument a constant a = np.array(0.54, requires_grad=True) b = np.array(0.8, requires_grad=False) @@ -366,9 +334,6 @@ def loss(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called only once - assert len(spy.spy_return[0]) == 2 - # trainability also updates on evaluation a = np.array(0.54, requires_grad=False) b = np.array(0.8, requires_grad=True) @@ -1264,7 +1229,7 @@ def cost_fn(x): assert np.allclose(hess, expected_hess, atol=tol, rtol=0) def test_hessian_vector_valued_separate_args( - self, interface, dev_name, diff_method, grad_on_execution, mocker, tol + self, interface, dev_name, diff_method, grad_on_execution, tol ): """Test hessian calculation of a vector valued QNode that has separate input arguments""" if diff_method not in {"parameter-shift", "backprop"}: @@ -1307,7 +1272,6 @@ def circuit(a, b): assert g[1].shape == (2,) assert np.allclose(g[1], expected_g[1], atol=tol, rtol=0) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") jac_fn_a = lambda *args: jac_fn(*args)[0] jac_fn_b = lambda *args: jac_fn(*args)[1] hess_a = qml.jacobian(jac_fn_a)(a, b) @@ -1315,11 +1279,6 @@ def circuit(a, b): assert isinstance(hess_a, tuple) and len(hess_a) == 2 assert isinstance(hess_b, tuple) and len(hess_b) == 2 - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called() - exp_hess_a = ( [-0.5 * np.cos(a) * np.cos(b), 0.5 * np.cos(a) * np.cos(b)], [0.5 * np.sin(a) * np.sin(b), -0.5 * np.sin(a) * np.sin(b)], @@ -1584,7 +1543,7 @@ class TestTapeExpansion: @pytest.mark.parametrize("max_diff", [1, 2]) def test_gradient_expansion_trainable_only( - self, dev_name, diff_method, grad_on_execution, max_diff, mocker + self, dev_name, diff_method, grad_on_execution, max_diff ): """Test that a *supported* operation with no gradient recipe is only expanded for parameter-shift and finite-differences when it is trainable.""" @@ -1613,21 +1572,12 @@ def circuit(x, y): PhaseShift(2 * y, wires=0) return qml.expval(qml.PauliX(0)) - spy = mocker.spy(circuit.device, "batch_execute") x = np.array(0.5, requires_grad=True) y = np.array(0.7, requires_grad=False) circuit(x, y) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") qml.grad(circuit)(x, y) - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 3 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - assert input_tape.operations[2].name == "PhaseShift" - assert input_tape.operations[2].grad_method is None - @pytest.mark.parametrize("max_diff", [1, 2]) def test_hamiltonian_expansion_analytic( self, dev_name, diff_method, grad_on_execution, max_diff, tol diff --git a/tests/interfaces/test_jacobian_products.py b/tests/interfaces/test_jacobian_products.py index f4d1cd036dc..b8a45d4afb1 100644 --- a/tests/interfaces/test_jacobian_products.py +++ b/tests/interfaces/test_jacobian_products.py @@ -57,7 +57,7 @@ def test_transform_jacobian_product_basics(self): expected_repr = ( f"TransformJacobianProducts({repr(inner_execute_numpy)}, " - "gradient_transform=, " + "gradient_transform=, " "gradient_kwargs={'aux_wire': 'aux'})" ) assert repr(jpc) == expected_repr diff --git a/tests/interfaces/test_jax_jit_qnode.py b/tests/interfaces/test_jax_jit_qnode.py index 6fb29cec859..f8fc3b262db 100644 --- a/tests/interfaces/test_jax_jit_qnode.py +++ b/tests/interfaces/test_jax_jit_qnode.py @@ -83,9 +83,7 @@ def circuit(a): assert isinstance(grad, jax.Array) assert grad.shape == () - def test_changing_trainability( - self, dev_name, diff_method, grad_on_execution, interface, mocker, tol - ): + def test_changing_trainability(self, dev_name, diff_method, grad_on_execution, interface, tol): """Test changing the trainability of parameters changes the number of differentiation requests made""" if diff_method != "parameter-shift": @@ -109,7 +107,6 @@ def circuit(a, b): return qml.expval(qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliY(1)])) grad_fn = jax.jit(jax.grad(circuit, argnums=[0, 1])) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") res = grad_fn(a, b) # the tape has reported both arguments as trainable @@ -118,9 +115,6 @@ def circuit(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called for each argument - assert len(spy.spy_return[0]) == 4 - # make the second QNode argument a constant grad_fn = jax.grad(circuit, argnums=0) res = grad_fn(a, b) @@ -131,9 +125,6 @@ def circuit(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called only once - assert len(spy.spy_return[0]) == 2 - # trainability also updates on evaluation a = np.array(0.54, requires_grad=False) b = np.array(0.8, requires_grad=True) @@ -258,13 +249,11 @@ def circuit(a, p): ) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_jacobian_options(self, dev_name, diff_method, grad_on_execution, interface, mocker): + def test_jacobian_options(self, dev_name, diff_method, grad_on_execution, interface): """Test setting jacobian options""" if diff_method != "finite-diff": pytest.skip("Test only applies to finite diff.") - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - a = np.array([0.1, 0.2], requires_grad=True) dev = qml.device(dev_name, wires=1) @@ -288,10 +277,6 @@ def circuit(a): jax.jit(jax.jacobian(circuit))(a) - for args in spy.call_args_list: - assert args[1]["approx_order"] == 2 - assert args[1]["h"] == 1e-8 - @pytest.mark.parametrize( "interface,dev_name,diff_method,grad_on_execution", interface_and_qubit_device_and_diff_method @@ -300,22 +285,14 @@ class TestVectorValuedQNode: """Test that using vector-valued QNodes with JAX integrate with the PennyLane stack""" - def test_diff_expval_expval( - self, dev_name, diff_method, grad_on_execution, interface, mocker, tol - ): + def test_diff_expval_expval(self, dev_name, diff_method, grad_on_execution, interface, tol): """Test jacobian calculation""" gradient_kwargs = {} - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + + if diff_method == "spsa": gradient_kwargs = {"sampler_rng": SEED_FOR_SPSA} tol = TOL_FOR_SPSA - elif diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") a = np.array(0.1, requires_grad=True) b = np.array(0.2, requires_grad=True) @@ -373,25 +350,13 @@ def circuit(a, b): assert res[1][1].shape == () assert np.allclose(res[1][1], expected[1][1], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff"): - spy.assert_called() - - def test_jacobian_no_evaluate( - self, dev_name, diff_method, grad_on_execution, interface, mocker, tol - ): + def test_jacobian_no_evaluate(self, dev_name, diff_method, grad_on_execution, interface, tol): """Test jacobian calculation when no prior circuit evaluation has been performed""" gradient_kwargs = {} - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": gradient_kwargs = {"sampler_rng": SEED_FOR_SPSA} tol = TOL_FOR_SPSA - elif diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") a = jax.numpy.array(0.1) b = jax.numpy.array(0.2) @@ -431,9 +396,6 @@ def circuit(a, b): assert r.shape == () assert np.allclose(r, e, atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - # call the Jacobian with new parameters a = jax.numpy.array(0.6) b = jax.numpy.array(0.832) @@ -1270,7 +1232,7 @@ def cost_fn(x): assert np.allclose(hess, expected_hess, atol=tol, rtol=0) def test_hessian_vector_valued_separate_args( - self, dev_name, diff_method, grad_on_execution, interface, mocker, tol + self, dev_name, diff_method, grad_on_execution, interface, tol ): """Test hessian calculation of a vector valued QNode that has separate input arguments""" gradient_kwargs = {} @@ -1321,15 +1283,8 @@ def circuit(a, b): ] ) assert np.allclose(g, expected_g.T, atol=tol, rtol=0) - - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") hess = jax.jit(jax.jacobian(jac_fn, argnums=[0, 1]))(a, b) - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called() - expected_hess = np.array( [ [ @@ -1528,7 +1483,7 @@ class TestTapeExpansion: @pytest.mark.parametrize("max_diff", [1, 2]) def test_gradient_expansion_trainable_only( - self, dev_name, diff_method, grad_on_execution, max_diff, interface, mocker + self, dev_name, diff_method, grad_on_execution, max_diff, interface ): """Test that a *supported* operation with no gradient recipe is only expanded for parameter-shift and finite-differences when it is trainable.""" @@ -1561,21 +1516,11 @@ def circuit(x, y): PhaseShift(2 * y, wires=0) return qml.expval(qml.PauliX(0)) - spy = mocker.spy(circuit.device, "batch_execute") x = jax.numpy.array(0.5) y = jax.numpy.array(0.7) circuit(x, y) - - spy = mocker.spy(circuit.gradient_fn, "transform_fn") jax.grad(circuit, argnums=[0])(x, y) - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 3 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - assert input_tape.operations[2].name == "PhaseShift" - assert input_tape.operations[2].grad_method is None - @pytest.mark.parametrize("max_diff", [1, 2]) def test_hamiltonian_expansion_analytic( self, dev_name, diff_method, grad_on_execution, max_diff, interface, mocker, tol diff --git a/tests/interfaces/test_jax_qnode.py b/tests/interfaces/test_jax_qnode.py index 91260e2d90c..6dec4753854 100644 --- a/tests/interfaces/test_jax_qnode.py +++ b/tests/interfaces/test_jax_qnode.py @@ -86,9 +86,7 @@ def circuit(a): assert isinstance(grad, jax.Array) assert grad.shape == () - def test_changing_trainability( - self, dev_name, diff_method, grad_on_execution, interface, mocker, tol - ): + def test_changing_trainability(self, dev_name, diff_method, grad_on_execution, interface, tol): """Test changing the trainability of parameters changes the number of differentiation requests made""" if diff_method != "parameter-shift": @@ -112,7 +110,6 @@ def circuit(a, b): return qml.expval(qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliY(1)])) grad_fn = jax.grad(circuit, argnums=[0, 1]) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") res = grad_fn(a, b) # the tape has reported both arguments as trainable @@ -121,9 +118,6 @@ def circuit(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called for each argument - assert len(spy.spy_return[0]) == 4 - # make the second QNode argument a constant grad_fn = jax.grad(circuit, argnums=0) res = grad_fn(a, b) @@ -134,9 +128,6 @@ def circuit(a, b): expected = [-np.sin(a) + np.sin(a) * np.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called only once - assert len(spy.spy_return[0]) == 2 - # trainability also updates on evaluation a = np.array(0.54, requires_grad=False) b = np.array(0.8, requires_grad=True) @@ -260,13 +251,11 @@ def circuit(a, p): ) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_jacobian_options(self, dev_name, diff_method, grad_on_execution, interface, mocker): + def test_jacobian_options(self, dev_name, diff_method, grad_on_execution, interface): """Test setting jacobian options""" if diff_method != "finite-diff": pytest.skip("Test only applies to finite diff.") - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - a = np.array([0.1, 0.2], requires_grad=True) dev = qml.device(dev_name, wires=1) @@ -286,10 +275,6 @@ def circuit(a): jax.jacobian(circuit)(a) - for args in spy.call_args_list: - assert args[1]["approx_order"] == 2 - assert args[1]["h"] == 1e-8 - @pytest.mark.parametrize( "interface,dev_name,diff_method,grad_on_execution", interface_and_qubit_device_and_diff_method @@ -298,19 +283,12 @@ class TestVectorValuedQNode: """Test that using vector-valued QNodes with JAX integrate with the PennyLane stack""" - def test_diff_expval_expval( - self, dev_name, diff_method, grad_on_execution, interface, mocker, tol - ): + def test_diff_expval_expval(self, dev_name, diff_method, grad_on_execution, interface, tol): """Test jacobian calculation""" kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution ) - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) tol = TOL_FOR_SPSA @@ -362,22 +340,12 @@ def circuit(a, b): assert res[1][1].shape == () assert np.allclose(res[1][1], expected[1][1], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff"): - spy.assert_called() - - def test_jacobian_no_evaluate( - self, dev_name, diff_method, grad_on_execution, interface, mocker, tol - ): + def test_jacobian_no_evaluate(self, dev_name, diff_method, grad_on_execution, interface, tol): """Test jacobian calculation when no prior circuit evaluation has been performed""" kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution ) - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) tol = TOL_FOR_SPSA @@ -412,9 +380,6 @@ def circuit(a, b): assert r.shape == () assert np.allclose(r, e, atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - # call the Jacobian with new parameters a = jax.numpy.array(0.6) b = jax.numpy.array(0.832) @@ -1217,7 +1182,7 @@ def cost_fn(x): assert np.allclose(hess, expected_hess, atol=tol, rtol=0) def test_hessian_vector_valued_separate_args( - self, dev_name, diff_method, grad_on_execution, interface, mocker, tol + self, dev_name, diff_method, grad_on_execution, interface, tol ): """Test hessian calculation of a vector valued QNode that has separate input arguments""" gradient_kwargs = {} @@ -1268,15 +1233,8 @@ def circuit(a, b): ] ) assert np.allclose(g, expected_g.T, atol=tol, rtol=0) - - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") hess = jax.jacobian(jac_fn, argnums=[0, 1])(a, b) - if diff_method == "backprop": - spy.assert_not_called() - elif diff_method == "parameter-shift": - spy.assert_called() - expected_hess = np.array( [ [ @@ -1472,7 +1430,7 @@ class TestTapeExpansion: @pytest.mark.parametrize("max_diff", [1, 2]) def test_gradient_expansion_trainable_only( - self, dev_name, diff_method, grad_on_execution, max_diff, interface, mocker + self, dev_name, diff_method, grad_on_execution, max_diff, interface ): """Test that a *supported* operation with no gradient recipe is only expanded for parameter-shift and finite-differences when it is trainable.""" @@ -1505,21 +1463,12 @@ def circuit(x, y): PhaseShift(2 * y, wires=0) return qml.expval(qml.PauliX(0)) - spy = mocker.spy(circuit.device, "batch_execute") x = jax.numpy.array(0.5) y = jax.numpy.array(0.7) circuit(x, y) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") jax.grad(circuit, argnums=[0])(x, y) - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 3 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - assert input_tape.operations[2].name == "PhaseShift" - assert input_tape.operations[2].grad_method is None - @pytest.mark.parametrize("max_diff", [1, 2]) def test_hamiltonian_expansion_analytic( self, dev_name, diff_method, grad_on_execution, max_diff, interface, mocker, tol diff --git a/tests/interfaces/test_tensorflow_qnode.py b/tests/interfaces/test_tensorflow_qnode.py index 7773c86d27d..da68bbf7f2f 100644 --- a/tests/interfaces/test_tensorflow_qnode.py +++ b/tests/interfaces/test_tensorflow_qnode.py @@ -161,24 +161,19 @@ def circuit(p1, p2=y, **kwargs): expected = "0: ──RX(0.10)──RX(0.40)─╭●─┤ State\n1: ──RY(0.06)───────────╰X─┤ State" assert result == expected - def test_jacobian(self, dev_name, diff_method, grad_on_execution, mocker, tol, interface): + def test_jacobian(self, dev_name, diff_method, grad_on_execution, tol, interface): """Test jacobian calculation""" kwargs = dict( diff_method=diff_method, grad_on_execution=grad_on_execution, interface=interface ) - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) tol = TOL_FOR_SPSA num_wires = 2 if diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") num_wires = 3 dev = qml.device(dev_name, wires=num_wires) @@ -209,9 +204,6 @@ def circuit(a, b): expected = [[-tf.sin(a), tf.sin(a) * tf.sin(b)], [0, -tf.cos(a) * tf.cos(b)]] assert np.allclose(res, expected, atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - def test_jacobian_dtype(self, dev_name, diff_method, grad_on_execution, interface): """Test calculating the jacobian with a different datatype""" if diff_method == "backprop": @@ -249,13 +241,11 @@ def circuit(a, b): res = tape.jacobian(res, [a, b]) assert [r.dtype is tf.float32 for r in res] - def test_jacobian_options(self, dev_name, diff_method, grad_on_execution, mocker, interface): + def test_jacobian_options(self, dev_name, diff_method, grad_on_execution, interface): """Test setting finite-difference jacobian options""" if diff_method not in {"finite-diff", "spsa"}: pytest.skip("Test only works with finite diff and spsa.") - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - a = tf.Variable([0.1, 0.2]) num_wires = 1 @@ -283,13 +273,7 @@ def circuit(a): tape.jacobian(res, a) - for args in spy.call_args_list: - assert args[1]["approx_order"] == 2 - assert args[1]["h"] == 1e-8 - - def test_changing_trainability( - self, dev_name, diff_method, grad_on_execution, mocker, tol, interface - ): + def test_changing_trainability(self, dev_name, diff_method, grad_on_execution, tol, interface): """Test changing the trainability of parameters changes the number of differentiation requests made""" if diff_method in ["backprop", "adjoint", "spsa"]: @@ -299,12 +283,10 @@ def test_changing_trainability( b = tf.Variable(0.2, dtype=tf.float64) num_wires = 2 - exp_num_calls = 4 # typically two shifted circuits per parameter diff_kwargs = {} if diff_method == "hadamard": num_wires = 3 - exp_num_calls = 2 # only one circuit per parameter elif diff_method == "finite-diff": diff_kwargs = {"approx_order": 2, "strategy": "center"} @@ -333,8 +315,6 @@ def circuit(a, b): expected = [tf.cos(a), -tf.cos(a) * tf.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") - jac = tape.jacobian(res, [a, b]) expected = [ [-tf.sin(a), tf.sin(a) * tf.sin(b)], @@ -342,9 +322,6 @@ def circuit(a, b): ] assert np.allclose(jac, expected, atol=tol, rtol=0) - # The parameter-shift rule has been called for each argument - assert len(spy.spy_return[0]) == exp_num_calls - # make the second QNode argument a constant a = tf.Variable(0.54, dtype=tf.float64) b = tf.constant(0.8, dtype=tf.float64) @@ -359,14 +336,10 @@ def circuit(a, b): expected = [tf.cos(a), -tf.cos(a) * tf.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) - spy.call_args_list = [] jac = tape.jacobian(res, a) expected = [-tf.sin(a), tf.sin(a) * tf.sin(b)] assert np.allclose(jac, expected, atol=tol, rtol=0) - # the gradient transform has only been called once - assert len(spy.call_args_list) == 1 - def test_classical_processing(self, dev_name, diff_method, grad_on_execution, interface): """Test classical processing within the quantum tape""" a = tf.Variable(0.1, dtype=tf.float64) @@ -553,7 +526,7 @@ def circuit(weights): spy.assert_not_called() # execute with shots=100 - res = circuit(weights, shots=100) # pylint: disable=unexpected-keyword-arg + circuit(weights, shots=100) # pylint: disable=unexpected-keyword-arg spy.assert_called() assert spy.spy_return.shape == (100,) @@ -1265,7 +1238,7 @@ class TestTapeExpansion: """Test that tape expansion within the QNode integrates correctly with the TF interface""" - def test_gradient_expansion(self, dev_name, diff_method, grad_on_execution, mocker, interface): + def test_gradient_expansion(self, dev_name, diff_method, grad_on_execution, interface): """Test that a *supported* operation with no gradient recipe is expanded for both parameter-shift and finite-differences, but not for execution.""" if diff_method not in ("parameter-shift", "finite-diff", "spsa", "hadamard"): @@ -1295,30 +1268,14 @@ def circuit(x): PhaseShift(x, wires=0) return qml.expval(qml.PauliX(0)) - spy = mocker.spy(circuit.device, "batch_execute") x = tf.Variable(0.5, dtype=tf.float64) with tf.GradientTape() as t2: with tf.GradientTape() as t1: loss = circuit(x) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") res = t1.gradient(loss, x) - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 2 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - - if diff_method != "hadamard": - shifted_tape1, shifted_tape2 = spy.spy_return[0] - - assert len(shifted_tape1.operations) == 2 - assert shifted_tape1.operations[1].name == "RY" - - assert len(shifted_tape2.operations) == 2 - assert shifted_tape2.operations[1].name == "RY" - assert np.allclose(res, -3 * np.sin(3 * x)) if diff_method == "parameter-shift": @@ -1328,7 +1285,7 @@ def circuit(x): @pytest.mark.parametrize("max_diff", [1, 2]) def test_gradient_expansion_trainable_only( - self, dev_name, diff_method, grad_on_execution, max_diff, mocker, interface + self, dev_name, diff_method, grad_on_execution, max_diff, interface ): """Test that a *supported* operation with no gradient recipe is only expanded for parameter-shift and finite-differences when it is trainable.""" @@ -1361,22 +1318,13 @@ def circuit(x, y): PhaseShift(2 * y, wires=0) return qml.expval(qml.PauliX(0)) - spy = mocker.spy(circuit.device, "batch_execute") x = tf.Variable(0.5, dtype=tf.float64) y = tf.constant(0.7, dtype=tf.float64) with tf.GradientTape() as t: res = circuit(x, y) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") - res = t.gradient(res, [x, y]) - - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 3 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - assert input_tape.operations[2].name == "PhaseShift" - assert input_tape.operations[2].grad_method is None + t.gradient(res, [x, y]) @pytest.mark.parametrize("max_diff", [1, 2]) def test_hamiltonian_expansion_analytic( diff --git a/tests/interfaces/test_torch_qnode.py b/tests/interfaces/test_torch_qnode.py index 77382a6597c..4218a6fc26b 100644 --- a/tests/interfaces/test_torch_qnode.py +++ b/tests/interfaces/test_torch_qnode.py @@ -158,21 +158,15 @@ def circuit(p1, p2=y, **kwargs): assert result == expected - def test_jacobian(self, interface, dev_name, diff_method, grad_on_execution, mocker, tol): + def test_jacobian(self, interface, dev_name, diff_method, grad_on_execution, tol): """Test jacobian calculation""" kwargs = dict( diff_method=diff_method, grad_on_execution=grad_on_execution, interface=interface ) - if diff_method == "parameter-shift": - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - elif diff_method == "finite-diff": - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - elif diff_method == "spsa": - spy = mocker.spy(qml.gradients.spsa_grad, "transform_fn") + + if diff_method == "spsa": kwargs["sampler_rng"] = np.random.default_rng(SEED_FOR_SPSA) tol = TOL_FOR_SPSA - elif diff_method == "hadamard": - spy = mocker.spy(qml.gradients.hadamard_grad, "transform_fn") a_val = 0.1 b_val = 0.2 @@ -221,9 +215,6 @@ def circuit(a, b): assert np.allclose(a.grad, expected[0], atol=tol, rtol=0) assert np.allclose(b.grad, expected[1], atol=tol, rtol=0) - if diff_method in ("parameter-shift", "finite-diff", "spsa"): - spy.assert_called() - # TODO: fix this behavior with float: already present before return type. @pytest.mark.xfail def test_jacobian_dtype(self, interface, dev_name, diff_method, grad_on_execution): @@ -261,13 +252,11 @@ def circuit(a, b): assert a.grad.dtype is torch.float32 assert b.grad.dtype is torch.float32 - def test_jacobian_options(self, interface, dev_name, diff_method, grad_on_execution, mocker): + def test_jacobian_options(self, interface, dev_name, diff_method, grad_on_execution): """Test setting jacobian options""" if diff_method not in {"finite-diff", "spsa"}: pytest.skip("Test only works with finite-diff and spsa") - spy = mocker.spy(qml.gradients.finite_diff, "transform_fn") - a = torch.tensor([0.1, 0.2], requires_grad=True) dev = qml.device(dev_name, wires=1) @@ -288,13 +277,7 @@ def circuit(a): res = circuit(a) res.backward() - for args in spy.call_args_list: - assert args[1]["approx_order"] == 2 - assert args[1]["h"] == 1e-8 - - def test_changing_trainability( - self, interface, dev_name, diff_method, grad_on_execution, mocker, tol - ): + def test_changing_trainability(self, interface, dev_name, diff_method, grad_on_execution, tol): """Test that changing the trainability of parameters changes the number of differentiation requests made""" if diff_method != "parameter-shift": @@ -327,8 +310,6 @@ def circuit(a, b): assert np.allclose(res[0].detach().numpy(), expected[0], atol=tol, rtol=0) assert np.allclose(res[1].detach().numpy(), expected[1], atol=tol, rtol=0) - spy = mocker.spy(qml.gradients.param_shift, "transform_fn") - loss = res[0] + res[1] loss.backward() @@ -338,9 +319,6 @@ def circuit(a, b): ] assert np.allclose([a.grad, b.grad], expected, atol=tol, rtol=0) - # The parameter-shift rule has been called for each argument - assert len(spy.spy_return[0]) == 4 - # make the second QNode argument a constant a_val = 0.54 b_val = 0.8 @@ -358,15 +336,11 @@ def circuit(a, b): assert np.allclose(res[0].detach().numpy(), expected[0], atol=tol, rtol=0) assert np.allclose(res[1].detach().numpy(), expected[1], atol=tol, rtol=0) - spy.call_args_list = [] loss = res[0] + res[1] loss.backward() expected = -np.sin(a_val) + np.sin(a_val) * np.sin(b_val) assert np.allclose(a.grad, expected, atol=tol, rtol=0) - # the gradient transform has only been called once - assert len(spy.call_args_list) == 1 - def test_classical_processing(self, interface, dev_name, diff_method, grad_on_execution): """Test classical processing within the quantum tape""" a = torch.tensor(0.1, dtype=torch.float64, requires_grad=True) @@ -1324,7 +1298,7 @@ class TestTapeExpansion: """Test that tape expansion within the QNode integrates correctly with the Torch interface""" - def test_gradient_expansion(self, dev_name, diff_method, grad_on_execution, mocker): + def test_gradient_expansion(self, dev_name, diff_method, grad_on_execution): """Test that a *supported* operation with no gradient recipe is expanded for both parameter-shift and finite-differences, but not for execution.""" if diff_method not in ("parameter-shift", "finite-diff", "spsa", "hadamard"): @@ -1355,29 +1329,12 @@ def circuit(x): PhaseShift(x, wires=0) return qml.expval(qml.PauliX(0)) - spy = mocker.spy(circuit.device, "batch_execute") x = torch.tensor(0.5, requires_grad=True, dtype=torch.float64) loss = circuit(x) - - spy = mocker.spy(circuit.gradient_fn, "transform_fn") loss.backward() res = x.grad - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 2 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - - if diff_method != "hadamard": - shifted_tape1, shifted_tape2 = spy.spy_return[0] - - assert len(shifted_tape1.operations) == 2 - assert shifted_tape1.operations[1].name == "RY" - - assert len(shifted_tape2.operations) == 2 - assert shifted_tape2.operations[1].name == "RY" - assert torch.allclose(res, -3 * torch.sin(3 * x)) if diff_method == "parameter-shift": @@ -1387,7 +1344,7 @@ def circuit(x): @pytest.mark.parametrize("max_diff", [1, 2]) def test_gradient_expansion_trainable_only( - self, dev_name, diff_method, grad_on_execution, max_diff, mocker + self, dev_name, diff_method, grad_on_execution, max_diff ): """Test that a *supported* operation with no gradient recipe is only expanded for parameter-shift and finite-differences when it is trainable.""" @@ -1420,22 +1377,12 @@ def circuit(x, y): PhaseShift(2 * y, wires=0) return qml.expval(qml.PauliX(0)) - spy = mocker.spy(circuit.device, "batch_execute") x = torch.tensor(0.5, requires_grad=True) y = torch.tensor(0.7, requires_grad=False) loss = circuit(x, y) - - spy = mocker.spy(circuit.gradient_fn, "transform_fn") loss.backward() - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 3 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - assert input_tape.operations[2].name == "PhaseShift" - assert input_tape.operations[2].grad_method is None - @pytest.mark.parametrize("max_diff", [1, 2]) def test_hamiltonian_expansion_analytic( self, dev_name, diff_method, grad_on_execution, max_diff, tol diff --git a/tests/interfaces/test_transform_program_integration.py b/tests/interfaces/test_transform_program_integration.py index 2ac697bcafe..5e7d4b328c1 100644 --- a/tests/interfaces/test_transform_program_integration.py +++ b/tests/interfaces/test_transform_program_integration.py @@ -17,6 +17,7 @@ """ import copy from typing import Tuple, Callable +from functools import partial import pytest import numpy as np @@ -250,3 +251,32 @@ def transform_mul(tape: qml.tape.QuantumTape): assert qml.math.allclose(results_reverse[0], 4.0) # (-1.0 + 1.0) * 2.0 = 0.0 assert qml.math.allclose(results_reverse[1], 0.0) + + def test_composable_transform(self): + """Test the composition of a gradient transform with another transform.""" + import jax + + dev = qml.device("default.qubit", wires=2) + + @partial(qml.gradients.param_shift, argnums=[0, 1]) + @qml.transforms.split_non_commuting + @qml.qnode(device=dev, interface="jax") + def circuit(x, y): + qml.RX(x, wires=0) + qml.RZ(y, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(wires=0)), qml.expval(qml.PauliY(wires=0)) + + x = jax.numpy.array(0.1) + y = jax.numpy.array(0.2) + + res = circuit(x, y) + + assert isinstance(res, tuple) + assert len(res) == 2 + + assert isinstance(res[0], tuple) + assert len(res[0]) == 2 + + assert isinstance(res[1], tuple) + assert len(res[1]) == 2 diff --git a/tests/logging/test_logging_autograd.py b/tests/logging/test_logging_autograd.py index c9839f308b8..aca061083b1 100644 --- a/tests/logging/test_logging_autograd.py +++ b/tests/logging/test_logging_autograd.py @@ -23,7 +23,7 @@ _grad_log_map = { "adjoint": "gradient_fn=adjoint, interface=autograd, grad_on_execution=best, gradient_kwargs={}", "backprop": "gradient_fn=backprop, interface=autograd, grad_on_execution=best, gradient_kwargs={}", - "parameter-shift": "gradient_fn=", + "parameter-shift": "gradient_fn=", } diff --git a/tests/qinfo/test_fisher.py b/tests/qinfo/test_fisher.py index 7ccae1e9e29..fb6afb31239 100644 --- a/tests/qinfo/test_fisher.py +++ b/tests/qinfo/test_fisher.py @@ -157,7 +157,7 @@ def qfunc(params): qml.RX(params[0], wires=0) qml.RX(params[1], wires=0) qml.CNOT(wires=(0, 1)) - return qml.probs() + return qml.probs(wires=[0, 1]) params = pnp.random.random(2) diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 34566606cf9..196c5329300 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -843,6 +843,7 @@ def circuit(inputs, w1, w2): expected = ( f"0: ─╭AngleEmbedding(M0)──RX({w1})─╭StronglyEntanglingLayers(M1)─┤ \n" f"1: ─╰AngleEmbedding(M0)───────────╰StronglyEntanglingLayers(M1)─┤ \n" + f"\n" f"M0 = \n{x}\n" f"M1 = \n{m1}" ) diff --git a/tests/qnn/test_qnn_torch.py b/tests/qnn/test_qnn_torch.py index ba2701063a9..16adcdf856a 100644 --- a/tests/qnn/test_qnn_torch.py +++ b/tests/qnn/test_qnn_torch.py @@ -775,6 +775,7 @@ def circuit(inputs, w1, w2): expected = ( f"0: ─╭AngleEmbedding(M0)──RX({w1})─╭StronglyEntanglingLayers(M1)─┤ \n" f"1: ─╰AngleEmbedding(M0)───────────╰StronglyEntanglingLayers(M1)─┤ \n" + f"\n" f"M0 = \n{x}\n" f"M1 = \n{m1}" ) diff --git a/tests/tape/test_qscript.py b/tests/tape/test_qscript.py index a115f99f4fd..66535b3c451 100644 --- a/tests/tape/test_qscript.py +++ b/tests/tape/test_qscript.py @@ -1048,9 +1048,9 @@ def test_output_shapes_single_qnode_check(self, measurement, expected_shape, sho ops = [qml.RY(a, 0), qml.RX(b, 0)] qs = QuantumScript(ops, [measurement], shots=shots) - + program, _ = dev.preprocess() # TODO: test gradient_fn is not None when the interface `execute` functions are implemented - res = qml.execute([qs], dev, gradient_fn=None)[0] + res = qml.execute([qs], dev, gradient_fn=None, transform_program=program)[0] if isinstance(shots, tuple): res_shape = tuple(r.shape for r in res) @@ -1212,7 +1212,10 @@ def test_broadcasting_single(self, measurement, expected_shape, shots): qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - expected_shape = qml.execute([tape], dev, gradient_fn=None)[0].shape + program, _ = dev.preprocess() + expected_shape = qml.execute([tape], dev, gradient_fn=None, transform_program=program)[ + 0 + ].shape assert tape.shape(dev) == expected_shape @@ -1240,7 +1243,8 @@ def test_broadcasting_multi(self, measurement, expected, shots): qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - expected = qml.execute([tape], dev, gradient_fn=None)[0] + program, _ = dev.preprocess() + expected = qml.execute([tape], dev, gradient_fn=None, transform_program=program)[0] actual = tape.shape(dev) for exp, act in zip(expected, actual): @@ -1290,7 +1294,8 @@ def test_multi_measure_sample_wires_shot_vector(self): res = qs.shape(dev) assert res == expected - expected = qml.execute([qs], dev, gradient_fn=None)[0] + program, _ = dev.preprocess() + expected = qml.execute([qs], dev, gradient_fn=None, transform_program=program)[0] expected_shape = tuple(tuple(e_.shape for e_ in e) for e in expected) assert res == expected_shape diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index e509694f117..5f6e07d75ae 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -1904,7 +1904,10 @@ def test_output_shapes_single_qnode_check(self, measurement, _, shots): qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - res = qml.execute([tape], dev, gradient_fn=qml.gradients.param_shift)[0] + program, _ = dev.preprocess() + res = qml.execute( + [tape], dev, gradient_fn=qml.gradients.param_shift, transform_program=program + )[0] if isinstance(res, tuple): res_shape = tuple(r.shape for r in res) @@ -2105,7 +2108,8 @@ def test_broadcasting_single(self, measurement, _, shots): qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - expected = qml.execute([tape], dev, gradient_fn=None)[0] + program, _ = dev.preprocess() + expected = qml.execute([tape], dev, gradient_fn=None, transform_program=program)[0] assert tape.shape(dev) == expected.shape @pytest.mark.autograd @@ -2132,7 +2136,8 @@ def test_broadcasting_multi(self, measurement, expected, shots): qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q, shots=shots) - expected = qml.execute([tape], dev, gradient_fn=None)[0] + program, _ = dev.preprocess() + expected = qml.execute([tape], dev, gradient_fn=None, transform_program=program)[0] expected = tuple(i.shape for i in expected) assert tape.shape(dev) == expected diff --git a/tests/templates/test_subroutines/test_approx_time_evolution.py b/tests/templates/test_subroutines/test_approx_time_evolution.py index 82203dbd926..93337c557a0 100644 --- a/tests/templates/test_subroutines/test_approx_time_evolution.py +++ b/tests/templates/test_subroutines/test_approx_time_evolution.py @@ -18,7 +18,6 @@ import numpy as np from pennylane import numpy as pnp import pennylane as qml -from pennylane.gradients.finite_difference import finite_diff # pylint: disable=protected-access @@ -389,7 +388,7 @@ def test_torch(self, tol): @pytest.mark.autograd @pytest.mark.parametrize( "dev_name,diff_method", - [["default.qubit.autograd", "backprop"], ["default.qubit", qml.gradients.param_shift]], + [["default.qubit", "backprop"], ["default.qubit", qml.gradients.param_shift]], ) def test_trainable_hamiltonian(dev_name, diff_method): """Test that the ApproxTimeEvolution template @@ -413,8 +412,9 @@ def cost(coeffs, t): if diff_method is qml.gradients.param_shift and dev_name != "default.qubit": tape = dev.expand_fn(tape) - - return qml.execute([tape], dev, diff_method)[0] + return qml.execute([tape], dev, diff_method)[0] + program, _ = dev.preprocess() + return qml.execute([tape], dev, gradient_fn=diff_method, transform_program=program)[0] t = pnp.array(0.54, requires_grad=True) coeffs = pnp.array([-0.6, 2.0], requires_grad=True) @@ -431,7 +431,12 @@ def cost(coeffs, t): assert grad[1].shape == tuple() # compare to finite-differences - tape = create_tape(coeffs, t) - g_tapes, fn = finite_diff(tape, _expand=False, validate_params=False) - expected = fn(qml.execute(g_tapes, dev, None)) - assert np.allclose(qml.math.hstack(grad), qml.math.stack(expected)) + + @qml.qnode(dev, diff_method="finite-diff") + def circuit(coeffs, t): + H = qml.Hamiltonian(coeffs, obs) + qml.ApproxTimeEvolution(H, t, 2) + return qml.expval(qml.PauliZ(0)) + + expected = qml.grad(circuit)(coeffs, t) + assert np.allclose(qml.math.hstack(grad), qml.math.hstack(expected)) diff --git a/tests/test_qnode.py b/tests/test_qnode.py index 0ec147e90b0..724ca8e1b7a 100644 --- a/tests/test_qnode.py +++ b/tests/test_qnode.py @@ -1679,24 +1679,8 @@ def circuit(x): return qml.expval(qml.PauliZ(0)) x = pnp.array(0.5, requires_grad=True) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") qml.grad(circuit)(x) - # check that the gradient recipe was applied *prior* to - # device expansion - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 1 - assert input_tape.operations[0].name == "UnsupportedOp" - assert input_tape.operations[0].data[0] == x - - shifted_tape1, shifted_tape2 = spy.spy_return[0] - - assert len(shifted_tape1.operations) == 1 - assert shifted_tape1.operations[0].name == "UnsupportedOp" - - assert len(shifted_tape2.operations) == 1 - assert shifted_tape2.operations[0].name == "UnsupportedOp" - # check second derivative assert np.allclose(qml.grad(qml.grad(circuit))(x), -9 * np.cos(3 * x)) @@ -1721,26 +1705,11 @@ def circuit(x): PhaseShift(x, wires=0) return qml.expval(qml.PauliX(0)) - spy = mocker.spy(circuit.device, "execute") x = pnp.array(0.5, requires_grad=True) circuit(x) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") res = qml.grad(circuit)(x) - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 2 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - - shifted_tape1, shifted_tape2 = spy.spy_return[0] - - assert len(shifted_tape1.operations) == 2 - assert shifted_tape1.operations[1].name == "RY" - - assert len(shifted_tape2.operations) == 2 - assert shifted_tape2.operations[1].name == "RY" - assert np.allclose(res, -3 * np.sin(3 * x)) # test second order derivatives diff --git a/tests/test_qnode_legacy.py b/tests/test_qnode_legacy.py index 4c728f78709..ad9ce49605b 100644 --- a/tests/test_qnode_legacy.py +++ b/tests/test_qnode_legacy.py @@ -1801,24 +1801,8 @@ def circuit(x): return qml.expval(qml.PauliZ(0)) x = pnp.array(0.5, requires_grad=True) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") qml.grad(circuit)(x) - # check that the gradient recipe was applied *prior* to - # device expansion - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 1 - assert input_tape.operations[0].name == "UnsupportedOp" - assert input_tape.operations[0].data[0] == x - - shifted_tape1, shifted_tape2 = spy.spy_return[0] - - assert len(shifted_tape1.operations) == 1 - assert shifted_tape1.operations[0].name == "UnsupportedOp" - - assert len(shifted_tape2.operations) == 1 - assert shifted_tape2.operations[0].name == "UnsupportedOp" - # check second derivative assert np.allclose(qml.grad(qml.grad(circuit))(x), -9 * np.cos(3 * x)) @@ -1843,26 +1827,11 @@ def circuit(x): PhaseShift(x, wires=0) return qml.expval(qml.PauliX(0)) - spy = mocker.spy(circuit.device, "batch_execute") x = pnp.array(0.5, requires_grad=True) circuit(x) - spy = mocker.spy(circuit.gradient_fn, "transform_fn") res = qml.grad(circuit)(x) - input_tape = spy.call_args[0][0] - assert len(input_tape.operations) == 2 - assert input_tape.operations[1].name == "RY" - assert input_tape.operations[1].data[0] == 3 * x - - shifted_tape1, shifted_tape2 = spy.spy_return[0] - - assert len(shifted_tape1.operations) == 2 - assert shifted_tape1.operations[1].name == "RY" - - assert len(shifted_tape2.operations) == 2 - assert shifted_tape2.operations[1].name == "RY" - assert np.allclose(res, -3 * np.sin(3 * x)) # test second order derivatives diff --git a/tests/test_return_types_dq2.py b/tests/test_return_types_dq2.py index 3acae93dc59..58494b14073 100644 --- a/tests/test_return_types_dq2.py +++ b/tests/test_return_types_dq2.py @@ -44,7 +44,14 @@ def circuit(x): if dev.shots: pytest.skip("cannot return analytic measurements with finite shots.") - res = qml.execute(tapes=[qnode.tape], device=dev, gradient_fn=None, interface=interface) + program, _ = dev.preprocess() + res = qml.execute( + tapes=[qnode.tape], + device=dev, + gradient_fn=None, + interface=interface, + transform_program=program, + ) assert res[0].shape == (2**wires,) assert isinstance(res[0], (np.ndarray, np.float64)) @@ -1219,7 +1226,8 @@ def return_type(self): tape = qml.tape.QuantumScript.from_queue(q) dev = qml.device("default.qubit", wires=3) with pytest.raises(qml.DeviceError, match="Analytic circuits must only contain"): - qml.execute(tapes=[tape], device=dev, gradient_fn=None) + program, _ = dev.preprocess() + qml.execute(tapes=[tape], device=dev, gradient_fn=None, transform_program=program) def test_state_return_with_other_types(self): """Test that an exception is raised when a state is returned along with another return @@ -1248,7 +1256,8 @@ def test_entropy_no_custom_wires(self): qml.vn_entropy(wires=["a"]) tape = qml.tape.QuantumScript.from_queue(q) - res = qml.execute(tapes=[tape], device=dev, gradient_fn=None) + program, _ = dev.preprocess() + res = qml.execute(tapes=[tape], device=dev, gradient_fn=None, transform_program=program) assert res == (0,) def test_custom_wire_labels_error(self): @@ -1261,5 +1270,6 @@ def test_custom_wire_labels_error(self): qml.mutual_info(wires0=["a"], wires1=["b"]) tape = qml.tape.QuantumScript.from_queue(q) - res = qml.execute(tapes=[tape], device=dev, gradient_fn=None) + program, _ = dev.preprocess() + res = qml.execute(tapes=[tape], device=dev, gradient_fn=None, transform_program=program) assert res == (0,) diff --git a/tests/test_vqe.py b/tests/test_vqe.py index a3472d59956..0b1834fb151 100644 --- a/tests/test_vqe.py +++ b/tests/test_vqe.py @@ -747,25 +747,6 @@ def test_optimize_grad_tf(self): assert np.allclose(dc, big_hamiltonian_grad) - @pytest.mark.parametrize("approx", [None, "block-diag", "diag"]) - def test_metric_tensor(self, approx): - """Test that the metric tensor can be calculated.""" - - dev = qml.device("default.qubit", wires=3) - p = pnp.array([1.0, 1.0, 1.0], requires_grad=True) - - def ansatz(params, **kwargs): - qml.RX(params[0], wires=0) - qml.RY(params[1], wires=0) - qml.CNOT(wires=[0, 1]) - qml.PhaseShift(params[2], wires=1) - - h = qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(1)]) - qnodes = catch_warn_ExpvalCost(ansatz, h, dev) - mt = qml.metric_tensor(qnodes, approx=approx)(p) # pylint:disable=not-callable - assert mt.shape == (3, 3) - assert isinstance(mt, pnp.ndarray) - def test_multiple_devices_opt_true(self): """Test if a ValueError is raised when multiple devices are passed when optimize=True.""" dev = [qml.device("default.qubit", wires=2), qml.device("default.qubit", wires=2)] diff --git a/tests/transforms/test_adjoint_metric_tensor.py b/tests/transforms/test_adjoint_metric_tensor.py index 04aa867ddec..f4987d077a4 100644 --- a/tests/transforms/test_adjoint_metric_tensor.py +++ b/tests/transforms/test_adjoint_metric_tensor.py @@ -272,16 +272,17 @@ def circuit(*params): return qml.expval(qml.PauliZ(wires[0])) circuit(*params) - mt = qml.adjoint_metric_tensor(circuit.qtape, dev) - expected = qml.math.reshape(expected, qml.math.shape(mt)) + + mt = qml.adjoint_metric_tensor(circuit)(*params) assert qml.math.allclose(mt, expected) - mt = qml.adjoint_metric_tensor(circuit, hybrid=False)(*params) + mt = qml.adjoint_metric_tensor(circuit.qtape) + expected = qml.math.reshape(expected, qml.math.shape(mt)) assert qml.math.allclose(mt, expected) @pytest.mark.jax - @pytest.mark.skip("JAX does not support forward pass executiong of the metric tensor.") - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.qubit.jax"]) + @pytest.mark.skip("JAX does not support forward pass execution of the metric tensor.") + @pytest.mark.parametrize("dev_name", ["default.qubit"]) def test_correct_output_tape_jax(self, dev_name, ansatz, params): """Test that the output is correct when using JAX and calling the adjoint metric tensor directly on a tape.""" @@ -302,18 +303,18 @@ def circuit(*params): return qml.expval(qml.PauliZ(0)) circuit(*j_params) - mt = qml.adjoint_metric_tensor(circuit.qtape, dev) + mt = qml.adjoint_metric_tensor(circuit.qtape) expected = qml.math.reshape(expected, qml.math.shape(mt)) assert qml.math.allclose(mt, expected) - mt = qml.adjoint_metric_tensor(circuit, hybrid=False)(*j_params) + mt = qml.adjoint_metric_tensor(circuit)(*j_params) assert qml.math.allclose(mt, expected) interfaces = ["auto", "torch"] @pytest.mark.torch @pytest.mark.parametrize("interface", interfaces) - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.qubit.torch"]) + @pytest.mark.parametrize("dev_name", ["default.qubit"]) def test_correct_output_tape_torch(self, ansatz, params, interface, dev_name): """Test that the output is correct when using Torch and calling the adjoint metric tensor directly on a tape.""" @@ -331,18 +332,18 @@ def circuit(*params): return qml.expval(qml.PauliZ(0)) circuit(*t_params) - mt = qml.adjoint_metric_tensor(circuit.qtape, dev) + mt = qml.adjoint_metric_tensor(circuit)(*t_params) + assert qml.math.allclose(mt, expected) + + mt = qml.adjoint_metric_tensor(circuit.qtape) expected = qml.math.reshape(expected, qml.math.shape(mt)) assert qml.math.allclose(mt.detach().numpy(), expected) - mt = qml.adjoint_metric_tensor(circuit, hybrid=False)(*t_params) - assert qml.math.allclose(mt, expected) - interfaces = ["auto", "tf"] @pytest.mark.tf @pytest.mark.parametrize("interface", interfaces) - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.qubit.tf"]) + @pytest.mark.parametrize("dev_name", ["default.qubit"]) def test_correct_output_tape_tf(self, ansatz, params, interface, dev_name): """Test that the output is correct when using TensorFlow and calling the adjoint metric tensor directly on a tape.""" @@ -361,13 +362,13 @@ def circuit(*params): with tf.GradientTape(): circuit(*t_params) - mt = qml.adjoint_metric_tensor(circuit.qtape, dev) + mt = qml.adjoint_metric_tensor(circuit.qtape) - expected = qml.math.reshape(expected, qml.math.shape(mt)) + with tf.GradientTape(): + mt = qml.adjoint_metric_tensor(circuit)(*t_params) assert qml.math.allclose(mt, expected) - with tf.GradientTape(): - mt = qml.adjoint_metric_tensor(circuit, hybrid=False)(*t_params) + expected = qml.math.reshape(expected, qml.math.shape(mt)) assert qml.math.allclose(mt, expected) @@ -402,7 +403,7 @@ def circuit(*params): assert qml.math.allclose(mt, expected) @pytest.mark.jax - @pytest.mark.skip("JAX does not support forward pass executiong of the metric tensor.") + @pytest.mark.skip("JAX does not support forward pass execution of the metric tensor.") @pytest.mark.parametrize("ansatz, params", list(zip(fubini_ansatze, fubini_params))) def test_correct_output_qnode_jax(self, ansatz, params): """Test that the output is correct when using JAX and @@ -435,7 +436,7 @@ def circuit(*params): @pytest.mark.torch @pytest.mark.parametrize("ansatz, params", list(zip(fubini_ansatze, fubini_params))) @pytest.mark.parametrize("interface", interfaces) - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.qubit.torch"]) + @pytest.mark.parametrize("dev_name", ["default.qubit"]) def test_correct_output_qnode_torch(self, ansatz, params, interface, dev_name): """Test that the output is correct when using Torch and calling the adjoint metric tensor on a QNode.""" @@ -488,31 +489,6 @@ def circuit(*params): else: assert qml.math.allclose(mt, expected) - @pytest.mark.autograd - @pytest.mark.parametrize("dev_name", ["default.qubit", "default.qubit.autograd"]) - def test_autograd_with_other_device(self, dev_name): - """Test passing an extra device to the QNode wrapper.""" - ansatz = fubini_ansatz2 - params = fubini_params[2] - - exp_fn = autodiff_metric_tensor(ansatz, self.num_wires) - expected = qml.jacobian(exp_fn)(*params) - dev = qml.device("default.qubit", wires=self.num_wires) - dev2 = qml.device(dev_name, wires=self.num_wires) - - @qml.qnode(dev) - def circuit(*params): - """Circuit with dummy output to create a QNode.""" - ansatz(*params, dev.wires) - return qml.expval(qml.PauliZ(0)) - - mt = qml.jacobian(qml.adjoint_metric_tensor(circuit, device=dev2))(*params) - - if isinstance(mt, tuple): - assert all(qml.math.allclose(_mt, _exp) for _mt, _exp in zip(mt, expected)) - else: - assert qml.math.allclose(mt, expected) - diff_fubini_ansatze = [ fubini_ansatz0, @@ -576,7 +552,7 @@ def circuit(*params): ansatz(*params, dev.wires) return qml.expval(qml.PauliZ(0)) - mt_fn = qml.adjoint_metric_tensor(circuit, hybrid=True) + mt_fn = qml.adjoint_metric_tensor(circuit) argnums = list(range(len(params))) mt_jac = jax.jacobian(mt_fn, argnums=argnums)(*j_params) @@ -641,43 +617,12 @@ def circuit(*params): assert qml.math.allclose(mt_jac, expected) -class TestErrors: - """Test that errors are raised correctly.""" - - def test_error_wrong_object_passed(self): - """Test that an error is raised if neither a tape nor a QNode is passed.""" - - def ansatz(x, y): - qml.RX(x, wires=0) - qml.RY(y, wires=1) - - dev = qml.device("default.qubit", wires=2) - - with pytest.raises(qml.QuantumFunctionError, match="The passed object is not a "): - qml.adjoint_metric_tensor(ansatz, device=dev) - - def test_error_finite_shots(self): - """Test that an error is raised if the device has a finite number of shots set.""" - with qml.queuing.AnnotatedQueue() as q: - qml.RX(0.2, wires=0) - qml.RY(1.9, wires=1) - tape = qml.tape.QuantumScript.from_queue(q, shots=1) - dev = qml.device("default.qubit", wires=2, shots=1) - - with pytest.raises(ValueError, match="The adjoint method for the metric tensor"): - qml.adjoint_metric_tensor(tape, device=dev) - - def test_warning_multiple_devices(self): - """Test that a warning is issued if an ExpvalCost with multiple - devices is passed.""" - dev1 = qml.device("default.qubit", wires=2) - dev2 = qml.device("default.qubit", wires=1) - H = qml.Hamiltonian([0.2, 0.9], [qml.PauliZ(0), qml.PauliY(0)]) - - def ansatz(x, wires): - qml.RX(x, wires=wires[0]) +def test_error_finite_shots(): + """Test that an error is raised if the device has a finite number of shots set.""" + with qml.queuing.AnnotatedQueue() as q: + qml.RX(0.2, wires=0) + qml.RY(1.9, wires=1) + tape = qml.tape.QuantumScript.from_queue(q, shots=1) - with pytest.warns(UserWarning, match="is deprecated,"): - cost = qml.ExpvalCost(ansatz, H, [dev1, dev2]) - with pytest.warns(UserWarning, match="ExpvalCost was instantiated"): - qml.adjoint_metric_tensor(cost) + with pytest.raises(ValueError, match="The adjoint method for the metric tensor"): + qml.adjoint_metric_tensor(tape) diff --git a/tests/transforms/test_batch_transform.py b/tests/transforms/test_batch_transform.py index eb2d671da38..146964ec6ac 100644 --- a/tests/transforms/test_batch_transform.py +++ b/tests/transforms/test_batch_transform.py @@ -648,7 +648,7 @@ def cost(x, weights): for g, e in zip(grad, expected): assert qml.math.allclose(g, e) - def test_batch_transforms_qnode(self, diff_method, mocker): + def test_batch_transforms_qnode(self, diff_method): """Test that batch transforms can be applied to a QNode without affecting device batch transforms""" if diff_method == "backprop": @@ -667,10 +667,8 @@ def circuit(weights): qml.CNOT(wires=[0, 1]) return qml.expval(H) - spy = mocker.spy(dev, "preprocess") - res = circuit(weights) - spy.assert_called() + assert np.allclose(res, [0, -np.sin(weights[1])], atol=0.1) diff --git a/tests/transforms/test_experimental/test_transform_dispatcher.py b/tests/transforms/test_experimental/test_transform_dispatcher.py index ec3a5afb3fd..9149a8f8a84 100644 --- a/tests/transforms/test_experimental/test_transform_dispatcher.py +++ b/tests/transforms/test_experimental/test_transform_dispatcher.py @@ -19,7 +19,6 @@ import pennylane as qml from pennylane.transforms.core import transform, TransformError -# TODO: Replace with default qubit 2 dev = qml.device("default.qubit", wires=2) with qml.tape.QuantumTape() as tape_circuit: @@ -352,6 +351,15 @@ def test_dispatcher_signature_non_valid_transform(self, non_valid_transform): with pytest.raises(TransformError): transform(non_valid_transform) + @pytest.mark.parametrize("valid_transform", valid_transforms) + def test_dispatcher_signature_classical_cotransform(self, valid_transform): + """Test that valid transforms with non-valid co transform raises a Transform error.""" + + with pytest.raises( + TransformError, match="The classical co-transform must be a valid Python function." + ): + transform(valid_transform, classical_cotransform=3) + def test_error_not_callable_transform(self): """Test that a non-callable is not a valid transforms.""" @@ -411,14 +419,6 @@ def test_multiple_args_expand_transform(self): ): transform(first_valid_transform, expand_transform=non_valid_expand_transform) - def test_cotransform_not_implemented(self): - """Test that a co-transform must be a callable.""" - - with pytest.raises( - NotImplementedError, match="Classical cotransforms are not yet integrated." - ): - transform(first_valid_transform, classical_cotransform=non_callable) - def test_qfunc_transform_multiple_tapes(self): """Test that quantum function is not compatible with multiple tapes.""" dispatched_transform = transform(second_valid_transform) diff --git a/tests/transforms/test_experimental/test_transform_program.py b/tests/transforms/test_experimental/test_transform_program.py index 4926411d0f7..bd0922a6aee 100644 --- a/tests/transforms/test_experimental/test_transform_program.py +++ b/tests/transforms/test_experimental/test_transform_program.py @@ -465,7 +465,7 @@ def test_insert_transform_with_expand(self): assert transform_program[1].transform is first_valid_transform def test_valid_transforms(self): - """Test that that it is only possible to create valid transforms.""" + """Test that it is only possible to create valid transforms.""" transform_program = TransformProgram() transform1 = TransformContainer(transform=first_valid_transform, is_informative=True) transform_program.push_back(transform1) @@ -500,21 +500,6 @@ def test_call_on_empty_program(self): obj = [1, 2, 3, "b"] assert null_postprocessing(obj) is obj - def test_cotransform_support_notimplemented(self): - """Test that a transform with a cotransform raises a not implemented error.""" - - my_transform = TransformContainer( - first_valid_transform, classical_cotransform=lambda res: res - ) - - prog = TransformProgram((my_transform,)) - - batch = (qml.tape.QuantumScript([], [qml.state()]),) - with pytest.raises( - NotImplementedError, match="cotransforms are not yet integrated with TransformProgram" - ): - prog(batch) - def test_single_transform_program(self): """Basic test with a single transform that only modifies the tape but not the results.""" diff --git a/tests/transforms/test_metric_tensor.py b/tests/transforms/test_metric_tensor.py index 1b4c6db5807..8037735801a 100644 --- a/tests/transforms/test_metric_tensor.py +++ b/tests/transforms/test_metric_tensor.py @@ -77,26 +77,6 @@ def circuit(a, b): assert qml.math.shape(result[0]) == () assert qml.math.shape(result[1]) == () - @pytest.mark.parametrize("diff_method", ["parameter-shift", "backprop"]) - def test_parameter_fan_out(self, diff_method): - """The metric tensor is with respect to the quantum circuit and ignores - classical processing if ``hybrid=False``. As a result, if there is - parameter fan-out, the returned metric tensor will be larger than - ``(len(args), len(args))`` if hybrid computation is deactivated. - """ - dev = qml.device("default.qubit", wires=2) - - def circuit(a): - qml.RX(a, wires=0) - qml.RX(a, wires=0) - return qml.expval(qml.PauliX(0)) - - circuit = qml.QNode(circuit, dev, diff_method=diff_method) - params = np.array([0.1], requires_grad=True) - # pylint:disable=unexpected-keyword-arg - result = qml.metric_tensor(circuit, hybrid=False, approx="block-diag")(*params) - assert result.shape == (2, 2) - def test_construct_subcircuit(self): """Test correct subcircuits constructed""" with qml.queuing.AnnotatedQueue() as q: @@ -231,13 +211,12 @@ def circuit(abc): assert qml.math.allclose(g_diag, np.diag(expected), atol=tol, rtol=0) assert qml.math.allclose(g_blockdiag, np.diag(expected), atol=tol, rtol=0) - @pytest.mark.parametrize("strategy", ["gradient", "device"]) - def test_template_integration(self, strategy): + def test_template_integration(self): """Test that the metric tensor transform acts on QNodes correctly when the QNode contains a template""" dev = qml.device("default.qubit", wires=3) - @qml.qnode(dev, expansion_strategy=strategy) + @qml.qnode(dev) def circuit(weights): qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1, 2]) return qml.probs(wires=[0, 1]) @@ -858,10 +837,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="tensor of a QNode with no trainable parameters"): - res = qml.metric_tensor(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.metric_tensor(circuit)(weights) @pytest.mark.torch @pytest.mark.parametrize("interface", ["auto", "torch"]) @@ -878,10 +855,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="tensor of a QNode with no trainable parameters"): - res = qml.metric_tensor(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.metric_tensor(circuit)(weights) @pytest.mark.tf @pytest.mark.parametrize("interface", ["auto", "tf"]) @@ -898,10 +873,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="tensor of a QNode with no trainable parameters"): - res = qml.metric_tensor(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.metric_tensor(circuit)(weights) @pytest.mark.jax @pytest.mark.parametrize("interface", ["auto", "jax"]) @@ -918,10 +891,8 @@ def circuit(weights): return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) weights = [0.1, 0.2] - with pytest.warns(UserWarning, match="tensor of a QNode with no trainable parameters"): - res = qml.metric_tensor(circuit)(weights) - - assert res == () + with pytest.raises(qml.QuantumFunctionError, match="No trainable parameters."): + qml.metric_tensor(circuit)(weights) def test_no_trainable_params_tape(self): """Test that the correct ouput and warning is generated in the absence of any trainable @@ -1621,7 +1592,9 @@ def circuit(x, z): x = np.array(0.5, requires_grad=True) z = np.array(0.1, requires_grad=True) - with pytest.warns(UserWarning, match="The device does not have a wire that is not used"): + with pytest.raises( + qml.wires.WireError, match="The device has no free wire for the auxiliary wire." + ): qml.metric_tensor(circuit, approx=None)(x, z) @@ -1637,34 +1610,14 @@ def circuit(x): qml.RY(x, wires=0) return qml.expval(qml.PauliZ(0)) - with pytest.warns(UserWarning, match="An auxiliary wire is not available."): + with pytest.raises( + qml.wires.WireError, match="The requested auxiliary wire does not exist on the used device." + ): qml.metric_tensor(circuit, aux_wire=404)(x) -@pytest.mark.filterwarnings("ignore:An auxiliary wire is not") -def test_error_aux_wire_replaced(): - """Tests that even if an aux_wire is provided, it is superseded by a device - wire if it does not exist itself on the device, so that the metric_tensor is - successfully computed.""" - dev = qml.device("default.qubit", wires=qml.wires.Wires(["wire1", "wire2", "hidden_wire"])) - - @qml.qnode(dev) - def circuit(x, z): - qml.RX(x, wires="wire1") - qml.RZ(z, wires="wire2") - qml.CNOT(wires=["wire1", "wire2"]) - qml.RX(x, wires="wire1") - qml.RZ(z, wires="wire2") - return qml.expval(qml.PauliZ("wire2")) - - x = np.array(0.5, requires_grad=True) - z = np.array(0.1, requires_grad=True) - - qml.metric_tensor(circuit, approx=None, aux_wire="wire3")(x, z) - - @pytest.mark.parametrize("allow_nonunitary", [True, False]) -def test_error_generator_not_registered(allow_nonunitary, monkeypatch): +def test_error_generator_not_registered(allow_nonunitary): """Tests that an error is raised if an operation doe not have a controlled-generator operation registered.""" dev = qml.device("default.qubit", wires=qml.wires.Wires(["wire1", "wire2", "wire3"])) @@ -1672,22 +1625,6 @@ def test_error_generator_not_registered(allow_nonunitary, monkeypatch): x = np.array(0.5, requires_grad=True) z = np.array(0.1, requires_grad=True) - @qml.qnode(dev) - def circuit0(x, z): - qml.CRX(x, wires=["wire1", "wire2"]) - qml.RZ(z, wires="wire2") - return qml.expval(qml.PauliZ("wire2")) - - with monkeypatch.context() as m: - exp_fn = lambda tape, *args, **kwargs: tape - m.setattr("pennylane.transforms.metric_tensor.expand_fn", exp_fn) - - if allow_nonunitary: - qml.metric_tensor(circuit0, approx=None, allow_nonunitary=allow_nonunitary)(x, z) - else: - with pytest.raises(ValueError, match="Generator for operation"): - qml.metric_tensor(circuit0, approx=None, allow_nonunitary=allow_nonunitary)(x, z) - class RX(qml.RX): def generator(self): return qml.Hadamard(self.wires) @@ -1698,15 +1635,11 @@ def circuit1(x, z): qml.RZ(z, wires="wire1") return qml.expval(qml.PauliZ("wire2")) - with monkeypatch.context() as m: - exp_fn = lambda tape, *args, **kwargs: tape - m.setattr("pennylane.transforms.metric_tensor.expand_fn", exp_fn) - - if allow_nonunitary: + if allow_nonunitary: + qml.metric_tensor(circuit1, approx=None, allow_nonunitary=allow_nonunitary)(x, z) + else: + with pytest.raises(ValueError, match="Generator for operation"): qml.metric_tensor(circuit1, approx=None, allow_nonunitary=allow_nonunitary)(x, z) - else: - with pytest.raises(ValueError, match="Generator for operation"): - qml.metric_tensor(circuit1, approx=None, allow_nonunitary=allow_nonunitary)(x, z) def test_no_error_missing_aux_wire_not_used(recwarn): diff --git a/tests/transforms/test_specs.py b/tests/transforms/test_specs.py index 10c499be588..0904ddb99c3 100644 --- a/tests/transforms/test_specs.py +++ b/tests/transforms/test_specs.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Unit tests for the specs transform""" +from typing import Sequence, Callable from collections import defaultdict from contextlib import nullcontext import pytest @@ -53,7 +54,9 @@ def circ(): if diff_method == "parameter-shift": assert info["num_gradient_executions"] == 0 - assert info["gradient_fn"] == "pennylane.gradients.parameter_shift.param_shift" + assert ( + info["gradient_fn"] == "pennylane.transforms.core.transform_dispatcher.param_shift" + ) @pytest.mark.parametrize( "diff_method, len_info", [("backprop", 11), ("parameter-shift", 12), ("adjoint", 11)] @@ -184,15 +187,15 @@ def circuit(): with pytest.warns(UserWarning, match="gradient of a tape with no trainable parameters"): info = qml.specs(circuit)() - assert info["diff_method"] == "pennylane.gradients.parameter_shift.param_shift" - assert info["gradient_fn"] == "pennylane.gradients.parameter_shift.param_shift" + assert info["diff_method"] == "pennylane.transforms.core.transform_dispatcher.param_shift" + assert info["gradient_fn"] == "pennylane.transforms.core.transform_dispatcher.param_shift" def test_custom_gradient_transform(self): """Test that a custom gradient transform is properly labelled""" dev = qml.device("default.qubit", wires=2) - @qml.gradients.gradient_transform - def my_transform(tape): + @qml.transforms.core.transform + def my_transform(tape: qml.tape.QuantumTape) -> (Sequence[qml.tape.QuantumTape], Callable): return tape, None @qml.qnode(dev, diff_method=my_transform) @@ -200,8 +203,8 @@ def circuit(): return qml.probs(wires=0) info = qml.specs(circuit)() - assert info["diff_method"] == "test_specs.my_transform" - assert info["gradient_fn"] == "test_specs.my_transform" + assert info["diff_method"] == "pennylane.transforms.core.transform_dispatcher.my_transform" + assert info["gradient_fn"] == "pennylane.transforms.core.transform_dispatcher.my_transform" @pytest.mark.parametrize( "device,num_wires", From 4c3ab729c0715c0a78a0e9b3454a650f0e7e7096 Mon Sep 17 00:00:00 2001 From: Christina Lee Date: Mon, 16 Oct 2023 14:15:59 -0400 Subject: [PATCH 7/8] Restructures `preprocess` to include building block, extensible transforms (#4659) The current `devices.qubit.preprocess` module has several transforms optimized for default qubit. Other devices would need to rewrite similar code in order to extend the behavior. This PR restructures the module so that `devices.preprocess` now offers the following transforms: * `decompose`: this accepts a stopping condition and decomposes operations until the stopping condition is met * `validate_measurements`: this accepts a stopping_condition for observables. It validates all observables are accepted, only sample based measurements are present when sampling, and only state based measurements are present when analytic * `validate_device_wires`: This makes sure device wire constraints are met, and fills in all wires for measurements like `StateMP` and `ProbabilityMP` that act on all available wires. * `validate_multiprocessing_workers`: Checks that the CPU is not oversubscribed for a given number of workers. * `warn_about_trainable_observables`: raises a warning is any observable are trainable For example, a plugin with a list of supported operations can use the `decompose` transform by doing: ``` def stopping_condition(op: qml.operation.Operator) -> bool: return op.name in supported_operations transform_program.add_transform(decompose, stopping_condition=stopping_condition) ``` **Overview of all the changes:** So this PR ended up changing a lot of lines of code, but most of them are moving tests around. As the tests for default qubit started being quite extensive, I added a folder `tests/devices/default_qubit` and moved the default qubit tests there over two files. Any test for default qubit specific preprocessing went into `tests/devices/default_qubit/test_default_qubit_preprocessing.py`. More general preprocessing tests are in `tests/devices/test_preprocessing.py`. Since the transforms are no longer default qubit specific, they are moved out of the `qubit` folder. Since `DefaultQubit.preprocess` was getting rather long, I split out two private helper methods, `_add_adjoint_transforms` and `_setup_execution_config`. This change just keeps `DefaultQubit.preprocess` a little more manage able. --------- Co-authored-by: Amintor Dusko <87949283+AmintorDusko@users.noreply.github.com> Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com> --- doc/releases/changelog-dev.md | 8 + pennylane/devices/__init__.py | 30 + pennylane/devices/default_qubit.py | 180 +++- pennylane/devices/preprocess.py | 404 +++++++++ pennylane/devices/qubit/__init__.py | 2 - pennylane/devices/qubit/preprocess.py | 449 ---------- pennylane/transforms/defer_measurements.py | 23 +- .../test_default_qubit.py} | 333 +------ .../test_default_qubit_preprocessing.py | 820 +++++++++++++++++ tests/devices/qubit/test_adjoint_jacobian.py | 48 +- tests/devices/qubit/test_preprocess.py | 842 ------------------ tests/devices/test_preprocess.py | 397 +++++++++ tests/docs/test_supported_confs.py | 37 +- tests/measurements/test_classical_shadow.py | 4 +- tests/measurements/test_measurements.py | 6 +- tests/measurements/test_mutual_info.py | 4 +- tests/measurements/test_vn_entropy.py | 4 +- tests/ops/qubit/test_hamiltonian.py | 2 +- tests/tape/test_tape.py | 4 +- tests/test_qnode.py | 14 +- tests/test_qnode_legacy.py | 4 +- tests/test_return_types_dq2.py | 4 +- tests/transforms/test_defer_measurements.py | 14 +- .../test_transform_dispatcher.py | 4 +- 24 files changed, 1964 insertions(+), 1673 deletions(-) create mode 100644 pennylane/devices/preprocess.py delete mode 100644 pennylane/devices/qubit/preprocess.py rename tests/devices/{experimental/test_default_qubit_2.py => default_qubit/test_default_qubit.py} (85%) create mode 100644 tests/devices/default_qubit/test_default_qubit_preprocessing.py delete mode 100644 tests/devices/qubit/test_preprocess.py create mode 100644 tests/devices/test_preprocess.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index d327f2829b3..1ac65dd0770 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -84,6 +84,14 @@

Improvements 🛠

+* `pennylane.devices.preprocess` now offers the transforms `decompose`, `validate_observables`, `validate_measurements`, + `validate_device_wires`, `validate_multiprocessing_workers`, `warn_about_trainable_observables`, + and `no_sampling` to assist in the construction of devices under the new `devices.Device` API. + [(#4659)](https://github.com/PennyLaneAI/pennylane/pull/4659) + +* `pennylane.defer_measurements` will now exit early if the input does not contain mid circuit measurements. + [(#4659)](https://github.com/PennyLaneAI/pennylane/pull/4659) + * `default.qubit` now tracks the number of equivalent qpu executions and total shots when the device is sampling. Note that `"simulations"` denotes the number of simulation passes, where as `"executions"` denotes how many different computational bases need to be sampled in. Additionally, the diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index 870c9a5eb4f..49f28211990 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -52,6 +52,36 @@ Device DefaultQubit +Preprocessing Transforms +------------------------ + +The ``preprocess`` module offers several transforms that can be used in constructing the :meth:`~.devices.Device.preprocess` +method for devices. + +.. currentmodule:: pennylane.devices.preprocess +.. autosummary:: + :toctree: api + + decompose + validate_observables + validate_measurements + validate_device_wires + validate_multiprocessing_workers + warn_about_trainable_observables + no_sampling + +Other transforms that may be relevant to device preprocessing include: + +.. currentmodule:: pennylane +.. autosummary:: + :toctree: api + + defer_measurements + transforms.broadcast_expand + transforms.sum_expand + transforms.split_non_commuting + transforms.hamiltonian_expand + Qubit Simulation Tools ---------------------- diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index 13f1c3d6d89..2453fe8a284 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -15,6 +15,7 @@ This module contains the next generation successor to default qubit """ +from dataclasses import replace from functools import partial from numbers import Number from typing import Union, Callable, Tuple, Optional, Sequence @@ -28,15 +29,18 @@ from pennylane.transforms.core import TransformProgram from . import Device -from .execution_config import ExecutionConfig, DefaultExecutionConfig -from .qubit.simulate import simulate, get_final_state, measure_final_state -from .qubit.sampling import get_num_shots_and_executions -from .qubit.preprocess import ( - preprocess, - validate_and_expand_adjoint, +from .preprocess import ( + decompose, + validate_observables, + validate_measurements, validate_multiprocessing_workers, validate_device_wires, + warn_about_trainable_observables, + no_sampling, ) +from .execution_config import ExecutionConfig, DefaultExecutionConfig +from .qubit.simulate import simulate, get_final_state, measure_final_state +from .qubit.sampling import get_num_shots_and_executions from .qubit.adjoint_jacobian import adjoint_jacobian, adjoint_vjp, adjoint_jvp Result_or_ResultBatch = Union[Result, ResultBatch] @@ -46,6 +50,95 @@ PostprocessingFn = Callable[[ResultBatch], Result_or_ResultBatch] +observables = { + "PauliX", + "PauliY", + "PauliZ", + "Hadamard", + "Hermitian", + "Identity", + "Projector", + "SparseHamiltonian", + "Hamiltonian", + "Sum", + "SProd", + "Prod", + "Exp", + "Evolution", +} + + +def observable_stopping_condition(obs: qml.operation.Operator) -> bool: + """Specifies whether or not an observable is accepted by DefaultQubit.""" + return obs.name in observables + + +def stopping_condition(op: qml.operation.Operator) -> bool: + """Specify whether or not an Operator object is supported by the device.""" + if op.name == "QFT" and len(op.wires) >= 6: + return False + if op.name == "GroverOperator" and len(op.wires) >= 13: + return False + if op.name == "Snapshot": + return True + if op.__class__.__name__ == "Pow" and qml.operation.is_trainable(op): + return False + + return op.has_matrix + + +def accepted_sample_measurement(m: qml.measurements.MeasurementProcess) -> bool: + """Specifies whether or not a measurement is accepted when sampling.""" + return isinstance( + m, + ( + qml.measurements.SampleMeasurement, + qml.measurements.ClassicalShadowMP, + qml.measurements.ShadowExpvalMP, + ), + ) + + +def _add_adjoint_transforms(program: TransformProgram) -> None: + """Private helper function for ``preprocess`` that adds the transforms specific + for adjoint differentiation. + + Args: + program (TransformProgram): where we will add the adjoint differentiation transforms + + Side Effects: + Adds transforms to the input program. + + """ + + def adjoint_ops(op: qml.operation.Operator) -> bool: + """Specify whether or not an Operator is supported by adjoint differentiation.""" + return op.num_params == 0 or op.num_params == 1 and op.has_generator + + def adjoint_observables(obs: qml.operation.Operator) -> bool: + """Specifies whether or not an observable is compatible with adjoint differentiation on DefaultQubit.""" + return obs.has_matrix + + def accepted_adjoint_measurement(m: qml.measurements.MeasurementProcess) -> bool: + return isinstance(m, qml.measurements.ExpectationMP) + + name = "adjoint + default.qubit" + program.add_transform(no_sampling, name=name) + program.add_transform( + decompose, + stopping_condition=adjoint_ops, + name=name, + ) + program.add_transform(validate_observables, adjoint_observables, name=name) + program.add_transform( + validate_measurements, + analytic_measurements=accepted_adjoint_measurement, + name=name, + ) + program.add_transform(qml.transforms.broadcast_expand) + program.add_transform(warn_about_trainable_observables) + + class DefaultQubit(Device): """A PennyLane device written in Python and capable of backpropagation derivatives. @@ -246,18 +339,28 @@ def supports_derivatives( ): return True - if execution_config.gradient_method == "adjoint" and execution_config.use_device_gradient: + if ( + execution_config.gradient_method == "adjoint" + and execution_config.use_device_gradient is not False + ): if circuit is None: return True - return isinstance(validate_and_expand_adjoint(circuit)[0][0], QuantumScript) + prog = TransformProgram() + _add_adjoint_transforms(prog) + + try: + prog((circuit,)) + except (qml.operation.DecompositionUndefinedError, qml.DeviceError): + return False + return True return False def preprocess( self, execution_config: ExecutionConfig = DefaultExecutionConfig, - ) -> Tuple[QuantumTapeBatch, PostprocessingFn, ExecutionConfig]: + ) -> Tuple[TransformProgram, ExecutionConfig]: """This function defines the device transform program to be applied and an updated device configuration. Args: @@ -276,19 +379,64 @@ def preprocess( * Currently does not intrinsically support parameter broadcasting """ + config = self._setup_execution_config(execution_config) transform_program = TransformProgram() - # Validate device wires - transform_program.add_transform(validate_device_wires, self) + + transform_program.add_transform(qml.defer_measurements) + transform_program.add_transform(validate_device_wires, self.wires, name=self.name) + transform_program.add_transform( + decompose, stopping_condition=stopping_condition, name=self.name + ) + transform_program.add_transform( + validate_measurements, sample_measurements=accepted_sample_measurement, name=self.name + ) + transform_program.add_transform( + validate_observables, stopping_condition=observable_stopping_condition, name=self.name + ) # Validate multi processing - max_workers = execution_config.device_options.get("max_workers", self._max_workers) - transform_program.add_transform(validate_multiprocessing_workers, max_workers, self) + max_workers = config.device_options.get("max_workers", self._max_workers) + if max_workers: + transform_program.add_transform(validate_multiprocessing_workers, max_workers, self) + + if config.gradient_method == "backprop": + transform_program.add_transform(no_sampling, name="backprop + default.qubit") + + if config.gradient_method == "adjoint": + _add_adjoint_transforms(transform_program) - # General preprocessing (Validate measurement, expand, adjoint expand, broadcast expand) - transform_program_preprocess, config = preprocess(execution_config=execution_config) - transform_program = transform_program + transform_program_preprocess return transform_program, config + def _setup_execution_config(self, execution_config: ExecutionConfig) -> ExecutionConfig: + """This is a private helper for ``preprocess`` that sets up the execution config. + + Args: + execution_config (ExecutionConfig) + + Returns: + ExecutionConfig: a preprocessed execution config + + """ + updated_values = {} + if execution_config.gradient_method == "best": + updated_values["gradient_method"] = "backprop" + if execution_config.use_device_gradient is None: + updated_values["use_device_gradient"] = execution_config.gradient_method in { + "best", + "adjoint", + "backprop", + } + if execution_config.grad_on_execution is None: + updated_values["grad_on_execution"] = execution_config.gradient_method == "adjoint" + updated_values["device_options"] = dict(execution_config.device_options) # copy + if "max_workers" not in updated_values["device_options"]: + updated_values["device_options"]["max_workers"] = self._max_workers + if "rng" not in updated_values["device_options"]: + updated_values["device_options"]["rng"] = self._rng + if "prng_key" not in updated_values["device_options"]: + updated_values["device_options"]["prng_key"] = self._prng_key + return replace(execution_config, **updated_values) + def execute( self, circuits: QuantumTape_or_Batch, diff --git a/pennylane/devices/preprocess.py b/pennylane/devices/preprocess.py new file mode 100644 index 00000000000..b2ee416414f --- /dev/null +++ b/pennylane/devices/preprocess.py @@ -0,0 +1,404 @@ +# Copyright 2018-2023 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains functions for preprocessing `QuantumTape` objects to ensure +that they are supported for execution by a device.""" +# pylint: disable=protected-access +import os +from typing import Generator, Callable, Union, Sequence, Optional +from copy import copy +import warnings + +import pennylane as qml +from pennylane import Snapshot +from pennylane.operation import Tensor, StatePrepBase +from pennylane.measurements import ( + StateMeasurement, + SampleMeasurement, +) +from pennylane.typing import ResultBatch, Result +from pennylane import DeviceError +from pennylane.transforms.core import transform +from pennylane.wires import WireError + +PostprocessingFn = Callable[[ResultBatch], Union[Result, ResultBatch]] + + +def null_postprocessing(results): + """A postprocessing function returned by a transform that only converts the batch of results + into a result for a single ``QuantumTape``. + """ + return results[0] + + +def _operator_decomposition_gen( + op: qml.operation.Operator, + acceptance_function: Callable[[qml.operation.Operator], bool], + name: str = "device", +) -> Generator[qml.operation.Operator, None, None]: + """A generator that yields the next operation that is accepted.""" + if acceptance_function(op): + yield op + else: + try: + decomp = op.decomposition() + except qml.operation.DecompositionUndefinedError as e: + raise DeviceError( + f"Operator {op} not supported on {name} and does not provide a decomposition." + ) from e + + for sub_op in decomp: + yield from _operator_decomposition_gen(sub_op, acceptance_function, name) + + +####################### + + +@transform +def no_sampling( + tape: qml.tape.QuantumTape, name: str = "device" +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Raises an error if the tape has finite shots. + + Args: + tape (QuantumTape): a quantum circuit + name="device" (str): name to use in error message. + + This transform can be added to forbid finite shots. For example, ``default.qubit`` uses it for + adjoint and backprop validation. + """ + if tape.shots: + raise qml.DeviceError(f"Finite shots are not supported with {name}") + return (tape,), null_postprocessing + + +@transform +def validate_device_wires( + tape: qml.tape.QuantumTape, wires: Optional[qml.wires.Wires] = None, name: str = "device" +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Validates that all wires present in the tape are in the set of provided wires. Adds the + device wires to measurement processes like :class:`~.measurements.StateMP` that are broadcasted + across all available wires. + + Args: + tape (QuantumTape): a quantum circuit. + wires=None (Optional[Wires]): the allowed wires. Wires of ``None`` allows any wires + to be present in the tape. + name="device" (str): the name of the device to use in error messages. + + Returns: + pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, + it returns a QNode with the transform added to its transform program. + If a tape is passed, returns a tuple containing a list of + quantum tapes to be evaluated, and a function to be applied to these + tape executions. + + Raises: + WireError: if the tape has a wire not present in the provided wires. + """ + if wires: + if extra_wires := set(tape.wires) - set(wires): + raise WireError( + f"Cannot run circuit(s) on {name} as they contain wires " + f"not found on the device: {extra_wires}" + ) + measurements = tape.measurements.copy() + modified = False + for m_idx, mp in enumerate(measurements): + if not mp.obs and not mp.wires: + modified = True + new_mp = copy(mp) + new_mp._wires = wires # pylint:disable=protected-access + measurements[m_idx] = new_mp + if modified: + tape = type(tape)(tape.operations, measurements, shots=tape.shots) + + return (tape,), null_postprocessing + + +@transform +def validate_multiprocessing_workers( + tape: qml.tape.QuantumTape, max_workers: int, device +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Validates the number of workers for multiprocessing. + + Checks that the CPU is not oversubscribed and warns user if it is, + making suggestions for the number of workers and/or the number of + threads per worker. + + Args: + tape (QuantumTape): a quantum circuit. + max_workers (int): Maximal number of multiprocessing workers + device (pennylane.devices.Device): The device to be checked. + + Returns: + pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, + it returns a QNode with the transform added to its transform program. + If a tape is passed, returns a tuple containing a list of + quantum tapes to be evaluated, and a function to be applied to these + tape executions. + """ + if max_workers is not None: + threads_per_proc = os.cpu_count() # all threads by default + varname = "OMP_NUM_THREADS" + varnames = ["MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS"] + for var in varnames: + if os.getenv(var): # pragma: no cover + varname = var + threads_per_proc = int(os.getenv(var)) + break + num_threads = threads_per_proc * max_workers + num_cpu = os.cpu_count() + num_threads_suggest = max(1, os.cpu_count() // max_workers) + num_workers_suggest = max(1, os.cpu_count() // threads_per_proc) + if num_threads > num_cpu: + warnings.warn( + f"""The device requested {num_threads} threads ({max_workers} processes + times {threads_per_proc} threads per process), but the processor only has + {num_cpu} logical cores. The processor is likely oversubscribed, which may + lead to performance deterioration. Consider decreasing the number of processes, + setting the device or execution config argument `max_workers={num_workers_suggest}` + for example, or decreasing the number of threads per process by setting the + environment variable `{varname}={num_threads_suggest}`.""", + UserWarning, + ) + + if device._debugger and device._debugger.active: + raise DeviceError("Debugging with ``Snapshots`` is not available with multiprocessing.") + + if any(isinstance(op, Snapshot) for op in tape.operations): + raise RuntimeError( + """ProcessPoolExecutor cannot execute a QuantumScript with + a ``Snapshot`` operation. Change the value of ``max_workers`` + to ``None`` or execute the QuantumScript separately.""" + ) + + return (tape,), null_postprocessing + + +@transform +def warn_about_trainable_observables( + tape: qml.tape.QuantumTape, +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Raises a warning if any of the observables is trainable. Can be used in validating circuits + for adjoint differentiation. + """ + + for k in tape.trainable_params: + if hasattr(tape._par_info[k]["op"], "return_type"): + warnings.warn( + "Differentiating with respect to the input parameters of " + f"{tape._par_info[k]['op'].name} is not supported with the " + "adjoint differentiation method. Gradients are computed " + "only with regards to the trainable parameters of the circuit.\n\n Mark " + "the parameters of the measured observables as non-trainable " + "to silence this warning.", + UserWarning, + ) + return (tape,), null_postprocessing + + +@transform +def decompose( + tape: qml.tape.QuantumTape, + stopping_condition: Callable[[qml.operation.Operator], bool], + skip_initial_state_prep: bool = True, + name: str = "device", +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Decompose operations until the stopping condition is met. + + Args: + tape (QuantumTape): a quantum circuit. + stopping_condition (Callable): a function from an operator to a boolean. If ``False``, the operator + should be decomposed. If an operator cannot be decomposed and is not accepted by ``stopping_condition``, + a ``DecompositionUndefinedError`` will be raised. + skip_initial_state_prep=True (bool): If ``True``, the first operator will not be decomposed if it inherits from :class:`~.StatePrepBase`. + + Returns: + pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, + it returns a QNode with the transform added to its transform program. + If a tape is passed, returns a tuple containing a list of + quantum tapes to be evaluated, and a function to be applied to these + tape executions. + + Raises: + DecompositionUndefinedError: if an operator is not accepted and does not define a decomposition + + DeviceError: If the decomposition enters and infinite loop and raises a ``RecursionError``. + + **Example:** + + >>> def stopping_condition(obj): + ... return obj.name in {"CNOT", "RX", "RZ"} + >>> tape = qml.tape.QuantumScript([qml.IsingXX(1.2, wires=(0,1))], [qml.expval(qml.PauliZ(0))]) + >>> batch, fn = decompose(tape, stopping_condition) + >>> batch[0].circuit + [CNOT(wires=[0, 1]), + RX(1.2, wires=[0]), + CNOT(wires=[0, 1]), + expval(PauliZ(wires=[0]))] + + If an operator cannot be decomposed into a supported operation, an error is raised: + + >>> decompose(tape, lambda obj: obj.name == "S") + DeviceError: Operator CNOT(wires=[0, 1]) not supported on device and does not provide a decomposition. + + The ``skip_initial_state_prep`` specifies whether or not the device supports state prep operations + at the beginning of the circuit. + + >>> tape = qml.tape.QuantumScript([qml.BasisState([1], wires=0), qml.BasisState([1], wires=1)]) + >>> batch, fn = decompose(tape, stopping_condition) + >>> batch[0].circuit + [BasisState(array([1]), wires=[0]), + RZ(1.5707963267948966, wires=[1]), + RX(3.141592653589793, wires=[1]), + RZ(1.5707963267948966, wires=[1])] + >>> batch, fn = decompose(tape, stopping_condition, skip_initial_state_prep=False) + >>> batch[0].circuit + [RZ(1.5707963267948966, wires=[0]), + RX(3.141592653589793, wires=[0]), + RZ(1.5707963267948966, wires=[0]), + RZ(1.5707963267948966, wires=[1]), + RX(3.141592653589793, wires=[1]), + RZ(1.5707963267948966, wires=[1])] + + """ + + if not all(stopping_condition(op) for op in tape.operations): + try: + # don't decompose initial operations if its StatePrepBase + prep_op = ( + [tape[0]] if isinstance(tape[0], StatePrepBase) and skip_initial_state_prep else [] + ) + + new_ops = [ + final_op + for op in tape.operations[bool(prep_op) :] + for final_op in _operator_decomposition_gen(op, stopping_condition, name) + ] + except RecursionError as e: + raise DeviceError( + "Reached recursion limit trying to decompose operations. " + "Operator decomposition may have entered an infinite loop." + ) from e + tape = qml.tape.QuantumScript(prep_op + new_ops, tape.measurements, shots=tape.shots) + + return (tape,), null_postprocessing + + +@transform +def validate_observables( + tape: qml.tape.QuantumTape, + stopping_condition: Callable[[qml.operation.Operator], bool], + name: str = "device", +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Validates the observables and measurements for a circuit. + + Args: + tape (QuantumTape): a quantum circuit. + stopping_condition (callable): a function that specifies whether or not an observable is accepted. + name (str): the name of the device to use in error messages. + + Returns: + pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, + it returns a QNode with the transform added to its transform program. + If a tape is passed, returns a tuple containing a list of + quantum tapes to be evaluated, and a function to be applied to these + tape executions. + + Raises: + DeviceError: if an observable is not supported + + **Example:** + + >>> def accepted_observable(obj): + ... return obj.name in {"PauliX", "PauliY", "PauliZ"} + >>> tape = qml.tape.QuantumScript([], [qml.expval(qml.PauliZ(0) + qml.PauliY(0))]) + >>> validate_observables(tape, accepted_observable) + DeviceError: Observable not supported on device + + Note that if the observable is a :class:`~.Tensor`, the validation is run on each object in the + ``Tensor`` instead. + + """ + for m in tape.measurements: + if m.obs is not None: + if isinstance(m.obs, Tensor): + if any(not stopping_condition(o) for o in m.obs.obs): + raise DeviceError(f"Observable {repr(m.obs)} not supported on {name}") + elif not stopping_condition(m.obs): + raise DeviceError(f"Observable {repr(m.obs)} not supported on {name}") + + return (tape,), null_postprocessing + + +@transform +def validate_measurements( + tape: qml.tape.QuantumTape, analytic_measurements=None, sample_measurements=None, name="device" +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Validates the supported state and sample based measurement processes. + + Args: + tape (QuantumTape): a quantum circuit. + analytic_measurements (Callable[[MeasurementProcess], bool]): a function from a measurement process + to whether or not it is accepted in analytic simulations. + sample_measurements (Callable[[MeasurementProcess], bool]): a function from a measurement process + to whether or not it accepted for finite shot siutations + name (str): the name to use in error messages. + + Returns: + pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, + it returns a QNode with the transform added to its transform program. + If a tape is passed, returns a tuple containing a list of + quantum tapes to be evaluated, and a function to be applied to these + tape executions. + + Raises: + DeviceError: if a measurement process is not supported. + + >>> def analytic_measurements(m): + ... return isinstance(m, qml.measurements.StateMP) + >>> def shots_measurements(m): + ... return isinstance(m, qml.measurements.CountsMP) + >>> tape = qml.tape.QuantumScript([], [qml.expval(qml.PauliZ(0))]) + >>> validate_measurements(tape, analytic_measurements, shots_measurements) + DeviceError: Measurement expval(PauliZ(wires=[0])) not accepted for analytic simulation on device. + >>> tape = qml.tape.QuantumScript([], [qml.sample()], shots=10) + >>> validate_measurements(tape, analytic_measurements, shots_measurements) + DeviceError: Measurement sample(wires=[]) not accepted with finite shots on device + + """ + if analytic_measurements is None: + + def analytic_measurements(m): + return isinstance(m, StateMeasurement) + + if sample_measurements is None: + + def sample_measurements(m): + return isinstance(m, SampleMeasurement) + + if tape.shots: + for m in tape.measurements: + if not sample_measurements(m): + raise DeviceError(f"Measurement {m} not accepted with finite shots on {name}") + + else: + for m in tape.measurements: + if not analytic_measurements(m): + raise DeviceError( + f"Measurement {m} not accepted for analytic simulation on {name}." + ) + + return (tape,), null_postprocessing diff --git a/pennylane/devices/qubit/__init__.py b/pennylane/devices/qubit/__init__.py index 1a3267f8023..860d91953c4 100644 --- a/pennylane/devices/qubit/__init__.py +++ b/pennylane/devices/qubit/__init__.py @@ -26,7 +26,6 @@ measure measure_with_samples sample_state - preprocess simulate adjoint_jacobian adjoint_jvp @@ -37,6 +36,5 @@ from .adjoint_jacobian import adjoint_jacobian, adjoint_jvp, adjoint_vjp from .initialize_state import create_initial_state from .measure import measure -from .preprocess import preprocess from .sampling import sample_state, measure_with_samples from .simulate import simulate, get_final_state, measure_final_state diff --git a/pennylane/devices/qubit/preprocess.py b/pennylane/devices/qubit/preprocess.py deleted file mode 100644 index 326ba6343ce..00000000000 --- a/pennylane/devices/qubit/preprocess.py +++ /dev/null @@ -1,449 +0,0 @@ -# Copyright 2018-2023 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains functions for preprocessing `QuantumTape` objects to ensure -that they are supported for execution by a device.""" -# pylint: disable=protected-access -from dataclasses import replace -import os -from typing import Generator, Callable, Tuple, Union, Sequence -from copy import copy -import warnings - -import pennylane as qml -from pennylane import Snapshot -from pennylane.operation import Tensor, StatePrepBase -from pennylane.measurements import ( - MidMeasureMP, - StateMeasurement, - SampleMeasurement, - ExpectationMP, - ClassicalShadowMP, - ShadowExpvalMP, -) -from pennylane.typing import ResultBatch, Result -from pennylane import DeviceError -from pennylane.transforms.core import transform, TransformProgram -from pennylane.wires import WireError - -from pennylane.devices import ExecutionConfig, DefaultExecutionConfig - -PostprocessingFn = Callable[[ResultBatch], Union[Result, ResultBatch]] - -# Update observable list. Current list is same as supported observables for -# default.qubit. -_observables = { - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "Hermitian", - "Identity", - "Projector", - "SparseHamiltonian", - "Hamiltonian", - "Sum", - "SProd", - "Prod", - "Exp", - "Evolution", -} - - -### UTILITY FUNCTIONS FOR EXPANDING UNSUPPORTED OPERATIONS ### - - -def _accepted_operator(op: qml.operation.Operator) -> bool: - """Specify whether or not an Operator object is supported by the device.""" - if op.name == "QFT" and len(op.wires) >= 6: - return False - if op.name == "GroverOperator" and len(op.wires) >= 13: - return False - if op.name == "Snapshot": - return True - if op.__class__.__name__ == "Pow" and qml.operation.is_trainable(op): - return False - - return op.has_matrix - - -def _accepted_adjoint_operator(op: qml.operation.Operator) -> bool: - """Specify whether or not an Oeprator is supported by adjoint differentiation.""" - return op.num_params == 0 or op.num_params == 1 and op.has_generator - - -def _operator_decomposition_gen( - op: qml.operation.Operator, acceptance_function: Callable[[qml.operation.Operator], bool] -) -> Generator[qml.operation.Operator, None, None]: - """A generator that yields the next operation that is accepted by DefaultQubit.""" - if acceptance_function(op): - yield op - else: - try: - decomp = op.decomposition() - except qml.operation.DecompositionUndefinedError as e: - raise DeviceError( - f"Operator {op} not supported on DefaultQubit. Must provide either a matrix or a decomposition." - ) from e - - for sub_op in decomp: - yield from _operator_decomposition_gen(sub_op, acceptance_function) - - -####################### - - -@transform -def validate_device_wires( - tape: qml.tape.QuantumTape, device -) -> (Sequence[qml.tape.QuantumTape], Callable): - """Validates the device wires. - - Args: - tape (QuantumTape): a quantum circuit. - device (pennylane.devices.Device): The device to be checked. - - Returns: - pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. - """ - if device.wires: - if extra_wires := set(tape.wires) - set(device.wires): - raise WireError( - f"Cannot run circuit(s) on {device.name} as they contain wires " - f"not found on the device: {extra_wires}" - ) - measurements = tape.measurements.copy() - modified = False - for m_idx, mp in enumerate(measurements): - if not mp.obs and not mp.wires: - modified = True - new_mp = copy(mp) - new_mp._wires = device.wires # pylint:disable=protected-access - measurements[m_idx] = new_mp - if modified: - tape = type(tape)(tape.operations, measurements, shots=tape.shots) - - def null_postprocessing(results): - """A postprocesing function returned by a transform that only converts the batch of results - into a result for a single ``QuantumTape``. - """ - return results[0] - - return [tape], null_postprocessing - - -@transform -def validate_multiprocessing_workers( - tape: qml.tape.QuantumTape, max_workers: int, device -) -> (Sequence[qml.tape.QuantumTape], Callable): - """Validates the number of workers for multiprocessing. - - Checks that the CPU is not oversubscribed and warns user if it is, - making suggestions for the number of workers and/or the number of - threads per worker. - - Args: - tape (QuantumTape): a quantum circuit. - max_workers (int): Maximal number of multiprocessing workers - device (pennylane.devices.Device): The device to be checked. - - Returns: - pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. - """ - if max_workers is not None: - threads_per_proc = os.cpu_count() # all threads by default - varname = "OMP_NUM_THREADS" - varnames = ["MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS"] - for var in varnames: - if os.getenv(var): # pragma: no cover - varname = var - threads_per_proc = int(os.getenv(var)) - break - num_threads = threads_per_proc * max_workers - num_cpu = os.cpu_count() - num_threads_suggest = max(1, os.cpu_count() // max_workers) - num_workers_suggest = max(1, os.cpu_count() // threads_per_proc) - if num_threads > num_cpu: - warnings.warn( - f"""The device requested {num_threads} threads ({max_workers} processes - times {threads_per_proc} threads per process), but the processor only has - {num_cpu} logical cores. The processor is likely oversubscribed, which may - lead to performance deterioration. Consider decreasing the number of processes, - setting the device or execution config argument `max_workers={num_workers_suggest}` - for example, or decreasing the number of threads per process by setting the - environment variable `{varname}={num_threads_suggest}`.""", - UserWarning, - ) - - if device._debugger and device._debugger.active: - raise DeviceError("Debugging with ``Snapshots`` is not available with multiprocessing.") - - if any(isinstance(op, Snapshot) for op in tape.operations): - raise RuntimeError( - """ProcessPoolExecutor cannot execute a QuantumScript with - a ``Snapshot`` operation. Change the value of ``max_workers`` - to ``None`` or execute the QuantumScript separately.""" - ) - - def null_postprocessing(results): - """A postprocesing function returned by a transform that only converts the batch of results - into a result for a single ``QuantumTape``. - """ - return results[0] - - return [tape], null_postprocessing - - -@transform -def validate_and_expand_adjoint( - tape: qml.tape.QuantumTape, -) -> (Sequence[qml.tape.QuantumTape], Callable): - """Function for validating that the operations and observables present in the input circuit - are valid for adjoint differentiation. - - Args: - circuit(.QuantumTape): the tape to validate - - Returns: - pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. - """ - - try: - new_ops = [ - final_op - for op in tape.operations[tape.num_preps :] - for final_op in _operator_decomposition_gen(op, _accepted_adjoint_operator) - ] - except RecursionError as e: - raise DeviceError( - "Reached recursion limit trying to decompose operations. " - "Operator decomposition may have entered an infinite loop." - ) from e - - for k in tape.trainable_params: - if hasattr(tape._par_info[k]["op"], "return_type"): - warnings.warn( - "Differentiating with respect to the input parameters of " - f"{tape._par_info[k]['op'].name} is not supported with the " - "adjoint differentiation method. Gradients are computed " - "only with regards to the trainable parameters of the circuit.\n\n Mark " - "the parameters of the measured observables as non-trainable " - "to silence this warning.", - UserWarning, - ) - - # Check validity of measurements - measurements = [] - for m in tape.measurements: - if not isinstance(m, ExpectationMP): - raise DeviceError( - "Adjoint differentiation method does not support " - f"measurement {m.__class__.__name__}." - ) - - if not m.obs.has_matrix: - raise DeviceError( - f"Adjoint differentiation method does not support observable {m.obs.name}." - ) - - measurements.append(m) - - new_ops = tape.operations[: tape.num_preps] + new_ops - new_tape = qml.tape.QuantumScript(new_ops, measurements, shots=tape.shots) - - def null_postprocessing(results): - """A postprocesing function returned by a transform that only converts the batch of results - into a result for a single ``QuantumTape``. - """ - return results[0] - - return [new_tape], null_postprocessing - - -@transform -def validate_measurements( - tape: qml.tape.QuantumTape, execution_config: ExecutionConfig = DefaultExecutionConfig -) -> (Sequence[qml.tape.QuantumTape], Callable): - """Check that the circuit contains a valid set of measurements. A valid - set of measurements is defined as: - - 1. If circuit.shots is None (i.e., the execution is analytic), then - the circuit must only contain ``StateMeasurements``. - 2. If circuit.shots is not None, then the circuit must only contain - ``SampleMeasurements``. - - If the circuit has an invalid set of measurements, then an error is raised. - - Args: - tape (.QuantumTape): the circuit to validate - execution_config (.ExecutionConfig): execution configuration with configurable - options for the execution. - - Returns: - pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. - """ - if not tape.shots: - for m in tape.measurements: - if not isinstance(m, StateMeasurement): - raise DeviceError(f"Analytic circuits must only contain StateMeasurements; got {m}") - - else: - # check if an analytic diff method is used with finite shots - if execution_config.gradient_method in ["adjoint", "backprop"]: - raise DeviceError( - f"Circuits with finite shots must be executed with non-analytic " - f"gradient methods; got {execution_config.gradient_method}" - ) - - for m in tape.measurements: - if not isinstance(m, (SampleMeasurement, ClassicalShadowMP, ShadowExpvalMP)): - raise DeviceError( - f"Circuits with finite shots must only contain SampleMeasurements, ClassicalShadowMP, or ShadowExpvalMP; got {m}" - ) - - def null_postprocessing(results): - """A postprocesing function returned by a transform that only converts the batch of results - into a result for a single ``QuantumTape``. - """ - return results[0] - - return [tape], null_postprocessing - - -@transform -def expand_fn(tape: qml.tape.QuantumTape) -> (Sequence[qml.tape.QuantumTape], Callable): - """Method for expanding or decomposing an input circuit. - - This method expands the tape if: - - - mid-circuit measurements are present, - - any operations are not supported on the device. - - Args: - tape (.QuantumTape): the circuit to expand. - - Returns: - pennylane.QNode or qfunc or Tuple[List[.QuantumTape], Callable]: If a QNode is passed, - it returns a QNode with the transform added to its transform program. - If a tape is passed, returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions. - """ - if any(isinstance(o, MidMeasureMP) for o in tape.operations): - tapes, _ = qml.defer_measurements(tape) - tape = tapes[0] - - if not all(_accepted_operator(op) for op in tape.operations): - try: - # don't decompose initial operations if its StatePrepBase - prep_op = [tape[0]] if isinstance(tape[0], StatePrepBase) else [] - - new_ops = [ - final_op - for op in tape.operations[bool(prep_op) :] - for final_op in _operator_decomposition_gen(op, _accepted_operator) - ] - except RecursionError as e: - raise DeviceError( - "Reached recursion limit trying to decompose operations. " - "Operator decomposition may have entered an infinite loop." - ) from e - tape = qml.tape.QuantumScript(prep_op + new_ops, tape.measurements, shots=tape.shots) - - for observable in tape.observables: - if isinstance(observable, Tensor): - if any(o.name not in _observables for o in observable.obs): - raise DeviceError(f"Observable {observable} not supported on DefaultQubit") - elif observable.name not in _observables: - raise DeviceError(f"Observable {observable} not supported on DefaultQubit") - - def null_postprocessing(results): - """A postprocesing function returned by a transform that only converts the batch of results - into a result for a single ``QuantumTape``. - """ - return results[0] - - return [tape], null_postprocessing - - -def _update_config(config: ExecutionConfig) -> ExecutionConfig: - """Choose the "best" options for the configuration if they are left unspecified. - - Args: - config (ExecutionConfig): the initial execution config - - Returns: - ExecutionConfig: a new config with the best choices selected. - """ - updated_values = {} - if config.gradient_method == "best": - updated_values["gradient_method"] = "backprop" - if config.use_device_gradient is None: - updated_values["use_device_gradient"] = config.gradient_method in { - "best", - "adjoint", - "backprop", - } - if config.grad_on_execution is None: - updated_values["grad_on_execution"] = config.gradient_method == "adjoint" - return replace(config, **updated_values) - - -def preprocess( - execution_config: ExecutionConfig = DefaultExecutionConfig, -) -> Tuple[Tuple[qml.tape.QuantumScript], PostprocessingFn, ExecutionConfig]: - """Preprocess a batch of :class:`~.QuantumTape` objects to make them ready for execution. - - This function validates a batch of :class:`~.QuantumTape` objects by transforming and expanding - them to ensure all operators and measurements are supported by the execution device. - - Args: - execution_config (ExecutionConfig): execution configuration with configurable - options for the execution. - - Returns: - TransformProgram, ExecutionConfig: A transform program and a configuration with originally unset specifications - filled in. - """ - transform_program = TransformProgram() - - # Validate measurement - transform_program.add_transform(validate_measurements, execution_config) - - # Circuit expand - transform_program.add_transform(expand_fn) - - if execution_config.gradient_method == "adjoint": - # Adjoint expand - transform_program.add_transform(validate_and_expand_adjoint) - ### Broadcast expand - transform_program.add_transform(qml.transforms.broadcast_expand) - - return transform_program, _update_config(execution_config) diff --git a/pennylane/transforms/defer_measurements.py b/pennylane/transforms/defer_measurements.py index 55e01c62f30..668ce74c03a 100644 --- a/pennylane/transforms/defer_measurements.py +++ b/pennylane/transforms/defer_measurements.py @@ -26,6 +26,13 @@ # pylint: disable=too-many-branches +def null_postprocessing(results): + """A postprocessing function returned by a transform that only converts the batch of results + into a result for a single ``QuantumTape``. + """ + return results[0] + + @transform def defer_measurements(tape: QuantumTape) -> (Sequence[QuantumTape], Callable): """Quantum function transform that substitutes operations conditioned on @@ -130,10 +137,16 @@ def func(x, y): tensor([0.76960924, 0.13204407, 0.08394415, 0.01440254], requires_grad=True) """ # pylint: disable=protected-access + if not any(isinstance(o, MidMeasureMP) for o in tape.operations): + return (tape,), null_postprocessing cv_types = (qml.operation.CVOperation, qml.operation.CVObservable) - ops_cv = any(isinstance(op, cv_types) for op in tape.operations) - obs_cv = any(isinstance(getattr(op, "obs", None), cv_types) for op in tape.measurements) + ops_cv = any(isinstance(op, cv_types) and op.name != "Identity" for op in tape.operations) + obs_cv = any( + isinstance(getattr(op, "obs", None), cv_types) + and not isinstance(getattr(op, "obs", None), qml.Identity) + for op in tape.measurements + ) if ops_cv or obs_cv: raise ValueError("Continuous variable operations and observables are not supported.") @@ -200,12 +213,6 @@ def func(x, y): new_tape = type(tape)(new_operations, new_measurements, shots=tape.shots) - def null_postprocessing(results): - """A postprocesing function returned by a transform that only converts the batch of results - into a result for a single ``QuantumTape``. - """ - return results[0] - return [new_tape], null_postprocessing diff --git a/tests/devices/experimental/test_default_qubit_2.py b/tests/devices/default_qubit/test_default_qubit.py similarity index 85% rename from tests/devices/experimental/test_default_qubit_2.py rename to tests/devices/default_qubit/test_default_qubit.py index 5ae3acfc5bf..1867834bf6d 100644 --- a/tests/devices/experimental/test_default_qubit_2.py +++ b/tests/devices/default_qubit/test_default_qubit.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for default qubit 2.""" +"""Tests for default qubit.""" # pylint: disable=import-outside-toplevel, no-member import pytest @@ -19,10 +19,8 @@ import numpy as np import pennylane as qml -from pennylane.measurements import SampleMP, StateMP, ProbabilityMP from pennylane.devices import DefaultQubit, ExecutionConfig -from pennylane.devices.qubit.preprocess import validate_and_expand_adjoint def test_name(): @@ -58,135 +56,23 @@ def test_debugger_attribute(): assert dev._debugger is None -class TestSnapshotMulti: - def test_snapshot_multiprocessing_execute(self): - """DefaultQubit cannot execute tapes with Snapshot if `max_workers` is not `None`""" - dev = DefaultQubit(max_workers=2) +def test_snapshot_multiprocessing_qnode(): + """DefaultQubit cannot execute tapes with Snapshot if `max_workers` is not `None`""" + dev = DefaultQubit(max_workers=2) - tape = qml.tape.QuantumScript( - [ - qml.Snapshot(), - qml.Hadamard(wires=0), - qml.Snapshot("very_important_state"), - qml.CNOT(wires=[0, 1]), - qml.Snapshot(), - ], - [qml.expval(qml.PauliX(0))], - ) - with pytest.raises( - RuntimeError, match="ProcessPoolExecutor cannot execute a QuantumScript" - ): - program, _ = dev.preprocess() - program([tape]) - - def test_snapshot_multiprocessing_qnode(self): - """DefaultQubit cannot execute tapes with Snapshot if `max_workers` is not `None`""" - dev = DefaultQubit(max_workers=2) - - @qml.qnode(dev) - def circuit(): - qml.Snapshot("tag") - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - qml.Snapshot() - return qml.expval(qml.PauliX(0) + qml.PauliY(0)) - - with pytest.raises( - qml.DeviceError, - match="Debugging with ``Snapshots`` is not available with multiprocessing.", - ): - qml.snapshots(circuit)() - - -# pylint: disable=too-few-public-methods -class TestPreprocessing: - """Unit tests for the preprocessing method.""" - - def test_chooses_best_gradient_method(self): - """Test that preprocessing chooses backprop as the best gradient method.""" - dev = DefaultQubit() - - config = ExecutionConfig( - gradient_method="best", use_device_gradient=None, grad_on_execution=None - ) - - _, new_config = dev.preprocess(config) - - assert new_config.gradient_method == "backprop" - assert new_config.use_device_gradient - assert not new_config.grad_on_execution - - def test_config_choices_for_adjoint(self): - """Test that preprocessing request grad on execution and says to use the device gradient if adjoint is requested.""" - dev = DefaultQubit() - - config = ExecutionConfig( - gradient_method="adjoint", use_device_gradient=None, grad_on_execution=None - ) - - _, new_config = dev.preprocess(config) - - assert new_config.use_device_gradient - assert new_config.grad_on_execution - - @pytest.mark.parametrize("max_workers", [None, 1, 2, 3]) - def test_config_choices_for_threading(self, max_workers): - """Test that preprocessing request grad on execution and says to use the device gradient if adjoint is requested.""" - dev = DefaultQubit() - - config = ExecutionConfig(device_options={"max_workers": max_workers}) - _, new_config = dev.preprocess(config) + @qml.qnode(dev) + def circuit(): + qml.Snapshot("tag") + qml.Hadamard(wires=0) + qml.CNOT(wires=[0, 1]) + qml.Snapshot() + return qml.expval(qml.PauliX(0) + qml.PauliY(0)) - assert new_config.device_options["max_workers"] == max_workers - - def test_circuit_wire_validation(self): - """Test that preprocessing validates wires on the circuits being executed.""" - dev = DefaultQubit(wires=3) - circuit_valid_0 = qml.tape.QuantumScript([qml.PauliX(0)]) - program, _ = dev.preprocess() - circuits, _ = program([circuit_valid_0]) - assert circuits == (circuit_valid_0,) - - circuit_valid_1 = qml.tape.QuantumScript([qml.PauliX(1)]) - program, _ = dev.preprocess() - circuits, _ = program([circuit_valid_0, circuit_valid_1]) - assert circuits == tuple([circuit_valid_0, circuit_valid_1]) - - invalid_circuit = qml.tape.QuantumScript([qml.PauliX(4)]) - with pytest.raises(qml.wires.WireError, match=r"Cannot run circuit\(s\) on"): - program, _ = dev.preprocess() - program( - [ - invalid_circuit, - ] - ) - - with pytest.raises(qml.wires.WireError, match=r"Cannot run circuit\(s\) on"): - program, _ = dev.preprocess() - program([circuit_valid_0, invalid_circuit]) - - @pytest.mark.parametrize( - "mp_fn,mp_cls,shots", - [ - (qml.sample, SampleMP, 10), - (qml.state, StateMP, None), - (qml.probs, ProbabilityMP, None), - ], - ) - def test_measurement_is_swapped_out(self, mp_fn, mp_cls, shots): - """Test that preprocessing swaps out any MP with no wires or obs""" - dev = DefaultQubit(wires=3) - original_mp = mp_fn() - exp_z = qml.expval(qml.PauliZ(0)) - qs = qml.tape.QuantumScript([qml.Hadamard(0)], [original_mp, exp_z], shots=shots) - program, _ = dev.preprocess() - tapes, _ = program([qs]) - assert len(tapes) == 1 - tape = tapes[0] - assert tape.operations == qs.operations - assert tape.measurements != qs.measurements - assert qml.equal(tape.measurements[0], mp_cls(wires=[0, 1, 2])) - assert tape.measurements[1] is exp_z + with pytest.raises( + qml.DeviceError, + match="Debugging with ``Snapshots`` is not available with multiprocessing.", + ): + qml.snapshots(circuit)() class TestSupportsDerivatives: @@ -1009,8 +895,10 @@ def test_derivatives_single_circuit(self, max_workers): dev = DefaultQubit(max_workers=max_workers) x = np.array(np.pi / 7) qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) - qs, _ = validate_and_expand_adjoint(qs) - qs = qs[0] + + config = ExecutionConfig(gradient_method="adjoint") + batch, _ = dev.preprocess(config)[0]((qs,)) + qs = batch[0] expected_grad = -qml.math.sin(x) actual_grad = dev.compute_derivatives(qs, self.ec) assert isinstance(actual_grad, np.ndarray) @@ -1027,8 +915,9 @@ def test_derivatives_list_with_single_circuit(self, max_workers): dev = DefaultQubit(max_workers=max_workers) x = np.array(np.pi / 7) qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) - qs, _ = validate_and_expand_adjoint(qs) - qs = qs[0] + config = ExecutionConfig(gradient_method="adjoint") + batch, _ = dev.preprocess(config)[0]((qs,)) + qs = batch[0] expected_grad = -qml.math.sin(x) actual_grad = dev.compute_derivatives([qs], self.ec) assert isinstance(actual_grad, tuple) @@ -1083,8 +972,9 @@ def test_jvps_single_circuit(self, max_workers): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) - qs, _ = validate_and_expand_adjoint(qs) - qs = qs[0] + config = ExecutionConfig(gradient_method="adjoint") + batch, _ = dev.preprocess(config)[0]((qs,)) + qs = batch[0] expected_grad = -qml.math.sin(x) * tangent[0] actual_grad = dev.compute_jvp(qs, tangent, self.ec) @@ -1105,8 +995,9 @@ def test_jvps_list_with_single_circuit(self, max_workers): qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) - qs, _ = validate_and_expand_adjoint(qs) - qs = qs[0] + config = ExecutionConfig(gradient_method="adjoint") + batch, _ = dev.preprocess(config)[0]((qs,)) + qs = batch[0] expected_grad = -qml.math.sin(x) * tangent[0] actual_grad = dev.compute_jvp([qs], [tangent], self.ec) @@ -1180,8 +1071,9 @@ def test_vjps_single_circuit(self, max_workers): cotangent = (0.456,) qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) - qs, _ = validate_and_expand_adjoint(qs) - qs = qs[0] + config = ExecutionConfig(gradient_method="adjoint") + batch, _ = dev.preprocess(config)[0]((qs,)) + qs = batch[0] expected_grad = -qml.math.sin(x) * cotangent[0] actual_grad = dev.compute_vjp(qs, cotangent, self.ec) @@ -1201,8 +1093,9 @@ def test_vjps_list_with_single_circuit(self, max_workers): cotangent = (0.456,) qs = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.PauliZ(0))]) - qs, _ = validate_and_expand_adjoint(qs) - qs = qs[0] + config = ExecutionConfig(gradient_method="adjoint") + batch, _ = dev.preprocess(config)[0]((qs,)) + qs = batch[0] expected_grad = -qml.math.sin(x) * cotangent[0] actual_grad = dev.compute_vjp([qs], [cotangent], self.ec) @@ -1269,131 +1162,6 @@ def test_vjps_integration(self, max_workers): assert np.isclose(actual_grad[1], expected_grad[1]) -class TestPreprocessingIntegration: - """Test preprocess produces output that can be executed by the device.""" - - @pytest.mark.parametrize("max_workers", [None, 1, 2]) - def test_preprocess_single_circuit(self, max_workers): - """Test integration between preprocessing and execution with numpy parameters.""" - - # pylint: disable=too-few-public-methods - class MyTemplate(qml.operation.Operation): - """Temp operator.""" - - num_wires = 2 - - # pylint: disable=missing-function-docstring - def decomposition(self): - return [ - qml.RX(self.data[0], self.wires[0]), - qml.RY(self.data[1], self.wires[1]), - qml.CNOT(self.wires), - ] - - x = 0.928 - y = -0.792 - qscript = qml.tape.QuantumScript( - [MyTemplate(x, y, ("a", "b"))], - [qml.expval(qml.PauliY("a")), qml.expval(qml.PauliZ("a")), qml.expval(qml.PauliX("b"))], - ) - - dev = DefaultQubit(max_workers=max_workers) - tapes = tuple([qscript]) - program, config = dev.preprocess() - tapes, pre_processing_fn = program(tapes) - - assert len(tapes) == 1 - execute_circuit = tapes[0] - assert qml.equal(execute_circuit[0], qml.RX(x, "a")) - assert qml.equal(execute_circuit[1], qml.RY(y, "b")) - assert qml.equal(execute_circuit[2], qml.CNOT(("a", "b"))) - assert qml.equal(execute_circuit[3], qml.expval(qml.PauliY("a"))) - assert qml.equal(execute_circuit[4], qml.expval(qml.PauliZ("a"))) - assert qml.equal(execute_circuit[5], qml.expval(qml.PauliX("b"))) - - results = dev.execute(tapes, config) - assert len(results) == 1 - assert len(results[0]) == 3 - - processed_results = pre_processing_fn(results) - processed_result = processed_results[0] - assert len(processed_result) == 3 - assert qml.math.allclose(processed_result[0], -np.sin(x) * np.sin(y)) - assert qml.math.allclose(processed_result[1], np.cos(x)) - assert qml.math.allclose(processed_result[2], np.sin(y)) - - @pytest.mark.parametrize("max_workers", [None, 1, 2]) - def test_preprocess_batch_circuit(self, max_workers): - """Test preprocess integrates with default qubit when we start with a batch of circuits.""" - - # pylint: disable=too-few-public-methods - class CustomIsingXX(qml.operation.Operation): - """Temp operator.""" - - num_wires = 2 - - # pylint: disable=missing-function-docstring - def decomposition(self): - return [qml.IsingXX(self.data[0], self.wires)] - - x = 0.692 - - measurements1 = [qml.density_matrix("a"), qml.vn_entropy("a")] - qs1 = qml.tape.QuantumScript([CustomIsingXX(x, ("a", "b"))], measurements1) - - y = -0.923 - - with qml.queuing.AnnotatedQueue() as q: - qml.PauliX(wires=1) - m_0 = qml.measure(1) - qml.cond(m_0, qml.RY)(y, wires=0) - qml.expval(qml.PauliZ(0)) - - qs2 = qml.tape.QuantumScript.from_queue(q) - - initial_batch = [qs1, qs2] - - dev = DefaultQubit(max_workers=max_workers) - - program, config = dev.preprocess() - batch, pre_processing_fn = program(initial_batch) - - results = dev.execute(batch, config) - processed_results = pre_processing_fn(results) - - assert len(processed_results) == 2 - assert len(processed_results[0]) == 2 - - expected_density_mat = np.array([[np.cos(x / 2) ** 2, 0], [0, np.sin(x / 2) ** 2]]) - assert qml.math.allclose(processed_results[0][0], expected_density_mat) - - eig_1 = (1 + np.sqrt(1 - 4 * np.cos(x / 2) ** 2 * np.sin(x / 2) ** 2)) / 2 - eig_2 = (1 - np.sqrt(1 - 4 * np.cos(x / 2) ** 2 * np.sin(x / 2) ** 2)) / 2 - eigs = [eig_1, eig_2] - eigs = [eig for eig in eigs if eig > 0] - - expected_entropy = -np.sum(eigs * np.log(eigs)) - assert qml.math.allclose(processed_results[0][1], expected_entropy) - - expected_expval = np.cos(y) - assert qml.math.allclose(expected_expval, processed_results[1]) - - def test_preprocess_defer_measurements(self, mocker): - """Test that a QNode with mid-circuit measurements is transformed - using defer_measurements.""" - dev = DefaultQubit() - - tape = qml.tape.QuantumScript( - [qml.PauliX(0), qml.measurements.MidMeasureMP(qml.wires.Wires(0))], - [qml.expval(qml.PauliZ(0))], - ) - spy = mocker.spy(qml, "defer_measurements") - - program, _ = dev.preprocess() - program([tape]) - spy.assert_called_once() - - class TestRandomSeed: """Test that the device behaves correctly when provided with a random seed""" @@ -1852,24 +1620,21 @@ def test_shot_vectors(self, max_workers, n_qubits, shots): assert np.all(np.logical_or(np.logical_or(r[1] == 0, r[1] == 1), r[1] == 2)) -class TestDynamicType: - """Tests the compatibility with dynamic type classes such as `qml.Projector`.""" +@pytest.mark.parametrize("n_wires", [1, 2, 3]) +@pytest.mark.parametrize("max_workers", [None, 1, 2]) +def test_projector_dynamic_type(max_workers, n_wires): + """Test that qml.Projector yields the expected results for both of its subclasses.""" + wires = list(range(n_wires)) + dev = DefaultQubit(max_workers=max_workers) + ops = [qml.adjoint(qml.Hadamard(q)) for q in wires] + basis_state = np.zeros((n_wires,)) + state_vector = np.zeros((2**n_wires,)) + state_vector[0] = 1 - @pytest.mark.parametrize("n_wires", [1, 2, 3]) - @pytest.mark.parametrize("max_workers", [None, 1, 2]) - def test_projector(self, max_workers, n_wires): - """Test that qml.Projector yields the expected results for both of its subclasses.""" - wires = list(range(n_wires)) - dev = DefaultQubit(max_workers=max_workers) - ops = [qml.adjoint(qml.Hadamard(q)) for q in wires] - basis_state = np.zeros((n_wires,)) - state_vector = np.zeros((2**n_wires,)) - state_vector[0] = 1 - - for state in [basis_state, state_vector]: - qs = qml.tape.QuantumScript(ops, [qml.expval(qml.Projector(state, wires))]) - res = dev.execute(qs) - assert np.isclose(res, 1 / 2**n_wires) + for state in [basis_state, state_vector]: + qs = qml.tape.QuantumScript(ops, [qml.expval(qml.Projector(state, wires))]) + res = dev.execute(qs) + assert np.isclose(res, 1 / 2**n_wires) class TestIntegration: diff --git a/tests/devices/default_qubit/test_default_qubit_preprocessing.py b/tests/devices/default_qubit/test_default_qubit_preprocessing.py new file mode 100644 index 00000000000..2a6cde2fea8 --- /dev/null +++ b/tests/devices/default_qubit/test_default_qubit_preprocessing.py @@ -0,0 +1,820 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for default qubit preprocessing.""" + +import pytest + +import numpy as np + +import pennylane as qml +from pennylane.devices import DefaultQubit, ExecutionConfig + +from pennylane.devices.default_qubit import stopping_condition + + +class NoMatOp(qml.operation.Operation): + """Dummy operation for expanding circuit.""" + + # pylint: disable=arguments-renamed, invalid-overridden-method + @property + def has_matrix(self): + return False + + def decomposition(self): + return [qml.PauliX(self.wires), qml.PauliY(self.wires)] + + +# pylint: disable=too-few-public-methods +class NoMatNoDecompOp(qml.operation.Operation): + """Dummy operation for checking check_validity throws error when + expected.""" + + # pylint: disable=arguments-renamed, invalid-overridden-method + @property + def has_matrix(self): + return False + + +def test_snapshot_multiprocessing_execute(): + """DefaultQubit cannot execute tapes with Snapshot if `max_workers` is not `None`""" + dev = qml.device("default.qubit", max_workers=2) + + tape = qml.tape.QuantumScript( + [ + qml.Snapshot(), + qml.Hadamard(wires=0), + qml.Snapshot("very_important_state"), + qml.CNOT(wires=[0, 1]), + qml.Snapshot(), + ], + [qml.expval(qml.PauliX(0))], + ) + with pytest.raises(RuntimeError, match="ProcessPoolExecutor cannot execute a QuantumScript"): + program, _ = dev.preprocess() + program([tape]) + + +class TestConfigSetup: + """Tests involving setting up the execution config.""" + + def test_choose_best_gradient_method(self): + """Test that preprocessing chooses backprop as the best gradient method.""" + config = qml.devices.ExecutionConfig(gradient_method="best") + _, config = qml.device("default.qubit").preprocess(config) + assert config.gradient_method == "backprop" + assert config.use_device_gradient + assert not config.grad_on_execution + + def test_config_choices_for_adjoint(self): + """Test that preprocessing request grad on execution and says to use the device gradient if adjoint is requested.""" + config = qml.devices.ExecutionConfig( + gradient_method="adjoint", use_device_gradient=None, grad_on_execution=None + ) + _, new_config = qml.device("default.qubit").preprocess(config) + + assert new_config.use_device_gradient + assert new_config.grad_on_execution + + +# pylint: disable=too-few-public-methods +class TestPreprocessing: + """Unit tests for the preprocessing method.""" + + def test_chooses_best_gradient_method(self): + """Test that preprocessing chooses backprop as the best gradient method.""" + dev = DefaultQubit() + + config = ExecutionConfig( + gradient_method="best", use_device_gradient=None, grad_on_execution=None + ) + + _, new_config = dev.preprocess(config) + + assert new_config.gradient_method == "backprop" + assert new_config.use_device_gradient + assert not new_config.grad_on_execution + + def test_config_choices_for_adjoint(self): + """Test that preprocessing request grad on execution and says to use the device gradient if adjoint is requested.""" + dev = DefaultQubit() + + config = ExecutionConfig( + gradient_method="adjoint", use_device_gradient=None, grad_on_execution=None + ) + + _, new_config = dev.preprocess(config) + + assert new_config.use_device_gradient + assert new_config.grad_on_execution + + @pytest.mark.parametrize("max_workers", [None, 1, 2, 3]) + def test_config_choices_for_threading(self, max_workers): + """Test that preprocessing request grad on execution and says to use the device gradient if adjoint is requested.""" + dev = DefaultQubit() + + config = ExecutionConfig(device_options={"max_workers": max_workers}) + _, new_config = dev.preprocess(config) + + assert new_config.device_options["max_workers"] == max_workers + + def test_circuit_wire_validation(self): + """Test that preprocessing validates wires on the circuits being executed.""" + dev = DefaultQubit(wires=3) + circuit_valid_0 = qml.tape.QuantumScript([qml.PauliX(0)]) + program, _ = dev.preprocess() + circuits, _ = program([circuit_valid_0]) + assert circuits[0].circuit == circuit_valid_0.circuit + + circuit_valid_1 = qml.tape.QuantumScript([qml.PauliX(1)]) + program, _ = dev.preprocess() + circuits, _ = program([circuit_valid_0, circuit_valid_1]) + assert circuits[0].circuit == circuit_valid_0.circuit + assert circuits[1].circuit == circuit_valid_1.circuit + + invalid_circuit = qml.tape.QuantumScript([qml.PauliX(4)]) + with pytest.raises(qml.wires.WireError, match=r"Cannot run circuit\(s\) on"): + program, _ = dev.preprocess() + program( + [ + invalid_circuit, + ] + ) + + with pytest.raises(qml.wires.WireError, match=r"Cannot run circuit\(s\) on"): + program, _ = dev.preprocess() + program([circuit_valid_0, invalid_circuit]) + + @pytest.mark.parametrize( + "mp_fn,mp_cls,shots", + [ + (qml.sample, qml.measurements.SampleMP, 10), + (qml.state, qml.measurements.StateMP, None), + (qml.probs, qml.measurements.ProbabilityMP, None), + ], + ) + def test_measurement_is_swapped_out(self, mp_fn, mp_cls, shots): + """Test that preprocessing swaps out any MP with no wires or obs""" + dev = DefaultQubit(wires=3) + original_mp = mp_fn() + exp_z = qml.expval(qml.PauliZ(0)) + qs = qml.tape.QuantumScript([qml.Hadamard(0)], [original_mp, exp_z], shots=shots) + program, _ = dev.preprocess() + tapes, _ = program([qs]) + assert len(tapes) == 1 + tape = tapes[0] + assert tape.operations == qs.operations + assert tape.measurements != qs.measurements + assert qml.equal(tape.measurements[0], mp_cls(wires=[0, 1, 2])) + assert tape.measurements[1] is exp_z + + @pytest.mark.parametrize( + "op, expected", + [ + (qml.PauliX(0), True), + (qml.CRX(0.1, wires=[0, 1]), True), + (qml.Snapshot(), True), + (qml.Barrier(), False), + (qml.QFT(wires=range(5)), True), + (qml.QFT(wires=range(10)), False), + (qml.GroverOperator(wires=range(10)), True), + (qml.GroverOperator(wires=range(14)), False), + (qml.pow(qml.RX(1.1, 0), 3), True), + (qml.pow(qml.RX(qml.numpy.array(1.1), 0), 3), False), + ], + ) + def test_accepted_operator(self, op, expected): + """Test that _accepted_operator works correctly""" + res = stopping_condition(op) + assert res == expected + + def test_adjoint_only_one_wire(self): + """Tests adjoint accepts operators with no parameters or a single parameter and a generator.""" + + program = qml.device("default.qubit").preprocess( + ExecutionConfig(gradient_method="adjoint") + )[0] + + class MatOp(qml.operation.Operation): + """Dummy operation for expanding circuit.""" + + # pylint: disable=arguments-renamed, invalid-overridden-method + @property + def has_matrix(self): + return True + + def decomposition(self): + return [qml.PauliX(self.wires), qml.PauliY(self.wires)] + + tape1 = qml.tape.QuantumScript([MatOp(wires=0)]) + batch, _ = program((tape1,)) + assert batch[0].circuit == tape1.circuit + + tape2 = qml.tape.QuantumScript([MatOp(1.2, wires=0)]) + batch, _ = program((tape2,)) + assert batch[0].circuit != tape2.circuit + + tape3 = qml.tape.QuantumScript([MatOp(1.2, 2.3, wires=0)]) + batch, _ = program((tape2,)) + assert batch[0].circuit != tape3.circuit + + class CustomOpWithGenerator(qml.operation.Operator): + """A custom operator with a generator.""" + + def generator(self): + return qml.PauliX(0) + + # pylint: disable=arguments-renamed, invalid-overridden-method + @property + def has_matrix(self): + return True + + tape4 = qml.tape.QuantumScript([CustomOpWithGenerator(1.2, wires=0)]) + batch, _ = program((tape4,)) + assert batch[0].circuit == tape4.circuit + + +class TestPreprocessingIntegration: + """Test preprocess produces output that can be executed by the device.""" + + def test_batch_transform_no_batching(self): + """Test that batch_transform does nothing when no batching is required.""" + ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(0.123, wires=1)] + measurements = [qml.expval(qml.PauliZ(1))] + tape = qml.tape.QuantumScript(ops=ops, measurements=measurements) + + device = qml.device("default.qubit") + + program, _ = device.preprocess() + tapes, _ = program([tape]) + + assert len(tapes) == 1 + for op, expected in zip(tapes[0].circuit, ops + measurements): + assert qml.equal(op, expected) + + def test_batch_transform_broadcast_not_adjoint(self): + """Test that batch_transform does nothing when batching is required but + internal PennyLane broadcasting can be used (diff method != adjoint)""" + ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX([np.pi, np.pi / 2], wires=1)] + measurements = [qml.expval(qml.PauliZ(1))] + tape = qml.tape.QuantumScript(ops=ops, measurements=measurements) + device = qml.devices.DefaultQubit() + + program, _ = device.preprocess() + tapes, _ = program([tape]) + + assert len(tapes) == 1 + assert tapes[0].circuit == ops + measurements + + def test_batch_transform_broadcast_adjoint(self): + """Test that batch_transform splits broadcasted tapes correctly when + the diff method is adjoint""" + ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX([np.pi, np.pi / 2], wires=1)] + measurements = [qml.expval(qml.PauliZ(1))] + tape = qml.tape.QuantumScript(ops=ops, measurements=measurements) + + execution_config = ExecutionConfig(gradient_method="adjoint") + + device = qml.devices.DefaultQubit() + + program, _ = device.preprocess(execution_config=execution_config) + tapes, _ = program([tape]) + expected_ops = [ + [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(np.pi, wires=1)], + [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(np.pi / 2, wires=1)], + ] + + assert len(tapes) == 2 + for i, t in enumerate(tapes): + for op, expected in zip(t.circuit, expected_ops[i] + measurements): + assert qml.equal(op, expected) + + def test_preprocess_batch_transform_not_adjoint(self): + """Test that preprocess returns the correct tapes when a batch transform + is needed.""" + ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX([np.pi, np.pi / 2], wires=1)] + # Need to specify grouping type to transform tape + measurements = [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1))] + tapes = [ + qml.tape.QuantumScript(ops=ops, measurements=[measurements[0]]), + qml.tape.QuantumScript(ops=ops, measurements=[measurements[1]]), + ] + + program, _ = qml.device("default.qubit").preprocess() + res_tapes, batch_fn = program(tapes) + + assert len(res_tapes) == 2 + for i, t in enumerate(res_tapes): + for op, expected_op in zip(t.operations, ops): + assert qml.equal(op, expected_op) + assert len(t.measurements) == 1 + if i == 0: + assert qml.equal(t.measurements[0], measurements[0]) + else: + assert qml.equal(t.measurements[0], measurements[1]) + + val = ([[1, 2], [3, 4]], [[5, 6], [7, 8]]) + assert np.array_equal(batch_fn(val), np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])) + + def test_preprocess_batch_transform_adjoint(self): + """Test that preprocess returns the correct tapes when a batch transform + is needed.""" + ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX([np.pi, np.pi / 2], wires=1)] + # Need to specify grouping type to transform tape + measurements = [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1))] + tapes = [ + qml.tape.QuantumScript(ops=ops, measurements=[measurements[0]]), + qml.tape.QuantumScript(ops=ops, measurements=[measurements[1]]), + ] + + execution_config = ExecutionConfig(gradient_method="adjoint") + + program, _ = qml.device("default.qubit").preprocess(execution_config=execution_config) + res_tapes, batch_fn = program(tapes) + + expected_ops = [ + [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(np.pi, wires=1)], + [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(np.pi / 2, wires=1)], + ] + + assert len(res_tapes) == 4 + for i, t in enumerate(res_tapes): + for op, expected_op in zip(t.operations, expected_ops[i % 2]): + assert qml.equal(op, expected_op) + assert len(t.measurements) == 1 + if i < 2: + assert qml.equal(t.measurements[0], measurements[0]) + else: + assert qml.equal(t.measurements[0], measurements[1]) + + val = ([[1, 2]], [[3, 4]], [[5, 6]], [[7, 8]]) + assert np.array_equal(batch_fn(val), np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])) + + def test_preprocess_expand(self): + """Test that preprocess returns the correct tapes when expansion is needed.""" + ops = [qml.Hadamard(0), NoMatOp(1), qml.RZ(0.123, wires=1)] + measurements = [[qml.expval(qml.PauliZ(0))], [qml.expval(qml.PauliX(1))]] + tapes = [ + qml.tape.QuantumScript(ops=ops, measurements=measurements[0]), + qml.tape.QuantumScript(ops=ops, measurements=measurements[1]), + ] + + program, _ = qml.device("default.qubit").preprocess() + res_tapes, batch_fn = program(tapes) + + expected = [qml.Hadamard(0), qml.PauliX(1), qml.PauliY(1), qml.RZ(0.123, wires=1)] + + assert len(res_tapes) == 2 + for i, t in enumerate(res_tapes): + for op, exp in zip(t.circuit, expected + measurements[i]): + assert qml.equal(op, exp) + + val = (("a", "b"), "c", "d") + assert batch_fn(val) == (("a", "b"), "c") + + def test_preprocess_split_and_expand_not_adjoint(self): + """Test that preprocess returns the correct tapes when splitting and expanding + is needed.""" + ops = [qml.Hadamard(0), NoMatOp(1), qml.RX([np.pi, np.pi / 2], wires=1)] + # Need to specify grouping type to transform tape + measurements = [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1))] + tapes = [ + qml.tape.QuantumScript(ops=ops, measurements=[measurements[0]]), + qml.tape.QuantumScript(ops=ops, measurements=[measurements[1]]), + ] + + program, _ = qml.device("default.qubit").preprocess() + res_tapes, batch_fn = program(tapes) + expected_ops = [ + qml.Hadamard(0), + qml.PauliX(1), + qml.PauliY(1), + qml.RX([np.pi, np.pi / 2], wires=1), + ] + + assert len(res_tapes) == 2 + for i, t in enumerate(res_tapes): + for op, expected_op in zip(t.operations, expected_ops): + assert qml.equal(op, expected_op) + assert len(t.measurements) == 1 + if i == 0: + assert qml.equal(t.measurements[0], measurements[0]) + else: + assert qml.equal(t.measurements[0], measurements[1]) + + val = ([[1, 2], [3, 4]], [[5, 6], [7, 8]]) + assert np.array_equal(batch_fn(val), np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])) + + def test_preprocess_split_and_expand_adjoint(self): + """Test that preprocess returns the correct tapes when splitting and expanding + is needed.""" + ops = [qml.Hadamard(0), NoMatOp(1), qml.RX([np.pi, np.pi / 2], wires=1)] + # Need to specify grouping type to transform tape + measurements = [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1))] + tapes = [ + qml.tape.QuantumScript(ops=ops, measurements=[measurements[0]]), + qml.tape.QuantumScript(ops=ops, measurements=[measurements[1]]), + ] + + execution_config = ExecutionConfig(gradient_method="adjoint") + + program, _ = qml.device("default.qubit").preprocess(execution_config=execution_config) + res_tapes, batch_fn = program(tapes) + + expected_ops = [ + [qml.Hadamard(0), qml.PauliX(1), qml.PauliY(1), qml.RX(np.pi, wires=1)], + [qml.Hadamard(0), qml.PauliX(1), qml.PauliY(1), qml.RX(np.pi / 2, wires=1)], + ] + + assert len(res_tapes) == 4 + for i, t in enumerate(res_tapes): + for op, expected_op in zip(t.operations, expected_ops[i % 2]): + assert qml.equal(op, expected_op) + assert len(t.measurements) == 1 + if i < 2: + assert qml.equal(t.measurements[0], measurements[0]) + else: + assert qml.equal(t.measurements[0], measurements[1]) + + val = ([[1, 2]], [[3, 4]], [[5, 6]], [[7, 8]]) + assert np.array_equal(batch_fn(val), np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])) + + def test_preprocess_check_validity_fail(self): + """Test that preprocess throws an error if the split and expanded tapes have + unsupported operators.""" + ops = [qml.Hadamard(0), NoMatNoDecompOp(1), qml.RZ(0.123, wires=1)] + measurements = [[qml.expval(qml.PauliZ(0))], [qml.expval(qml.PauliX(1))]] + tapes = [ + qml.tape.QuantumScript(ops=ops, measurements=measurements[0]), + qml.tape.QuantumScript(ops=ops, measurements=measurements[1]), + ] + + program, _ = qml.device("default.qubit").preprocess() + with pytest.raises(qml.DeviceError, match="Operator NoMatNoDecompOp"): + program(tapes) + + @pytest.mark.parametrize( + "ops, measurement, message", + [ + ( + [qml.RX(0.1, wires=0)], + [qml.probs(wires=[0, 1, 2])], + "not accepted for analytic simulation on adjoint", + ), + ( + [qml.RX(0.1, wires=0)], + [qml.expval(qml.Hamiltonian([1], [qml.PauliZ(0)]))], + "not supported on adjoint", + ), + ], + ) + @pytest.mark.filterwarnings("ignore:Differentiating with respect to") + def test_preprocess_invalid_tape_adjoint(self, ops, measurement, message): + """Test that preprocessing fails if adjoint differentiation is requested and an + invalid tape is used""" + qs = qml.tape.QuantumScript(ops, measurement) + execution_config = qml.devices.ExecutionConfig(gradient_method="adjoint") + + program, _ = qml.device("default.qubit").preprocess(execution_config) + with pytest.raises(qml.DeviceError, match=message): + program([qs]) + + def test_preprocess_tape_for_adjoint(self): + """Test that a tape is expanded correctly if adjoint differentiation is requested""" + qs = qml.tape.QuantumScript( + [qml.Rot(0.1, 0.2, 0.3, wires=0), qml.CNOT([0, 1])], + [qml.expval(qml.PauliZ(1))], + ) + execution_config = qml.devices.ExecutionConfig(gradient_method="adjoint") + + program, _ = qml.device("default.qubit").preprocess(execution_config) + expanded_tapes, _ = program([qs]) + + assert len(expanded_tapes) == 1 + expanded_qs = expanded_tapes[0] + + expected_qs = qml.tape.QuantumScript( + [qml.RZ(0.1, wires=0), qml.RY(0.2, wires=0), qml.RZ(0.3, wires=0), qml.CNOT([0, 1])], + [qml.expval(qml.PauliZ(1))], + ) + + assert expanded_qs.operations == expected_qs.operations + assert expanded_qs.measurements == expected_qs.measurements + assert expanded_qs.trainable_params == expected_qs.trainable_params + + @pytest.mark.parametrize("max_workers", [None, 1, 2]) + def test_preprocess_single_circuit(self, max_workers): + """Test integration between preprocessing and execution with numpy parameters.""" + + # pylint: disable=too-few-public-methods + class MyTemplate(qml.operation.Operation): + """Temp operator.""" + + num_wires = 2 + + # pylint: disable=missing-function-docstring + def decomposition(self): + return [ + qml.RX(self.data[0], self.wires[0]), + qml.RY(self.data[1], self.wires[1]), + qml.CNOT(self.wires), + ] + + x = 0.928 + y = -0.792 + qscript = qml.tape.QuantumScript( + [MyTemplate(x, y, ("a", "b"))], + [qml.expval(qml.PauliY("a")), qml.expval(qml.PauliZ("a")), qml.expval(qml.PauliX("b"))], + ) + + dev = DefaultQubit(max_workers=max_workers) + tapes = tuple([qscript]) + program, config = dev.preprocess() + tapes, pre_processing_fn = program(tapes) + + assert len(tapes) == 1 + execute_circuit = tapes[0] + assert qml.equal(execute_circuit[0], qml.RX(x, "a")) + assert qml.equal(execute_circuit[1], qml.RY(y, "b")) + assert qml.equal(execute_circuit[2], qml.CNOT(("a", "b"))) + assert qml.equal(execute_circuit[3], qml.expval(qml.PauliY("a"))) + assert qml.equal(execute_circuit[4], qml.expval(qml.PauliZ("a"))) + assert qml.equal(execute_circuit[5], qml.expval(qml.PauliX("b"))) + + results = dev.execute(tapes, config) + assert len(results) == 1 + assert len(results[0]) == 3 + + processed_results = pre_processing_fn(results) + processed_result = processed_results[0] + assert len(processed_result) == 3 + assert qml.math.allclose(processed_result[0], -np.sin(x) * np.sin(y)) + assert qml.math.allclose(processed_result[1], np.cos(x)) + assert qml.math.allclose(processed_result[2], np.sin(y)) + + @pytest.mark.parametrize("max_workers", [None, 1, 2]) + def test_preprocess_batch_circuit(self, max_workers): + """Test preprocess integrates with default qubit when we start with a batch of circuits.""" + + # pylint: disable=too-few-public-methods + class CustomIsingXX(qml.operation.Operation): + """Temp operator.""" + + num_wires = 2 + + # pylint: disable=missing-function-docstring + def decomposition(self): + return [qml.IsingXX(self.data[0], self.wires)] + + x = 0.692 + + measurements1 = [qml.density_matrix("a"), qml.vn_entropy("a")] + qs1 = qml.tape.QuantumScript([CustomIsingXX(x, ("a", "b"))], measurements1) + + y = -0.923 + + with qml.queuing.AnnotatedQueue() as q: + qml.PauliX(wires=1) + m_0 = qml.measure(1) + qml.cond(m_0, qml.RY)(y, wires=0) + qml.expval(qml.PauliZ(0)) + + qs2 = qml.tape.QuantumScript.from_queue(q) + + initial_batch = [qs1, qs2] + + dev = DefaultQubit(max_workers=max_workers) + + program, config = dev.preprocess() + batch, pre_processing_fn = program(initial_batch) + + results = dev.execute(batch, config) + processed_results = pre_processing_fn(results) + + assert len(processed_results) == 2 + assert len(processed_results[0]) == 2 + + expected_density_mat = np.array([[np.cos(x / 2) ** 2, 0], [0, np.sin(x / 2) ** 2]]) + assert qml.math.allclose(processed_results[0][0], expected_density_mat) + + eig_1 = (1 + np.sqrt(1 - 4 * np.cos(x / 2) ** 2 * np.sin(x / 2) ** 2)) / 2 + eig_2 = (1 - np.sqrt(1 - 4 * np.cos(x / 2) ** 2 * np.sin(x / 2) ** 2)) / 2 + eigs = [eig_1, eig_2] + eigs = [eig for eig in eigs if eig > 0] + + expected_entropy = -np.sum(eigs * np.log(eigs)) + assert qml.math.allclose(processed_results[0][1], expected_entropy) + + expected_expval = np.cos(y) + assert qml.math.allclose(expected_expval, processed_results[1]) + + def test_preprocess_defer_measurements(self): + """Test preprocessing contains the defer measurement transform.""" + dev = DefaultQubit() + + program, _ = dev.preprocess() + assert qml.defer_measurements.transform in [t.transform for t in program] + + +class TestAdjointDiffTapeValidation: + """Unit tests for validate_and_expand_adjoint""" + + @pytest.mark.parametrize("diff_method", ["adjoint", "backprop"]) + def test_finite_shots_analytic_diff_method(self, diff_method): + """Test that a circuit with finite shots executed with diff_method "adjoint" + or "backprop" raises an error""" + tape = qml.tape.QuantumScript([], [qml.expval(qml.PauliZ(0))], shots=100) + + execution_config = ExecutionConfig(gradient_method=diff_method) + program, _ = qml.device("default.qubit").preprocess(execution_config) + + msg = "Finite shots are not supported with" + with pytest.raises(qml.DeviceError, match=msg): + program((tape,)) + + def test_not_expval(self): + """Test if a QuantumFunctionError is raised for a tape with measurements that are not + expectation values""" + + measurements = [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliX(3))] + qs = qml.tape.QuantumScript(ops=[], measurements=measurements) + + program = qml.device("default.qubit").preprocess( + ExecutionConfig(gradient_method="adjoint") + )[0] + + with pytest.raises( + qml.DeviceError, + match=r"not accepted for analytic simulation on adjoint", + ): + program((qs,)) + + def test_unsupported_op_decomposed(self): + """Test that an operation supported on the forward pass but not adjoint is decomposed when adjoint is requested.""" + + qs = qml.tape.QuantumScript([qml.U2(0.1, 0.2, wires=[0])], [qml.expval(qml.PauliZ(2))]) + batch = (qs,) + program = qml.device("default.qubit").preprocess( + ExecutionConfig(gradient_method="adjoint") + )[0] + res, _ = program(batch) + res = res[0] + assert isinstance(res, qml.tape.QuantumScript) + assert qml.equal(res[0], qml.RZ(0.2, wires=0)) + assert qml.equal(res[1], qml.RY(np.pi / 2, wires=0)) + assert qml.equal(res[2], qml.RZ(-0.2, wires=0)) + assert qml.equal(res[3], qml.PhaseShift(0.2, wires=0)) + assert qml.equal(res[4], qml.PhaseShift(0.1, wires=0)) + + def test_trainable_params_decomposed(self): + """Test that the trainable parameters of a tape are updated when it is expanded""" + ops = [ + qml.QubitUnitary([[0, 1], [1, 0]], wires=0), + qml.CNOT([0, 1]), + qml.Rot(0.1, 0.2, 0.3, wires=0), + ] + qs = qml.tape.QuantumScript(ops, [qml.expval(qml.PauliZ(0))]) + + qs.trainable_params = [0] + program = qml.device("default.qubit").preprocess( + ExecutionConfig(gradient_method="adjoint") + )[0] + res, _ = program((qs,)) + res = res[0] + + assert isinstance(res, qml.tape.QuantumScript) + assert len(res.operations) == 7 + assert qml.equal(res[0], qml.RZ(np.pi / 2, 0)) + assert qml.equal(res[1], qml.RY(np.pi, 0)) + assert qml.equal(res[2], qml.RZ(7 * np.pi / 2, 0)) + assert qml.equal(res[3], qml.CNOT([0, 1])) + assert qml.equal(res[4], qml.RZ(0.1, 0)) + assert qml.equal(res[5], qml.RY(0.2, 0)) + assert qml.equal(res[6], qml.RZ(0.3, 0)) + assert res.trainable_params == [0, 1, 2, 3, 4, 5] + + qs.trainable_params = [2, 3] + res, _ = program((qs,)) + res = res[0] + assert isinstance(res, qml.tape.QuantumScript) + assert len(res.operations) == 7 + assert qml.equal(res[0], qml.RZ(np.pi / 2, 0)) + assert qml.equal(res[1], qml.RY(np.pi, 0)) + assert qml.equal(res[2], qml.RZ(7 * np.pi / 2, 0)) + assert qml.equal(res[3], qml.CNOT([0, 1])) + assert qml.equal(res[4], qml.RZ(0.1, 0)) + assert qml.equal(res[5], qml.RY(0.2, 0)) + assert qml.equal(res[6], qml.RZ(0.3, 0)) + assert res.trainable_params == [0, 1, 2, 3, 4, 5] + + def test_u3_non_trainable_params(self): + """Test that a warning is raised and all parameters are trainable in the expanded + tape when not all parameters in U3 are trainable""" + qs = qml.tape.QuantumScript([qml.U3(0.2, 0.4, 0.6, wires=0)], [qml.expval(qml.PauliZ(0))]) + qs.trainable_params = [0, 2] + + program = qml.device("default.qubit").preprocess( + ExecutionConfig(gradient_method="adjoint") + )[0] + res, _ = program((qs,)) + res = res[0] + assert isinstance(res, qml.tape.QuantumScript) + + # U3 decomposes into 5 operators + assert len(res.operations) == 5 + assert res.trainable_params == [0, 1, 2, 3, 4] + + def test_unsupported_obs(self): + """Test that the correct error is raised if a Hamiltonian measurement is differentiated""" + obs = qml.Hamiltonian([2, 0.5], [qml.PauliZ(0), qml.PauliY(1)]) + qs = qml.tape.QuantumScript([qml.RX(0.5, wires=1)], [qml.expval(obs)]) + qs.trainable_params = {0} + + program = qml.device("default.qubit").preprocess( + ExecutionConfig(gradient_method="adjoint") + )[0] + + with pytest.raises(qml.DeviceError, match=r"Observable "): + program((qs,)) + + def test_trainable_hermitian_warns(self): + """Test attempting to compute the gradient of a tape that obtains the + expectation value of a Hermitian operator emits a warning if the + parameters to Hermitian are trainable.""" + + mx = qml.matrix(qml.PauliX(0) @ qml.PauliY(2)) + qs = qml.tape.QuantumScript([], [qml.expval(qml.Hermitian(mx, wires=[0, 2]))]) + + qs.trainable_params = {0} + + program = qml.device("default.qubit").preprocess( + ExecutionConfig(gradient_method="adjoint") + )[0] + + with pytest.warns( + UserWarning, match="Differentiating with respect to the input parameters of Hermitian" + ): + _ = program((qs,)) + + @pytest.mark.parametrize("G", [qml.RX, qml.RY, qml.RZ]) + def test_valid_tape_no_expand(self, G): + """Test that a tape that is valid doesn't raise errors and is not expanded""" + prep_op = qml.StatePrep( + qml.numpy.array([1.0, -1.0], requires_grad=False) / np.sqrt(2), wires=0 + ) + qs = qml.tape.QuantumScript( + ops=[prep_op, G(np.pi, wires=[0])], + measurements=[qml.expval(qml.PauliZ(0))], + ) + + program = qml.device("default.qubit").preprocess( + ExecutionConfig(gradient_method="adjoint") + )[0] + + qs.trainable_params = {1} + qs_valid, _ = program((qs,)) + qs_valid = qs_valid[0] + assert all(qml.equal(o1, o2) for o1, o2 in zip(qs.operations, qs_valid.operations)) + assert all(qml.equal(o1, o2) for o1, o2 in zip(qs.measurements, qs_valid.measurements)) + assert qs_valid.trainable_params == [0, 1] + + def test_valid_tape_with_expansion(self): + """Test that a tape that is valid with operations that need to be expanded doesn't raise errors + and is expanded""" + prep_op = qml.StatePrep( + qml.numpy.array([1.0, -1.0], requires_grad=False) / np.sqrt(2), wires=0 + ) + qs = qml.tape.QuantumScript( + ops=[prep_op, qml.Rot(0.1, 0.2, 0.3, wires=[0])], + measurements=[qml.expval(qml.PauliZ(0))], + ) + + program = qml.device("default.qubit").preprocess( + ExecutionConfig(gradient_method="adjoint") + )[0] + + qs.trainable_params = {1, 2, 3} + qs_valid, _ = program((qs,)) + qs_valid = qs_valid[0] + + expected_ops = [ + prep_op, + qml.RZ(0.1, wires=[0]), + qml.RY(0.2, wires=[0]), + qml.RZ(0.3, wires=[0]), + ] + + assert all(qml.equal(o1, o2) for o1, o2 in zip(qs_valid.operations, expected_ops)) + assert all(qml.equal(o1, o2) for o1, o2 in zip(qs.measurements, qs_valid.measurements)) + assert qs_valid.trainable_params == [0, 1, 2, 3] + assert qs.shots == qs_valid.shots diff --git a/tests/devices/qubit/test_adjoint_jacobian.py b/tests/devices/qubit/test_adjoint_jacobian.py index afac77d5854..dd5b2e23486 100644 --- a/tests/devices/qubit/test_adjoint_jacobian.py +++ b/tests/devices/qubit/test_adjoint_jacobian.py @@ -17,7 +17,11 @@ from pennylane.devices.qubit import adjoint_jacobian, adjoint_jvp, adjoint_vjp from pennylane.tape import QuantumScript import pennylane.numpy as np -from pennylane.devices.qubit.preprocess import validate_and_expand_adjoint + + +def adjoint_ops(op: qml.operation.Operator) -> bool: + """Specify whether or not an Operator is supported by adjoint differentiation.""" + return op.num_params == 0 or op.num_params == 1 and op.has_generator class TestAdjointJacobian: @@ -30,10 +34,7 @@ def test_custom_wire_labels(self, tol): ) qs.trainable_params = {0, 1} - qs_valid, _ = validate_and_expand_adjoint(qs) - qs_valid = qs_valid[0] - - calculated_val = adjoint_jacobian(qs_valid) + calculated_val = adjoint_jacobian(qs) tapes, fn = qml.gradients.finite_diff(qs) results = tuple(qml.devices.qubit.simulate(t) for t in tapes) @@ -48,17 +49,12 @@ def test_pauli_rotation_gradient(self, G, theta, tol): prep_op = qml.StatePrep(np.array([1.0, -1.0], requires_grad=False) / np.sqrt(2), wires=0) qs = QuantumScript( - ops=[G(theta, wires=[0])], measurements=[qml.expval(qml.PauliZ(0))], prep=[prep_op] + ops=[prep_op, G(theta, wires=[0])], measurements=[qml.expval(qml.PauliZ(0))] ) qs.trainable_params = {1} - qs_valid, _ = validate_and_expand_adjoint(qs) - qs_valid = qs_valid[0] - - qs_valid.trainable_params = {1} - - calculated_val = adjoint_jacobian(qs_valid) + calculated_val = adjoint_jacobian(qs) # compare to finite differences tapes, fn = qml.gradients.finite_diff(qs) results = tuple(qml.devices.qubit.simulate(t) for t in tapes) @@ -75,18 +71,17 @@ def test_Rot_gradient(self, theta, tol): prep_op = qml.StatePrep(np.array([1.0, -1.0], requires_grad=False) / np.sqrt(2), wires=0) qs = QuantumScript( - ops=[qml.Rot(*params, wires=[0])], + ops=[prep_op, qml.Rot(*params, wires=[0])], measurements=[qml.expval(qml.PauliZ(0))], - prep=[prep_op], ) qs.trainable_params = {1, 2, 3} - qs_valid, _ = validate_and_expand_adjoint(qs) - qs_valid = qs_valid[0] + qs_valid, _ = qml.devices.preprocess.decompose(qs, adjoint_ops) + qs = qs_valid[0] - qs_valid.trainable_params = {1, 2, 3} + qs.trainable_params = {1, 2, 3} - calculated_val = adjoint_jacobian(qs_valid) + calculated_val = adjoint_jacobian(qs) # compare to finite differences tapes, fn = qml.gradients.finite_diff(qs) @@ -120,7 +115,7 @@ def test_gradients(self, op, obs, tol): qs = QuantumScript(ops, measurements) qs.trainable_params = set(range(1, 1 + op.num_params)) - qs_valid, _ = validate_and_expand_adjoint(qs) + qs_valid, _ = qml.devices.preprocess.decompose(qs, adjoint_ops) qs_valid = qs_valid[0] qs_valid.trainable_params = set(range(1, 1 + op.num_params)) @@ -143,7 +138,7 @@ def test_multiple_rx_gradient(self, tol): [qml.RX(params[0], wires=0), qml.RX(params[1], wires=1), qml.RX(params[2], wires=2)], [qml.expval(qml.PauliZ(idx)) for idx in range(3)], ) - qs_valid, _ = validate_and_expand_adjoint(qs) + qs_valid, _ = qml.devices.preprocess.decompose(qs, adjoint_ops) qs_valid = qs_valid[0] # circuit jacobians @@ -177,7 +172,7 @@ def compute_matrix(angle): # pylint: disable=arguments-differ [MyOp(p, w) for p, w in zip(params, [0, 1, 2])], [qml.expval(qml.PauliZ(idx)) for idx in range(3)], ) - qs_valid, _ = validate_and_expand_adjoint(qs) + qs_valid, _ = qml.devices.preprocess.decompose(qs, adjoint_ops) qs_valid = qs_valid[0] # circuit jacobians @@ -198,7 +193,7 @@ def test_gradient_gate_with_multiple_parameters(self, tol): ) qs.trainable_params = {1, 2, 3} - qs_valid, _ = validate_and_expand_adjoint(qs) + qs_valid, _ = qml.devices.preprocess.decompose(qs, adjoint_ops) qs_valid = qs_valid[0] qs_valid.trainable_params = {1, 2, 3} @@ -223,13 +218,12 @@ def test_state_prep(self, prep_op, tol): x, y, z = [0.5, 0.3, -0.7] qs = QuantumScript( - [qml.RX(0.4, wires=[0]), qml.Rot(x, y, z, wires=[0]), qml.RY(-0.2, wires=[0])], + [prep_op, qml.RX(0.4, wires=[0]), qml.Rot(x, y, z, wires=[0]), qml.RY(-0.2, wires=[0])], [qml.expval(qml.PauliZ(0))], - [prep_op], ) qs.trainable_params = {2, 3, 4} - qs_valid, _ = validate_and_expand_adjoint(qs) + qs_valid, _ = qml.devices.preprocess.decompose(qs, adjoint_ops) qs_valid = qs_valid[0] qs_valid.trainable_params = {2, 3, 4} @@ -259,7 +253,7 @@ def test_gradient_of_tape_with_hermitian(self, tol): ) qs.trainable_params = {0, 1, 2} - qs_valid, _ = validate_and_expand_adjoint(qs) + qs_valid, _ = qml.devices.preprocess.decompose(qs, adjoint_ops) qs_valid = qs_valid[0] qs_valid.trainable_params = {0, 1, 2} @@ -290,7 +284,7 @@ def test_gradient_of_tape_with_tensor(self, tol): ) qs.trainable_params = {0, 1, 2} - qs_valid, _ = validate_and_expand_adjoint(qs) + qs_valid, _ = qml.devices.preprocess.decompose(qs, adjoint_ops) qs_valid = qs_valid[0] res = adjoint_jacobian(qs_valid) diff --git a/tests/devices/qubit/test_preprocess.py b/tests/devices/qubit/test_preprocess.py deleted file mode 100644 index 58a3bddea1e..00000000000 --- a/tests/devices/qubit/test_preprocess.py +++ /dev/null @@ -1,842 +0,0 @@ -# Copyright 2018-2023 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unit tests for preprocess in devices/qubit.""" - -import pytest - -import numpy as np -import pennylane.numpy as pnp -import pennylane as qml -from pennylane.operation import Operation -from pennylane.devices.qubit.preprocess import ( - _accepted_operator, - _accepted_adjoint_operator, - _operator_decomposition_gen, - expand_fn, - preprocess, - validate_and_expand_adjoint, - validate_measurements, - validate_multiprocessing_workers, -) -from pennylane.devices import ExecutionConfig -from pennylane.measurements import MidMeasureMP, MeasurementValue -from pennylane.tape import QuantumScript -from pennylane import DeviceError - -# pylint: disable=too-few-public-methods - - -class NoMatOp(Operation): - """Dummy operation for expanding circuit.""" - - # pylint: disable=arguments-renamed, invalid-overridden-method - @property - def has_matrix(self): - return False - - def decomposition(self): - return [qml.PauliX(self.wires), qml.PauliY(self.wires)] - - -class NoMatNoDecompOp(Operation): - """Dummy operation for checking check_validity throws error when - expected.""" - - # pylint: disable=arguments-renamed, invalid-overridden-method - @property - def has_matrix(self): - return False - - -class TestPrivateHelpers: - """Test the private helpers for preprocessing.""" - - @pytest.mark.parametrize( - "op, expected", - [ - (qml.PauliX(0), True), - (qml.CRX(0.1, wires=[0, 1]), True), - (qml.Snapshot(), True), - (qml.Barrier(), False), - (qml.QFT(wires=range(5)), True), - (qml.QFT(wires=range(10)), False), - (qml.GroverOperator(wires=range(10)), True), - (qml.GroverOperator(wires=range(14)), False), - (qml.pow(qml.RX(1.1, 0), 3), True), - (qml.pow(qml.RX(pnp.array(1.1), 0), 3), False), - ], - ) - def test_accepted_operator(self, op, expected): - """Test that _accepted_operator works correctly""" - res = _accepted_operator(op) - assert res == expected - - def test_adjoint_accepted_operator_only_one_wire(self): - """Tests adjoint accepts operators with no parameters or a sinlge parameter and a generator.""" - - assert _accepted_adjoint_operator(NoMatOp(wires=0)) - assert not _accepted_adjoint_operator(NoMatOp(1.2, wires=0)) - assert not _accepted_adjoint_operator(NoMatOp(1.2, 2.3, wires=0)) - - class CustomOpWithGenerator(qml.operation.Operator): - """A custom operator with a generator.""" - - def generator(self): - return qml.PauliX(0) - - assert _accepted_adjoint_operator(CustomOpWithGenerator(1.2, wires=0)) - - @pytest.mark.parametrize("op", (qml.PauliX(0), qml.RX(1.2, wires=0), qml.QFT(wires=range(3)))) - def test_operator_decomposition_gen_accepted_operator(self, op): - """Test the _operator_decomposition_gen function on an operator that is accepted.""" - casted_to_list = list(_operator_decomposition_gen(op, _accepted_operator)) - assert len(casted_to_list) == 1 - assert casted_to_list[0] is op - - def test_operator_decomposition_gen_decomposed_operators_single_nesting(self): - """Assert _operator_decomposition_gen turns into a list with the operators decomposition - when only a single layer of expansion is necessary.""" - op = NoMatOp("a") - casted_to_list = list(_operator_decomposition_gen(op, _accepted_operator)) - assert len(casted_to_list) == 2 - assert qml.equal(casted_to_list[0], qml.PauliX("a")) - assert qml.equal(casted_to_list[1], qml.PauliY("a")) - - def test_operator_decomposition_gen_decomposed_operator_ragged_nesting(self): - """Test that _operator_decomposition_gen handles a decomposition that requires different depths of decomposition.""" - - class RaggedDecompositionOp(Operation): - """class with a ragged decomposition.""" - - num_wires = 1 - - def decomposition(self): - return [NoMatOp(self.wires), qml.S(self.wires), qml.adjoint(NoMatOp(self.wires))] - - op = RaggedDecompositionOp("a") - final_decomp = list(_operator_decomposition_gen(op, _accepted_operator)) - assert len(final_decomp) == 5 - assert qml.equal(final_decomp[0], qml.PauliX("a")) - assert qml.equal(final_decomp[1], qml.PauliY("a")) - assert qml.equal(final_decomp[2], qml.S("a")) - assert qml.equal(final_decomp[3], qml.adjoint(qml.PauliY("a"))) - assert qml.equal(final_decomp[4], qml.adjoint(qml.PauliX("a"))) - - def test_error_from_unsupported_operation(self): - """Test that a device error is raised if the operator cant be decomposed and doesn't have a matrix.""" - op = NoMatNoDecompOp("a") - with pytest.raises(DeviceError, match=r"Operator NoMatNoDecompOp"): - tuple(_operator_decomposition_gen(op, _accepted_operator)) - - -class TestExpandFnValidation: - """Unit tests for helper functions in qml.devices.qubit.preprocess""" - - def test_error_if_invalid_op(self): - """Test that expand_fn throws an error when an operation is does not define a matrix or decomposition.""" - tape = QuantumScript(ops=[NoMatNoDecompOp(0)], measurements=[qml.expval(qml.Hadamard(0))]) - with pytest.raises(DeviceError, match="Operator NoMatNoDecompOp"): - expand_fn(tape) - - def test_expand_fn_invalid_observable(self): - """Test that expand_fn throws an error when an observable is invalid.""" - tape = QuantumScript( - ops=[qml.PauliX(0)], measurements=[qml.expval(qml.GellMann(wires=0, index=1))] - ) - with pytest.raises(DeviceError, match=r"Observable GellMann1"): - expand_fn(tape) - - def test_expand_fn_invalid_tensor_observable(self): - """Test that expand_fn throws an error when a tensor includes invalid obserables""" - tape = QuantumScript( - ops=[qml.PauliX(0), qml.PauliY(1)], - measurements=[qml.expval(qml.PauliX(0) @ qml.GellMann(wires=1, index=2))], - ) - with pytest.raises(DeviceError, match="Observable expval"): - expand_fn(tape) - - def test_valid_tensor_observable(self): - """Test that a valid tensor ovservable passes without error.""" - tape = QuantumScript([], [qml.expval(qml.PauliZ(0) @ qml.PauliY(1))]) - assert expand_fn(tape)[0][0] is tape - - def test_expand_fn_passes(self): - """Test that expand_fn doesn't throw any errors for a valid circuit""" - tape = QuantumScript( - ops=[qml.PauliX(0), qml.RZ(0.123, wires=0)], measurements=[qml.state()] - ) - expand_fn(tape) - - def test_infinite_decomposition_loop(self): - """Test that a device error is raised if decomposition enters an infinite loop.""" - - class InfiniteOp(qml.operation.Operation): - """An op with an infinite decomposition.""" - - num_wires = 1 - - def decomposition(self): - return [InfiniteOp(*self.parameters, self.wires)] - - qs = qml.tape.QuantumScript([InfiniteOp(1.23, 0)]) - with pytest.raises(DeviceError, match=r"Reached recursion limit trying to decompose"): - expand_fn(qs) - - with pytest.raises(DeviceError, match=r"Reached recursion limit trying to decompose"): - validate_and_expand_adjoint(qs) - - -class TestExpandFnTransformations: - """Tests for the behavior of the `expand_fn` helper.""" - - @pytest.mark.parametrize("shots", [None, 100]) - def test_expand_fn_expand_unsupported_op(self, shots): - """Test that expand_fn expands the tape when unsupported operators are present""" - ops = [qml.Hadamard(0), NoMatOp(1), qml.RZ(0.123, wires=1)] - measurements = [qml.expval(qml.PauliZ(0)), qml.probs()] - tape = QuantumScript(ops=ops, measurements=measurements, shots=shots) - - expanded_tapes, _ = expand_fn(tape) - expanded_tape = expanded_tapes[0] - expected = [qml.Hadamard(0), qml.PauliX(1), qml.PauliY(1), qml.RZ(0.123, wires=1)] - - for op, exp in zip(expanded_tape.circuit, expected + measurements): - assert qml.equal(op, exp) - - assert tape.shots == expanded_tape.shots - - # pylint: disable=no-member - def test_expand_fn_defer_measurement(self): - """Test that expand_fn defers mid-circuit measurements.""" - mp = MidMeasureMP(wires=[0], reset=True, id="test_id") - mv = MeasurementValue([mp], processing_fn=lambda v: v) - ops = [ - qml.Hadamard(0), - mp, - qml.transforms.Conditional(mv, qml.RX(0.123, wires=1)), - ] - measurements = [qml.expval(qml.PauliZ(1))] - tape = QuantumScript(ops=ops, measurements=measurements) - - expanded_tapes, _ = expand_fn(tape) - expanded_tape = expanded_tapes[0] - expected = [ - qml.Hadamard(0), - qml.CNOT([0, 2]), - qml.CNOT([2, 0]), - qml.ops.Controlled(qml.RX(0.123, wires=1), 2), - ] - - for op, exp in zip(expanded_tape, expected + measurements): - assert qml.equal(op, exp) - - def test_expand_fn_no_expansion(self): - """Test that expand_fn does nothing to a fully supported quantum script.""" - ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RZ(0.123, wires=1)] - measurements = [qml.expval(qml.PauliZ(0)), qml.probs()] - tape = QuantumScript(ops=ops, measurements=measurements) - expanded_tapes, _ = expand_fn(tape) - expanded_tape = expanded_tapes[0] - - for op, exp in zip(expanded_tape.circuit, ops + measurements): - assert qml.equal(op, exp) - - def test_expand_fn_non_commuting_measurements(self): - """Test that expand function can decompose operations even when non commuting measurements exist in the circuit.""" - - qs = QuantumScript([NoMatOp("a")], [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(0))]) - new_qs, _ = expand_fn(qs) - new_qs = new_qs[0] - print(new_qs.circuit) - assert new_qs.measurements == qs.measurements - - @pytest.mark.parametrize( - "prep_op", - ( - qml.BasisState([1], wires=0), - qml.StatePrep([0, 1], wires=1), - qml.AmplitudeEmbedding([0, 1], wires=1), - ), - ) - def test_expand_fn_state_prep(self, prep_op): - """Test that the expand_fn only expands mid-circuit instances of StatePrepBase""" - ops = [ - prep_op, - qml.Hadamard(wires=0), - qml.StatePrep([0, 1], wires=1), - qml.BasisState([1], wires=0), - qml.RZ(0.123, wires=1), - qml.AmplitudeEmbedding([0, 1, 0, 0], wires=[0, 1]), - ] - measurements = [qml.expval(qml.PauliZ(0)), qml.probs()] - tape = QuantumScript(ops=ops, measurements=measurements) - - expanded_tapes, _ = expand_fn(tape) - expanded_tape = expanded_tapes[0] - expected = [ - prep_op, - qml.Hadamard(0), - qml.RY(3.14159265, wires=1), # decomposition of StatePrep - qml.PauliX(wires=0), # decomposition of BasisState - qml.RZ(0.123, wires=1), - qml.RY(1.57079633, wires=[1]), # decomposition of AmplitudeEmbedding - qml.CNOT(wires=[0, 1]), - qml.RY(1.57079633, wires=[1]), - qml.CNOT(wires=[0, 1]), - ] - - assert expanded_tape.circuit == expected + measurements - - -class TestValidateMeasurements: - """Unit tests for the validate_measurements function""" - - @pytest.mark.parametrize( - "measurements", - [ - [qml.state()], - [qml.expval(qml.PauliZ(0))], - [qml.state(), qml.expval(qml.PauliZ(0)), qml.probs(0)], - [qml.state(), qml.vn_entropy(0), qml.mutual_info(0, 1)], - ], - ) - def test_only_state_measurements(self, measurements): - """Test that an analytic circuit containing only StateMeasurements works""" - tape = QuantumScript([], measurements, shots=None) - validate_measurements(tape) - - @pytest.mark.parametrize( - "measurements", - [ - [qml.sample(wires=0)], - [qml.expval(qml.PauliZ(0))], - [qml.sample(wires=0), qml.expval(qml.PauliZ(0)), qml.probs(0)], - [qml.classical_shadow(wires=[0])], - [qml.shadow_expval(qml.PauliZ(0))], - ], - ) - def test_only_sample_measurements(self, measurements): - """Test that a circuit with finite shots containing only SampleMeasurements works""" - tape = QuantumScript([], measurements, shots=100) - validate_measurements(tape) - - @pytest.mark.parametrize( - "measurements", - [ - [qml.sample(wires=0)], - [qml.state(), qml.sample(wires=0)], - [qml.sample(wires=0), qml.expval(qml.PauliZ(0))], - [qml.classical_shadow(wires=[0])], - [qml.shadow_expval(qml.PauliZ(0))], - ], - ) - def test_analytic_with_samples(self, measurements): - """Test that an analytic circuit containing SampleMeasurements raises an error""" - tape = QuantumScript([], measurements, shots=None) - - msg = "Analytic circuits must only contain StateMeasurements" - with pytest.raises(DeviceError, match=msg): - validate_measurements(tape) - - @pytest.mark.parametrize( - "measurements", - [ - [qml.state()], - [qml.sample(wires=0), qml.state()], - [qml.expval(qml.PauliZ(0)), qml.state(), qml.sample(wires=0)], - ], - ) - def test_finite_shots_with_state(self, measurements): - """Test that a circuit with finite shots containing StateMeasurements raises an error""" - tape = QuantumScript([], measurements, shots=100) - - msg = "Circuits with finite shots must only contain SampleMeasurements" - with pytest.raises(DeviceError, match=msg): - validate_measurements(tape) - - @pytest.mark.parametrize("diff_method", ["adjoint", "backprop"]) - def test_finite_shots_analytic_diff_method(self, diff_method): - """Test that a circuit with finite shots executed with diff_method "adjoint" - or "backprop" raises an error""" - tape = QuantumScript([], [qml.expval(qml.PauliZ(0))], shots=100) - - execution_config = ExecutionConfig() - execution_config.gradient_method = diff_method - - msg = "Circuits with finite shots must be executed with non-analytic gradient methods" - with pytest.raises(DeviceError, match=msg): - validate_measurements(tape, execution_config) - - -class TestBatchTransform: - """Tests for the batch transformations.""" - - def test_batch_transform_no_batching(self): - """Test that batch_transform does nothing when no batching is required.""" - ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(0.123, wires=1)] - measurements = [qml.expval(qml.PauliZ(1))] - tape = QuantumScript(ops=ops, measurements=measurements) - - device = qml.devices.DefaultQubit() - - program, _ = device.preprocess() - tapes, _ = program([tape]) - - assert len(tapes) == 1 - for op, expected in zip(tapes[0].circuit, ops + measurements): - assert qml.equal(op, expected) - - def test_batch_transform_broadcast_not_adjoint(self): - """Test that batch_transform does nothing when batching is required but - internal PennyLane broadcasting can be used (diff method != adjoint)""" - ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX([np.pi, np.pi / 2], wires=1)] - measurements = [qml.expval(qml.PauliZ(1))] - tape = QuantumScript(ops=ops, measurements=measurements) - device = qml.devices.DefaultQubit() - - program, _ = device.preprocess() - tapes, _ = program([tape]) - - assert len(tapes) == 1 - for op, expected in zip(tapes[0].circuit, ops + measurements): - assert qml.equal(op, expected) - - def test_batch_transform_broadcast_adjoint(self): - """Test that batch_transform splits broadcasted tapes correctly when - the diff method is adjoint""" - ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX([np.pi, np.pi / 2], wires=1)] - measurements = [qml.expval(qml.PauliZ(1))] - tape = QuantumScript(ops=ops, measurements=measurements) - - execution_config = ExecutionConfig() - execution_config.gradient_method = "adjoint" - - device = qml.devices.DefaultQubit() - - program, _ = device.preprocess(execution_config=execution_config) - tapes, _ = program([tape]) - expected_ops = [ - [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(np.pi, wires=1)], - [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(np.pi / 2, wires=1)], - ] - - assert len(tapes) == 2 - for i, t in enumerate(tapes): - for op, expected in zip(t.circuit, expected_ops[i] + measurements): - assert qml.equal(op, expected) - - -class TestAdjointDiffTapeValidation: - """Unit tests for validate_and_expand_adjoint""" - - def test_not_expval(self): - """Test if a QuantumFunctionError is raised for a tape with measurements that are not - expectation values""" - - measurements = [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliX(3)), qml.sample()] - qs = QuantumScript(ops=[], measurements=measurements) - - with pytest.raises( - DeviceError, - match="Adjoint differentiation method does not support measurement VarianceMP.", - ): - validate_and_expand_adjoint(qs) - - def test_unsupported_op_decomposed(self): - """Test that an operation supported on the forward pass but not adjoint is decomposed when adjoint is requested.""" - - qs = QuantumScript([qml.U2(0.1, 0.2, wires=[0])], [qml.expval(qml.PauliZ(2))]) - res, _ = validate_and_expand_adjoint(qs) - res = res[0] - assert isinstance(res, qml.tape.QuantumScript) - assert qml.equal(res[0], qml.RZ(0.2, wires=0)) - assert qml.equal(res[1], qml.RY(np.pi / 2, wires=0)) - assert qml.equal(res[2], qml.RZ(-0.2, wires=0)) - assert qml.equal(res[3], qml.PhaseShift(0.2, wires=0)) - assert qml.equal(res[4], qml.PhaseShift(0.1, wires=0)) - - def test_trainable_params_decomposed(self): - """Test that the trainable parameters of a tape are updated when it is expanded""" - ops = [ - qml.QubitUnitary([[0, 1], [1, 0]], wires=0), - qml.CNOT([0, 1]), - qml.Rot(0.1, 0.2, 0.3, wires=0), - ] - qs = QuantumScript(ops, [qml.expval(qml.PauliZ(0))]) - - qs.trainable_params = [0] - res, _ = validate_and_expand_adjoint(qs) - res = res[0] - - assert isinstance(res, QuantumScript) - assert len(res.operations) == 7 - assert qml.equal(res[0], qml.RZ(np.pi / 2, 0)) - assert qml.equal(res[1], qml.RY(np.pi, 0)) - assert qml.equal(res[2], qml.RZ(7 * np.pi / 2, 0)) - assert qml.equal(res[3], qml.CNOT([0, 1])) - assert qml.equal(res[4], qml.RZ(0.1, 0)) - assert qml.equal(res[5], qml.RY(0.2, 0)) - assert qml.equal(res[6], qml.RZ(0.3, 0)) - assert res.trainable_params == [0, 1, 2, 3, 4, 5] - - qs.trainable_params = [2, 3] - res, _ = validate_and_expand_adjoint(qs) - res = res[0] - assert isinstance(res, QuantumScript) - assert len(res.operations) == 7 - assert qml.equal(res[0], qml.RZ(np.pi / 2, 0)) - assert qml.equal(res[1], qml.RY(np.pi, 0)) - assert qml.equal(res[2], qml.RZ(7 * np.pi / 2, 0)) - assert qml.equal(res[3], qml.CNOT([0, 1])) - assert qml.equal(res[4], qml.RZ(0.1, 0)) - assert qml.equal(res[5], qml.RY(0.2, 0)) - assert qml.equal(res[6], qml.RZ(0.3, 0)) - assert res.trainable_params == [0, 1, 2, 3, 4, 5] - - def test_u3_non_trainable_params(self): - """Test that a warning is raised and all parameters are trainable in the expanded - tape when not all parameters in U3 are trainable""" - qs = QuantumScript([qml.U3(0.2, 0.4, 0.6, wires=0)], [qml.expval(qml.PauliZ(0))]) - qs.trainable_params = [0, 2] - - res, _ = validate_and_expand_adjoint(qs) - res = res[0] - assert isinstance(res, QuantumScript) - - # U3 decomposes into 5 operators - assert len(res.operations) == 5 - assert res.trainable_params == [0, 1, 2, 3, 4] - - def test_unsupported_obs(self): - """Test that the correct error is raised if a Hamiltonian or Sum measurement is differentiated""" - obs = qml.Hamiltonian([2, 0.5], [qml.PauliZ(0), qml.PauliY(1)]) - qs = QuantumScript([qml.RX(0.5, wires=1)], [qml.expval(obs)]) - qs.trainable_params = {0} - - with pytest.raises( - DeviceError, - match="Adjoint differentiation method does not support observable Hamiltonian.", - ): - validate_and_expand_adjoint(qs) - - def test_trainable_hermitian_warns(self): - """Test attempting to compute the gradient of a tape that obtains the - expectation value of a Hermitian operator emits a warning if the - parameters to Hermitian are trainable.""" - - mx = qml.matrix(qml.PauliX(0) @ qml.PauliY(2)) - qs = QuantumScript([], [qml.expval(qml.Hermitian(mx, wires=[0, 2]))]) - - qs.trainable_params = {0} - - with pytest.warns( - UserWarning, match="Differentiating with respect to the input parameters of Hermitian" - ): - _ = validate_and_expand_adjoint(qs) - - @pytest.mark.parametrize("G", [qml.RX, qml.RY, qml.RZ]) - def test_valid_tape_no_expand(self, G): - """Test that a tape that is valid doesn't raise errors and is not expanded""" - prep_op = qml.StatePrep(pnp.array([1.0, -1.0], requires_grad=False) / np.sqrt(2), wires=0) - qs = QuantumScript( - ops=[G(np.pi, wires=[0])], - measurements=[qml.expval(qml.PauliZ(0))], - prep=[prep_op], - ) - - qs.trainable_params = {1} - qs_valid, _ = validate_and_expand_adjoint(qs) - qs_valid = qs_valid[0] - assert all(qml.equal(o1, o2) for o1, o2 in zip(qs.operations, qs_valid.operations)) - assert all(qml.equal(o1, o2) for o1, o2 in zip(qs.measurements, qs_valid.measurements)) - assert qs_valid.trainable_params == [0, 1] - - @pytest.mark.parametrize("shots", [None, 100]) - def test_valid_tape_with_expansion(self, shots): - """Test that a tape that is valid with operations that need to be expanded doesn't raise errors - and is expanded""" - prep_op = qml.StatePrep(pnp.array([1.0, -1.0], requires_grad=False) / np.sqrt(2), wires=0) - qs = QuantumScript( - ops=[qml.Rot(0.1, 0.2, 0.3, wires=[0])], - measurements=[qml.expval(qml.PauliZ(0))], - prep=[prep_op], - shots=shots, - ) - - qs.trainable_params = {1, 2, 3} - qs_valid, _ = validate_and_expand_adjoint(qs) - qs_valid = qs_valid[0] - - expected_ops = [ - prep_op, - qml.RZ(0.1, wires=[0]), - qml.RY(0.2, wires=[0]), - qml.RZ(0.3, wires=[0]), - ] - - assert all(qml.equal(o1, o2) for o1, o2 in zip(qs_valid.operations, expected_ops)) - assert all(qml.equal(o1, o2) for o1, o2 in zip(qs.measurements, qs_valid.measurements)) - assert qs_valid.trainable_params == [0, 1, 2, 3] - assert qs.shots == qs_valid.shots - - -class TestPreprocess: - """Unit tests for ``qml.devices.qubit.preprocess``.""" - - def test_choose_best_gradient_method(self): - """Test that preprocessing chooses backprop as the best gradient method.""" - config = qml.devices.ExecutionConfig(gradient_method="best") - _, config = preprocess(config) - assert config.gradient_method == "backprop" - assert config.use_device_gradient - assert not config.grad_on_execution - - def test_config_choices_for_adjoint(self): - """Test that preprocessing request grad on execution and says to use the device gradient if adjoint is requested.""" - config = qml.devices.ExecutionConfig( - gradient_method="adjoint", use_device_gradient=None, grad_on_execution=None - ) - _, new_config = preprocess(config) - - assert new_config.use_device_gradient - assert new_config.grad_on_execution - - def test_preprocess_batch_transform_not_adjoint(self): - """Test that preprocess returns the correct tapes when a batch transform - is needed.""" - ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX([np.pi, np.pi / 2], wires=1)] - # Need to specify grouping type to transform tape - measurements = [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1))] - tapes = [ - QuantumScript(ops=ops, measurements=[measurements[0]]), - QuantumScript(ops=ops, measurements=[measurements[1]]), - ] - - program, _ = preprocess() - res_tapes, batch_fn = program(tapes) - - assert len(res_tapes) == 2 - for i, t in enumerate(res_tapes): - for op, expected_op in zip(t.operations, ops): - assert qml.equal(op, expected_op) - assert len(t.measurements) == 1 - if i == 0: - assert qml.equal(t.measurements[0], measurements[0]) - else: - assert qml.equal(t.measurements[0], measurements[1]) - - input = ([[1, 2], [3, 4]], [[5, 6], [7, 8]]) - assert np.array_equal(batch_fn(input), np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])) - - def test_preprocess_batch_transform_adjoint(self): - """Test that preprocess returns the correct tapes when a batch transform - is needed.""" - ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX([np.pi, np.pi / 2], wires=1)] - # Need to specify grouping type to transform tape - measurements = [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1))] - tapes = [ - QuantumScript(ops=ops, measurements=[measurements[0]]), - QuantumScript(ops=ops, measurements=[measurements[1]]), - ] - - execution_config = ExecutionConfig() - execution_config.gradient_method = "adjoint" - - program, _ = preprocess(execution_config=execution_config) - res_tapes, batch_fn = program(tapes) - - expected_ops = [ - [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(np.pi, wires=1)], - [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RX(np.pi / 2, wires=1)], - ] - - assert len(res_tapes) == 4 - for i, t in enumerate(res_tapes): - for op, expected_op in zip(t.operations, expected_ops[i % 2]): - assert qml.equal(op, expected_op) - assert len(t.measurements) == 1 - if i < 2: - assert qml.equal(t.measurements[0], measurements[0]) - else: - assert qml.equal(t.measurements[0], measurements[1]) - - input = ([[1, 2]], [[3, 4]], [[5, 6]], [[7, 8]]) - assert np.array_equal(batch_fn(input), np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])) - - def test_preprocess_expand(self): - """Test that preprocess returns the correct tapes when expansion is needed.""" - ops = [qml.Hadamard(0), NoMatOp(1), qml.RZ(0.123, wires=1)] - measurements = [[qml.expval(qml.PauliZ(0))], [qml.expval(qml.PauliX(1))]] - tapes = [ - QuantumScript(ops=ops, measurements=measurements[0]), - QuantumScript(ops=ops, measurements=measurements[1]), - ] - - program, _ = preprocess() - res_tapes, batch_fn = program(tapes) - - expected = [qml.Hadamard(0), qml.PauliX(1), qml.PauliY(1), qml.RZ(0.123, wires=1)] - - assert len(res_tapes) == 2 - for i, t in enumerate(res_tapes): - for op, exp in zip(t.circuit, expected + measurements[i]): - assert qml.equal(op, exp) - - input = (("a", "b"), "c", "d") - assert batch_fn(input) == (("a", "b"), "c") - - def test_preprocess_split_and_expand_not_adjoint(self): - """Test that preprocess returns the correct tapes when splitting and expanding - is needed.""" - ops = [qml.Hadamard(0), NoMatOp(1), qml.RX([np.pi, np.pi / 2], wires=1)] - # Need to specify grouping type to transform tape - measurements = [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1))] - tapes = [ - QuantumScript(ops=ops, measurements=[measurements[0]]), - QuantumScript(ops=ops, measurements=[measurements[1]]), - ] - - program, _ = preprocess() - res_tapes, batch_fn = program(tapes) - expected_ops = [ - qml.Hadamard(0), - qml.PauliX(1), - qml.PauliY(1), - qml.RX([np.pi, np.pi / 2], wires=1), - ] - - assert len(res_tapes) == 2 - for i, t in enumerate(res_tapes): - for op, expected_op in zip(t.operations, expected_ops): - assert qml.equal(op, expected_op) - assert len(t.measurements) == 1 - if i == 0: - assert qml.equal(t.measurements[0], measurements[0]) - else: - assert qml.equal(t.measurements[0], measurements[1]) - - input = ([[1, 2], [3, 4]], [[5, 6], [7, 8]]) - assert np.array_equal(batch_fn(input), np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])) - - def test_preprocess_split_and_expand_adjoint(self): - """Test that preprocess returns the correct tapes when splitting and expanding - is needed.""" - ops = [qml.Hadamard(0), NoMatOp(1), qml.RX([np.pi, np.pi / 2], wires=1)] - # Need to specify grouping type to transform tape - measurements = [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(1))] - tapes = [ - QuantumScript(ops=ops, measurements=[measurements[0]]), - QuantumScript(ops=ops, measurements=[measurements[1]]), - ] - - execution_config = ExecutionConfig() - execution_config.gradient_method = "adjoint" - - program, _ = preprocess(execution_config=execution_config) - res_tapes, batch_fn = program(tapes) - - expected_ops = [ - [qml.Hadamard(0), qml.PauliX(1), qml.PauliY(1), qml.RX(np.pi, wires=1)], - [qml.Hadamard(0), qml.PauliX(1), qml.PauliY(1), qml.RX(np.pi / 2, wires=1)], - ] - - assert len(res_tapes) == 4 - for i, t in enumerate(res_tapes): - for op, expected_op in zip(t.operations, expected_ops[i % 2]): - assert qml.equal(op, expected_op) - assert len(t.measurements) == 1 - if i < 2: - assert qml.equal(t.measurements[0], measurements[0]) - else: - assert qml.equal(t.measurements[0], measurements[1]) - - input = ([[1, 2]], [[3, 4]], [[5, 6]], [[7, 8]]) - assert np.array_equal(batch_fn(input), np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])) - - def test_preprocess_check_validity_fail(self): - """Test that preprocess throws an error if the split and expanded tapes have - unsupported operators.""" - ops = [qml.Hadamard(0), NoMatNoDecompOp(1), qml.RZ(0.123, wires=1)] - measurements = [[qml.expval(qml.PauliZ(0))], [qml.expval(qml.PauliX(1))]] - tapes = [ - QuantumScript(ops=ops, measurements=measurements[0]), - QuantumScript(ops=ops, measurements=measurements[1]), - ] - - with pytest.raises(qml.DeviceError, match="Operator NoMatNoDecompOp"): - program, _ = preprocess() - program(tapes) - - @pytest.mark.parametrize( - "ops, measurement, message", - [ - ( - [qml.RX(0.1, wires=0)], - [qml.probs(wires=[0, 1, 2])], - "does not support measurement ProbabilityMP", - ), - ( - [qml.RX(0.1, wires=0)], - [qml.expval(qml.Hamiltonian([1], [qml.PauliZ(0)]))], - "does not support observable Hamiltonian", - ), - ], - ) - @pytest.mark.filterwarnings("ignore:Differentiating with respect to") - def test_preprocess_invalid_tape_adjoint(self, ops, measurement, message): - """Test that preprocessing fails if adjoint differentiation is requested and an - invalid tape is used""" - qs = QuantumScript(ops, measurement) - execution_config = qml.devices.ExecutionConfig(gradient_method="adjoint") - - with pytest.raises(DeviceError, match=message): - program, _ = preprocess(execution_config) - program([qs]) - - def test_preprocess_tape_for_adjoint(self): - """Test that a tape is expanded correctly if adjoint differentiation is requested""" - qs = QuantumScript( - [qml.Rot(0.1, 0.2, 0.3, wires=0), qml.CNOT([0, 1])], - [qml.expval(qml.PauliZ(1))], - ) - execution_config = qml.devices.ExecutionConfig(gradient_method="adjoint") - - program, _ = preprocess(execution_config) - expanded_tapes, _ = program([qs]) - - assert len(expanded_tapes) == 1 - expanded_qs = expanded_tapes[0] - - expected_qs = QuantumScript( - [qml.RZ(0.1, wires=0), qml.RY(0.2, wires=0), qml.RZ(0.3, wires=0), qml.CNOT([0, 1])], - [qml.expval(qml.PauliZ(1))], - ) - - assert all( - qml.equal(o1, o2) for o1, o2 in zip(expanded_qs.operations, expected_qs.operations) - ) - assert all( - qml.equal(o1, o2) for o1, o2 in zip(expanded_qs.measurements, expected_qs.measurements) - ) - assert expanded_qs.trainable_params == expected_qs.trainable_params - - -def test_validate_multiprocessing_workers_None(): - """Test that validation does not fail when max_workers is None""" - qs = QuantumScript( - [qml.Rot(0.1, 0.2, 0.3, wires=0), qml.CNOT([0, 1])], - [qml.expval(qml.PauliZ(1))], - ) - device = qml.devices.DefaultQubit() - validate_multiprocessing_workers(qs, None, device) diff --git a/tests/devices/test_preprocess.py b/tests/devices/test_preprocess.py new file mode 100644 index 00000000000..0a4d58fff57 --- /dev/null +++ b/tests/devices/test_preprocess.py @@ -0,0 +1,397 @@ +# Copyright 2018-2023 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Unit tests for preprocess in devices/qubit.""" + +import pytest + +import pennylane as qml +from pennylane.operation import Operation +from pennylane.tape import QuantumScript +from pennylane import DeviceError + +# pylint: disable=too-few-public-methods + +from pennylane.devices.preprocess import ( + no_sampling, + validate_device_wires, + validate_multiprocessing_workers, + warn_about_trainable_observables, + _operator_decomposition_gen, + decompose, + validate_observables, + validate_measurements, +) + + +class NoMatOp(Operation): + """Dummy operation for expanding circuit.""" + + # pylint: disable=arguments-renamed, invalid-overridden-method + @property + def has_matrix(self): + return False + + def decomposition(self): + return [qml.PauliX(self.wires), qml.PauliY(self.wires)] + + +class NoMatNoDecompOp(Operation): + """Dummy operation for checking check_validity throws error when + expected.""" + + # pylint: disable=arguments-renamed, invalid-overridden-method + @property + def has_matrix(self): + return False + + +class TestPrivateHelpers: + """Test the private helpers for preprocessing.""" + + @pytest.mark.parametrize("op", (qml.PauliX(0), qml.RX(1.2, wires=0), qml.QFT(wires=range(3)))) + def test_operator_decomposition_gen_accepted_operator(self, op): + """Test the _operator_decomposition_gen function on an operator that is accepted.""" + + def stopping_condition(op): + return op.has_matrix + + casted_to_list = list(_operator_decomposition_gen(op, stopping_condition)) + assert len(casted_to_list) == 1 + assert casted_to_list[0] is op + + def test_operator_decomposition_gen_decomposed_operators_single_nesting(self): + """Assert _operator_decomposition_gen turns into a list with the operators decomposition + when only a single layer of expansion is necessary.""" + + def stopping_condition(op): + return op.has_matrix + + op = NoMatOp("a") + casted_to_list = list(_operator_decomposition_gen(op, stopping_condition)) + assert len(casted_to_list) == 2 + assert qml.equal(casted_to_list[0], qml.PauliX("a")) + assert qml.equal(casted_to_list[1], qml.PauliY("a")) + + def test_operator_decomposition_gen_decomposed_operator_ragged_nesting(self): + """Test that _operator_decomposition_gen handles a decomposition that requires different depths of decomposition.""" + + def stopping_condition(op): + return op.has_matrix + + class RaggedDecompositionOp(Operation): + """class with a ragged decomposition.""" + + num_wires = 1 + + def decomposition(self): + return [NoMatOp(self.wires), qml.S(self.wires), qml.adjoint(NoMatOp(self.wires))] + + op = RaggedDecompositionOp("a") + final_decomp = list(_operator_decomposition_gen(op, stopping_condition)) + assert len(final_decomp) == 5 + assert qml.equal(final_decomp[0], qml.PauliX("a")) + assert qml.equal(final_decomp[1], qml.PauliY("a")) + assert qml.equal(final_decomp[2], qml.S("a")) + assert qml.equal(final_decomp[3], qml.adjoint(qml.PauliY("a"))) + assert qml.equal(final_decomp[4], qml.adjoint(qml.PauliX("a"))) + + def test_error_from_unsupported_operation(self): + """Test that a device error is raised if the operator cant be decomposed and doesn't have a matrix.""" + op = NoMatNoDecompOp("a") + with pytest.raises( + DeviceError, + match=r"not supported on abc and does", + ): + tuple(_operator_decomposition_gen(op, lambda op: op.has_matrix, name="abc")) + + +def test_no_sampling(): + """Tests for the no_sampling transform.""" + + tape1 = qml.tape.QuantumScript(shots=None) + batch, _ = no_sampling(tape1) + assert batch[0] is tape1 + + tape2 = qml.tape.QuantumScript(shots=2) + with pytest.raises(qml.DeviceError, match="Finite shots are not supported with abc"): + no_sampling(tape2, name="abc") + + +def test_warn_about_trainable_observables(): + """Tests warning raised for warn_about_trainable_observables.""" + tape = qml.tape.QuantumScript([], [qml.expval(2 * qml.PauliX(0))]) + with pytest.warns(UserWarning, match="Differentiating with respect to the input "): + warn_about_trainable_observables(tape) + + +class TestValidateDeviceWires: + def test_error(self): + """Tests for the error raised by validate_device_wires transform.""" + + tape1 = qml.tape.QuantumScript([qml.S("a")]) + with pytest.raises(qml.wires.WireError, match="on abc as they contain wires"): + validate_device_wires(tape1, wires=qml.wires.Wires((0,)), name="abc") + + def test_null_if_no_wires_provided(self): + """Test that nothing happens if no wires are provided to the transform.""" + + tape1 = qml.tape.QuantumScript([qml.S("b")], [qml.expval(qml.PauliZ(0))]) + batch, _ = validate_device_wires(tape1) + assert batch[0] is tape1 + + def test_fill_in_wires(self): + """Tests that if the wires are provided, measurements without wires take them gain them.""" + tape1 = qml.tape.QuantumScript([qml.S("b")], [qml.state(), qml.probs()], shots=52) + + wires = qml.wires.Wires(["a", "b", "c"]) + batch, _ = validate_device_wires(tape1, wires=wires) + assert batch[0][1].wires == wires + assert batch[0][2].wires == wires + assert batch[0].operations == tape1.operations + assert batch[0].shots == tape1.shots + + +class TestDecomposeValidation: + """Unit tests for helper functions in qml.devices.qubit.preprocess""" + + def test_error_if_invalid_op(self): + """Test that expand_fn throws an error when an operation is does not define a matrix or decomposition.""" + + tape = QuantumScript(ops=[NoMatNoDecompOp(0)], measurements=[qml.expval(qml.Hadamard(0))]) + with pytest.raises(DeviceError, match="not supported on abc"): + decompose(tape, lambda op: op.has_matrix, name="abc") + + def test_decompose(self): + """Test that expand_fn doesn't throw any errors for a valid circuit""" + tape = QuantumScript( + ops=[qml.PauliX(0), qml.RZ(0.123, wires=0)], measurements=[qml.state()] + ) + decompose(tape, lambda obj: obj.has_matrix) + + def test_infinite_decomposition_loop(self): + """Test that a device error is raised if decomposition enters an infinite loop.""" + + class InfiniteOp(qml.operation.Operation): + """An op with an infinite decomposition.""" + + num_wires = 1 + + def decomposition(self): + return [InfiniteOp(*self.parameters, self.wires)] + + qs = qml.tape.QuantumScript([InfiniteOp(1.23, 0)]) + with pytest.raises(DeviceError, match=r"Reached recursion limit trying to decompose"): + decompose(qs, lambda obj: obj.has_matrix) + + +class TestValidateObservables: + """Tests for the validate observables transform.""" + + def test_invalid_observable(self): + """Test that expand_fn throws an error when an observable is invalid.""" + tape = QuantumScript( + ops=[qml.PauliX(0)], measurements=[qml.expval(qml.GellMann(wires=0, index=1))] + ) + with pytest.raises(DeviceError, match=r"not supported on abc"): + validate_observables(tape, lambda obs: obs.name == "PauliX", name="abc") + + def test_invalid_tensor_observable(self): + """Test that expand_fn throws an error when a tensor includes invalid obserables""" + tape = QuantumScript( + ops=[qml.PauliX(0), qml.PauliY(1)], + measurements=[qml.expval(qml.PauliX(0) @ qml.GellMann(wires=1, index=2))], + ) + with pytest.raises(DeviceError, match="not supported on device"): + validate_observables(tape, lambda obj: obj.name == "PauliX") + + def test_valid_tensor_observable(self): + """Test that a valid tensor ovservable passes without error.""" + tape = QuantumScript([], [qml.expval(qml.PauliZ(0) @ qml.PauliY(1))]) + assert ( + validate_observables(tape, lambda obs: obs.name in {"PauliZ", "PauliY"})[0][0] is tape + ) + + +class TestValidateMeasurements: + """Tests for the validate measurements transform.""" + + @pytest.mark.parametrize( + "measurements", + [ + [qml.state()], + [qml.expval(qml.PauliZ(0))], + [qml.state(), qml.expval(qml.PauliZ(0)), qml.probs(0)], + [qml.state(), qml.vn_entropy(0), qml.mutual_info(0, 1)], + ], + ) + def test_only_state_measurements(self, measurements): + """Test that an analytic circuit containing only StateMeasurements works""" + tape = QuantumScript([], measurements, shots=None) + validate_measurements(tape, lambda obj: True) + + @pytest.mark.parametrize( + "measurements", + [ + [qml.sample(wires=0)], + [qml.expval(qml.PauliZ(0))], + [qml.sample(wires=0), qml.expval(qml.PauliZ(0)), qml.probs(0)], + [qml.classical_shadow(wires=[0])], + [qml.shadow_expval(qml.PauliZ(0))], + ], + ) + def test_only_sample_measurements(self, measurements): + """Test that a circuit with finite shots containing only SampleMeasurements works""" + tape = QuantumScript([], measurements, shots=100) + validate_measurements(tape, sample_measurements=lambda obj: True) + + @pytest.mark.parametrize( + "measurements", + [ + [qml.sample(wires=0)], + [qml.state(), qml.sample(wires=0)], + [qml.sample(wires=0), qml.expval(qml.PauliZ(0))], + [qml.classical_shadow(wires=[0])], + [qml.shadow_expval(qml.PauliZ(0))], + ], + ) + def test_analytic_with_samples(self, measurements): + """Test that an analytic circuit containing SampleMeasurements raises an error""" + tape = QuantumScript([], measurements, shots=None) + + msg = "not accepted for analytic simulation on device" + with pytest.raises(DeviceError, match=msg): + validate_measurements(tape) + + @pytest.mark.parametrize( + "measurements", + [ + [qml.state()], + [qml.sample(wires=0), qml.state()], + [qml.expval(qml.PauliZ(0)), qml.state(), qml.sample(wires=0)], + ], + ) + def test_finite_shots_with_state(self, measurements): + """Test that a circuit with finite shots containing StateMeasurements raises an error""" + tape = QuantumScript([], measurements, shots=100) + + msg = "not accepted with finite shots on device" + with pytest.raises(DeviceError, match=msg): + validate_measurements(tape, lambda obj: True) + + +class TestExpandFnTransformations: + """Tests for the behavior of the `expand_fn` helper.""" + + @pytest.mark.parametrize("shots", [None, 100]) + def test_decompose_expand_unsupported_op(self, shots): + """Test that expand_fn expands the tape when unsupported operators are present""" + ops = [qml.Hadamard(0), NoMatOp(1), qml.RZ(0.123, wires=1)] + measurements = [qml.expval(qml.PauliZ(0)), qml.probs()] + tape = QuantumScript(ops=ops, measurements=measurements, shots=shots) + + expanded_tapes, _ = decompose(tape, lambda obj: obj.has_matrix) + expanded_tape = expanded_tapes[0] + expected = [qml.Hadamard(0), qml.PauliX(1), qml.PauliY(1), qml.RZ(0.123, wires=1)] + + for op, exp in zip(expanded_tape.circuit, expected + measurements): + assert qml.equal(op, exp) + + assert tape.shots == expanded_tape.shots + + def test_decompose_no_expansion(self): + """Test that expand_fn does nothing to a fully supported quantum script.""" + ops = [qml.Hadamard(0), qml.CNOT([0, 1]), qml.RZ(0.123, wires=1)] + measurements = [qml.expval(qml.PauliZ(0)), qml.probs()] + tape = QuantumScript(ops=ops, measurements=measurements) + expanded_tapes, _ = decompose(tape, lambda obj: obj.has_matrix) + expanded_tape = expanded_tapes[0] + + for op, exp in zip(expanded_tape.circuit, ops + measurements): + assert qml.equal(op, exp) + + @pytest.mark.parametrize("validation_transform", (validate_measurements, validate_observables)) + def test_valdiate_measurements_non_commuting_measurements(self, validation_transform): + """Test that validate_measurements and validate_observables works when non commuting measurements exist in the circuit.""" + + qs = QuantumScript([NoMatOp("a")], [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(0))]) + + new_qs, _ = validation_transform(qs, lambda obj: True) + new_qs = new_qs[0] + assert new_qs.measurements == qs.measurements + + @pytest.mark.parametrize( + "prep_op", + ( + qml.BasisState([1], wires=0), + qml.StatePrep([0, 1], wires=1), + qml.AmplitudeEmbedding([0, 1], wires=1), + ), + ) + def test_decompose_state_prep_skip_first(self, prep_op): + """Test that the decompose only expands mid-circuit instances of StatePrepBase if requested.""" + ops = [ + prep_op, + qml.Hadamard(wires=0), + qml.StatePrep([0, 1], wires=1), + qml.BasisState([1], wires=0), + qml.RZ(0.123, wires=1), + qml.AmplitudeEmbedding([0, 1, 0, 0], wires=[0, 1]), + ] + measurements = [qml.expval(qml.PauliZ(0)), qml.probs()] + tape = QuantumScript(ops=ops, measurements=measurements) + + expanded_tapes, _ = decompose( + tape, lambda obj: obj.has_matrix, skip_initial_state_prep=True + ) + expanded_tape = expanded_tapes[0] + expected = [ + prep_op, + qml.Hadamard(0), + qml.RY(3.14159265, wires=1), # decomposition of StatePrep + qml.PauliX(wires=0), # decomposition of BasisState + qml.RZ(0.123, wires=1), + qml.RY(1.57079633, wires=[1]), # decomposition of AmplitudeEmbedding + qml.CNOT(wires=[0, 1]), + qml.RY(1.57079633, wires=[1]), + qml.CNOT(wires=[0, 1]), + ] + + assert expanded_tape.circuit == expected + measurements + + @pytest.mark.parametrize( + "prep_op", + ( + qml.BasisState([1], wires=0), + qml.StatePrep([0, 1], wires=1), + qml.AmplitudeEmbedding([0, 1], wires=1), + ), + ) + def test_decompose_initial_state_prep_if_requested(self, prep_op): + """Test that initial state prep operations are decomposed if skip_initial_state_prep is False.""" + + tape = qml.tape.QuantumScript([prep_op]) + batch, _ = decompose(tape, lambda obj: obj.has_matrix, skip_initial_state_prep=False) + new_tape = batch[0] + + assert new_tape[0] != prep_op + + +def test_validate_multiprocessing_workers_None(): + """Test that validation does not fail when max_workers is None""" + qs = QuantumScript( + [qml.Rot(0.1, 0.2, 0.3, wires=0), qml.CNOT([0, 1])], + [qml.expval(qml.PauliZ(1))], + ) + device = qml.devices.DefaultQubit() + validate_multiprocessing_workers(qs, None, device) diff --git a/tests/docs/test_supported_confs.py b/tests/docs/test_supported_confs.py index 84e38b40822..896add72a2a 100644 --- a/tests/docs/test_supported_confs.py +++ b/tests/docs/test_supported_confs.py @@ -295,14 +295,21 @@ def test_none_all(self, diff_method, return_type, shots, wire_specs): msg = None if not shots and return_type is Sample: - msg = "Analytic circuits must only contain StateMeasurements" + msg = "not accepted for analytic simulation" elif diff_method == "adjoint": if shots: - msg = ( - "Circuits with finite shots must be executed with non-analytic gradient methods" - ) + if return_type in { + VnEntropy, + MutualInfo, + "StateCost", + "StateVector", + "DensityMatrix", + }: + msg = "not accepted with finite shots" + else: + msg = "Finite shots are not supported with" elif return_type not in ("Hermitian", "Projector", Expectation): - msg = "Adjoint differentiation method does not support measurement .*" + msg = "not accepted for analytic simulation" elif shots and return_type in ( VnEntropy, MutualInfo, @@ -310,7 +317,7 @@ def test_none_all(self, diff_method, return_type, shots, wire_specs): "StateCost", "StateVector", ): - msg = "Circuits with finite shots must only contain" + msg = "not accepted with finite shots on" if msg is not None: with pytest.raises(qml.DeviceError, match=msg): @@ -360,9 +367,9 @@ def test_all_adjoint_nonexp(self, interface, return_type, shots, wire_specs): """Test diff_method=adjoint raises an error for non-expectation measurements for all interfaces""" msg = ( - "Circuits with finite shots must be executed with non-analytic gradient methods" + "" # two options of message both along the lines of "no shots" if shots - else "Adjoint differentiation method does not support measurement .*" + else "not accepted for analytic simulation on adjoint" ) circuit = get_qnode(interface, "adjoint", return_type, shots, wire_specs) @@ -387,7 +394,7 @@ def test_all_adjoint_exp(self, interface, return_type, shots, wire_specs): x = get_variable(interface, wire_specs) with pytest.raises( qml.DeviceError, - match="Circuits with finite shots must be executed with non-analytic gradient methods", + match="Finite shots are not supported", ): compute_gradient(x, interface, circuit, return_type) @@ -426,9 +433,7 @@ def test_all_paramshift_state(self, interface, return_type, shots, wire_specs): circuit = get_qnode(interface, "parameter-shift", return_type, shots, wire_specs) x = get_variable(interface, wire_specs, complex=complex) if shots is not None: - with pytest.raises( - qml.DeviceError, match="Circuits with finite shots must only contain" - ): + with pytest.raises(qml.DeviceError, match="not accepted with finite shots"): compute_gradient(x, interface, circuit, return_type, complex=complex) else: with pytest.raises(ValueError, match=msg): @@ -450,9 +455,7 @@ def test_all_finitediff_nonstate(self, interface, return_type, shots, wire_specs circuit = get_qnode(interface, diff_method, return_type, shots, wire_specs) x = get_variable(interface, wire_specs) if shots is not None and return_type in (VnEntropy, MutualInfo): - with pytest.raises( - qml.DeviceError, match="Circuits with finite shots must only contain" - ): + with pytest.raises(qml.DeviceError, match="not accepted with finite shots"): compute_gradient(x, interface, circuit, return_type) else: compute_gradient(x, interface, circuit, return_type) @@ -492,7 +495,7 @@ def test_all_sample_none_shots(self, interface, diff_method, wire_specs): """Test sample measurement fails for all interfaces and diff_methods when shots=None""" - with pytest.raises(qml.DeviceError, match="Analytic circuits must only contain"): + with pytest.raises(qml.DeviceError, match="not accepted for analytic simulation on"): circuit = get_qnode(interface, diff_method, Sample, None, wire_specs) x = get_variable(interface, wire_specs) circuit(x) @@ -549,7 +552,7 @@ def test_all_hadamard_nonstate_non_var( if return_type in (VnEntropy, MutualInfo): if shots: err_cls = qml.DeviceError - msg = "Circuits with finite shots must only contain" + msg = "not accepted with finite shots" else: err_cls = ValueError msg = "Computing the gradient of circuits that return the state with the Hadamard test gradient transform is not supported" diff --git a/tests/measurements/test_classical_shadow.py b/tests/measurements/test_classical_shadow.py index 47fb06f2b02..b156c9ea5b6 100644 --- a/tests/measurements/test_classical_shadow.py +++ b/tests/measurements/test_classical_shadow.py @@ -346,7 +346,7 @@ def test_shots_none_error(self, wires, seed): to obtain classical shadows""" circuit = get_circuit(wires, None, seed) - msg = "Analytic circuits must only contain StateMeasurements" + msg = "not accepted for analytic simulation on default.qubit" with pytest.raises(qml.DeviceError, match=msg): circuit() @@ -462,7 +462,7 @@ def test_shots_none_error(self): circuit = hadamard_circuit(2, None) H = qml.PauliZ(0) - msg = "Analytic circuits must only contain StateMeasurements" + msg = "not accepted for analytic simulation on default.qubit" with pytest.raises(qml.DeviceError, match=msg): _ = circuit(H, k=10) diff --git a/tests/measurements/test_measurements.py b/tests/measurements/test_measurements.py index 9540c78efaa..f3ce2de0d98 100644 --- a/tests/measurements/test_measurements.py +++ b/tests/measurements/test_measurements.py @@ -577,7 +577,7 @@ def circuit(): with pytest.raises( qml.DeviceError, - match="Analytic circuits must only contain StateMeasurements; got sample", + match="not accepted for analytic simulation on default.qubit", ): circuit() @@ -617,7 +617,9 @@ def return_type(self): def circuit(): return MyMeasurement() - with pytest.raises(qml.DeviceError, match="Circuits with finite shots must only contain"): + with pytest.raises( + qml.DeviceError, match="not accepted with finite shots on default.qubit" + ): circuit() diff --git a/tests/measurements/test_mutual_info.py b/tests/measurements/test_mutual_info.py index 0424b97fd8a..8726c708dd6 100644 --- a/tests/measurements/test_mutual_info.py +++ b/tests/measurements/test_mutual_info.py @@ -120,7 +120,9 @@ def circuit(x): qml.CRX(x, wires=[0, 1]) return qml.mutual_info(wires0=[0], wires1=[1]) - with pytest.raises(qml.DeviceError, match="Circuits with finite shots must only contain"): + with pytest.raises( + qml.DeviceError, match="not accepted with finite shots on default.qubit" + ): circuit(0.5) diff_methods = ["backprop", "finite-diff"] diff --git a/tests/measurements/test_vn_entropy.py b/tests/measurements/test_vn_entropy.py index b8dc7830ccf..40b87e8ff29 100644 --- a/tests/measurements/test_vn_entropy.py +++ b/tests/measurements/test_vn_entropy.py @@ -147,7 +147,9 @@ def circuit(x): qml.CRX(x, wires=[0, 1]) return qml.vn_entropy(wires=[0]) - with pytest.raises(qml.DeviceError, match="Circuits with finite shots must only contain"): + with pytest.raises( + qml.DeviceError, match="not accepted with finite shots on default.qubit" + ): circuit(0.5) @pytest.mark.parametrize("wires", single_wires_list) diff --git a/tests/ops/qubit/test_hamiltonian.py b/tests/ops/qubit/test_hamiltonian.py index 3a9ede92715..b6cc4292661 100644 --- a/tests/ops/qubit/test_hamiltonian.py +++ b/tests/ops/qubit/test_hamiltonian.py @@ -2070,6 +2070,6 @@ def circuit(coeffs, param): grad_fn = qml.grad(circuit) with pytest.raises( qml.DeviceError, - match="Adjoint differentiation method does not support observable Hamiltonian", + match="not supported on adjoint", ): grad_fn(coeffs, param) diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index 5f6e07d75ae..6612f721f6d 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -1456,14 +1456,14 @@ def test_samples_expval(self): def test_decomposition(self, tol): """Test decomposition onto a device's supported gate set""" dev = qml.device("default.qubit", wires=1) - from pennylane.devices.qubit.preprocess import _accepted_operator + from pennylane.devices.default_qubit import stopping_condition with QuantumTape() as tape: qml.U3(0.1, 0.2, 0.3, wires=[0]) qml.expval(qml.PauliZ(0)) def stop_fn(op): - return isinstance(op, qml.measurements.MeasurementProcess) or _accepted_operator(op) + return isinstance(op, qml.measurements.MeasurementProcess) or stopping_condition(op) tape = tape.expand(stop_at=stop_fn) res = dev.execute(tape) diff --git a/tests/test_qnode.py b/tests/test_qnode.py index 724ca8e1b7a..a528d54f43f 100644 --- a/tests/test_qnode.py +++ b/tests/test_qnode.py @@ -335,7 +335,7 @@ def circ(): with pytest.raises( qml._device.DeviceError, - match="Circuits with finite shots must be executed with non-analytic gradient methods; got adjoint", + match="Finite shots are not supported with adjoint", ): circ() @@ -888,13 +888,13 @@ def conditional_ry_qnode(x, y): qml.cond(m_0, qml.RY)(y, wires=1) return qml.apply(return_type), mv_return(op=m_0) - spy = mocker.spy(qml, "defer_measurements") + spy = mocker.spy(qml.defer_measurements, "_transform") r1 = cry_qnode(first_par, sec_par) r2 = conditional_ry_qnode(first_par, sec_par) assert np.allclose(r1, r2[0]) assert np.allclose(r2[1], mv_res(first_par)) - spy.assert_called_once() + assert spy.call_count == 3 # once for each preprocessing, once for conditional qnode def test_drawing_has_deferred_measurements(self): """Test that `qml.draw` with qnodes uses defer_measurements @@ -937,11 +937,11 @@ def conditional_ry_qnode(x): qml.cond(m_0, qml.RY)(x, wires=1) return qml.sample(qml.PauliZ(1)) - spy = mocker.spy(qml, "defer_measurements") + spy = mocker.spy(qml.defer_measurements, "_transform") r1 = cry_qnode(first_par) r2 = conditional_ry_qnode(first_par) assert np.allclose(r1, r2) - spy.assert_called_once() + assert spy.call_count == 3 # once per device preprocessing, once for conditional qnode @pytest.mark.tf @pytest.mark.parametrize("interface", ["tf", "auto"]) @@ -1604,9 +1604,7 @@ def test_shots_integration(self): def circuit(): return qml.sample(wires=(0, 1)) - with pytest.raises( - qml.DeviceError, match="Analytic circuits must only contain StateMeasurements" - ): + with pytest.raises(qml.DeviceError, match="not accepted for analytic simulation"): circuit() results = circuit(shots=10) # pylint: disable=unexpected-keyword-arg diff --git a/tests/test_qnode_legacy.py b/tests/test_qnode_legacy.py index ad9ce49605b..e2c7cc7bbaa 100644 --- a/tests/test_qnode_legacy.py +++ b/tests/test_qnode_legacy.py @@ -1135,13 +1135,13 @@ def conditional_ry_qnode(x, y): qml.cond(m_0, qml.RY)(y, wires=1) return qml.apply(return_type), mv_return(op=m_0) - spy = mocker.spy(qml, "defer_measurements") + spy = mocker.spy(qml.defer_measurements, "_transform") r1 = cry_qnode(first_par, sec_par) r2 = conditional_ry_qnode(first_par, sec_par) assert np.allclose(r1, r2[0]) assert np.allclose(r2[1], mv_res(first_par)) - spy.assert_called_once() + assert spy.call_count == 3 if dev.name == "defaut.qubit" else 1 def test_drawing_has_deferred_measurements(self): """Test that `qml.draw` with qnodes uses defer_measurements diff --git a/tests/test_return_types_dq2.py b/tests/test_return_types_dq2.py index 58494b14073..fda205d4f15 100644 --- a/tests/test_return_types_dq2.py +++ b/tests/test_return_types_dq2.py @@ -1225,7 +1225,9 @@ def return_type(self): tape = qml.tape.QuantumScript.from_queue(q) dev = qml.device("default.qubit", wires=3) - with pytest.raises(qml.DeviceError, match="Analytic circuits must only contain"): + with pytest.raises( + qml.DeviceError, match="not accepted for analytic simulation on default.qubit" + ): program, _ = dev.preprocess() qml.execute(tapes=[tape], device=dev, gradient_fn=None, transform_program=program) diff --git a/tests/transforms/test_defer_measurements.py b/tests/transforms/test_defer_measurements.py index 6d63e807a54..9c24826147b 100644 --- a/tests/transforms/test_defer_measurements.py +++ b/tests/transforms/test_defer_measurements.py @@ -95,11 +95,11 @@ def qnode2(phi): qml.RX(phi, 0) return qml.expval(qml.PauliZ(0)) - spy = mocker.spy(qml, "defer_measurements") + spy = mocker.spy(qml.defer_measurements, "_transform") # Outputs should match assert np.isclose(qnode1(np.pi / 4), qnode2(np.pi / 4)) - spy.assert_called_once() + assert spy.call_count == 3 # once per device preprocessing, one for qnode deferred_tapes, _ = qml.defer_measurements(qnode1.qtape) deferred_tape = deferred_tapes[0] @@ -113,7 +113,7 @@ def test_new_wires_after_reuse(self, mocker): """Test that a new wire is added for every measurement after which the wire is reused.""" dev = qml.device("default.qubit", wires=4) - spy = mocker.spy(qml, "defer_measurements") + spy = mocker.spy(qml.defer_measurements, "_transform") @qml.qnode(dev) def qnode1(phi, theta): @@ -139,7 +139,7 @@ def qnode2(phi, theta): res2 = qnode2(np.pi / 4, 3 * np.pi / 4) - assert spy.call_count == 2 + assert spy.call_count == 4 deferred_tapes1, _ = qml.defer_measurements(qnode1.qtape) deferred_tape1 = deferred_tapes1[0] @@ -314,6 +314,7 @@ def test_cv_op_error(self): @qml.qnode(dev) @qml.defer_measurements def qnode(): + qml.measure(0) qml.Rotation(0.123, wires=[0]) return qml.expval(qml.NumberOperator(1)) @@ -329,6 +330,7 @@ def test_cv_obs_error(self): @qml.qnode(dev) @qml.defer_measurements def qnode(): + qml.measure(0) return qml.expval(qml.NumberOperator(1)) with pytest.raises( @@ -1200,9 +1202,9 @@ def qnode(p, x, y): qml.cond(m1 | m2, qml.RY)(y, 2) return qml.expval(qml.PauliZ(2)) - spy = mocker.spy(qml, "defer_measurements") + spy = mocker.spy(qml.defer_measurements, "_transform") _ = qnode(0.123, 0.456, 0.789) - spy.assert_called_once() + assert spy.call_count == 2 expected_circuit = [ qml.Hadamard(0), diff --git a/tests/transforms/test_experimental/test_transform_dispatcher.py b/tests/transforms/test_experimental/test_transform_dispatcher.py index 9149a8f8a84..5dbb1d6bc5f 100644 --- a/tests/transforms/test_experimental/test_transform_dispatcher.py +++ b/tests/transforms/test_experimental/test_transform_dispatcher.py @@ -546,8 +546,8 @@ def test_device_transform(self, valid_transform): assert isinstance(program, qml.transforms.core.TransformProgram) assert isinstance(new_program, qml.transforms.core.TransformProgram) - assert len(program) == 4 - assert len(new_program) == 5 + assert len(program) == 5 + assert len(new_program) == 6 assert new_program[-1].transform is valid_transform From b45f808071a2ba20b5f6f459ab2b9241ae9d9cb0 Mon Sep 17 00:00:00 2001 From: Jack Brown Date: Mon, 16 Oct 2023 15:43:13 -0400 Subject: [PATCH 8/8] Fix error in 'qml.data.load()` when using 'full' parameter value (#4663) --- doc/releases/changelog-dev.md | 3 ++ pennylane/data/data_manager/foldermap.py | 28 +++++++--- tests/data/data_manager/test_foldermap.py | 62 +++++++++++++++++++++-- 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 1ac65dd0770..b904b6e4aa8 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -210,6 +210,9 @@ still a `_qfunc_output` property on `QNode` instances. [(#4651)](https://github.com/PennyLaneAI/pennylane/pull/4651) +* `qml.data.load` properly handles parameters that come after `'full'` + [(#4663)](https://github.com/PennyLaneAI/pennylane/pull/4663) + * The `qml.jordan_wigner` function has been modified to optionally remove the imaginary components of the computed qubit operator, if imaginary components are smaller than a threshold. [(#4639)](https://github.com/PennyLaneAI/pennylane/pull/4639) diff --git a/pennylane/data/data_manager/foldermap.py b/pennylane/data/data_manager/foldermap.py index 9080535e855..bf4c0cde4e0 100644 --- a/pennylane/data/data_manager/foldermap.py +++ b/pennylane/data/data_manager/foldermap.py @@ -119,6 +119,7 @@ def find( while curr: curr_description, curr_level = curr.pop() + if param_arg == ParamArg.FULL: next_params = curr_level elif param_arg == ParamArg.DEFAULT: @@ -131,18 +132,29 @@ def find( else: next_params = param_arg - try: - todo.extend( + for next_param in next_params: + try: + fmap_next = curr_level[next_param] + except KeyError: + continue + + todo.append( ( Description((*curr_description.items(), (param_name, next_param))), - curr_level[next_param], + fmap_next, ) - for next_param in next_params ) - except KeyError as exc: - raise ValueError( - f"{param_name} '{exc.args[0]}' is not available. Available values are: {list(curr_level)}" - ) from exc + + if len(todo) == 0: + # None of the parameters matched + param_arg_repr = ( + repr([param_arg]) + if isinstance(param_arg, (str, ParamArg)) + else repr(list(param_arg)) + ) + raise ValueError( + f"{param_name} value(s) {param_arg_repr} are not available. Available values are: {list(curr_level)}" + ) curr, todo = todo, curr diff --git a/tests/data/data_manager/test_foldermap.py b/tests/data/data_manager/test_foldermap.py index 2bfa56e6073..8b5b21c8818 100644 --- a/tests/data/data_manager/test_foldermap.py +++ b/tests/data/data_manager/test_foldermap.py @@ -15,6 +15,8 @@ Tests for the :class:`pennylane.data.data_manger.FolderMapView` class. """ +import re + import pytest from pennylane.data.data_manager import DEFAULT, FULL, DataPath @@ -125,6 +127,38 @@ class TestFolderMapView: ), ], ), + ( + {"missing_default": DEFAULT, "molname": "O2", "basis": FULL, "bondlength": ["0.6"]}, + [ + ( + {"molname": "O2", "basis": "STO-3G", "bondlength": "0.6"}, + "qchem/O2/STO-3G/0.6.h5", + ), + ], + ), + ( + { + "missing_default": DEFAULT, + "molname": "O2", + "basis": FULL, + "bondlength": ["0.6", "200"], + }, + [ + ( + {"molname": "O2", "basis": "STO-3G", "bondlength": "0.6"}, + "qchem/O2/STO-3G/0.6.h5", + ), + ], + ), + ( + {"missing_default": FULL, "molname": "O2", "bondlength": ["0.6"]}, + [ + ( + {"molname": "O2", "basis": "STO-3G", "bondlength": "0.6"}, + "qchem/O2/STO-3G/0.6.h5", + ), + ], + ), ], ) def test_find(self, foldermap, kwds, expect): # pylint: disable=redefined-outer-name @@ -155,14 +189,36 @@ def test_find_missing_arg_no_default(self): with pytest.raises(ValueError, match="No default available for parameter 'molname'"): FolderMapView(FOLDERMAP).find("qchem") - def test_find_invalid_parameter(self): + @pytest.mark.parametrize( + "arg, error_fmt", [("Z3", repr(["Z3"])), (("Z3", "Z4"), repr(["Z3", "Z4"]))] + ) + def test_find_invalid_parameter(self, arg, error_fmt): """Test that a ValueError is raised when a parameter provided does not exist.""" with pytest.raises( - ValueError, match=r"molname 'Z3' is not available. Available values are: \['O2', 'H2'\]" + ValueError, + match=re.escape( + f"molname value(s) {error_fmt} are not available. Available values are: ['O2', 'H2']" + ), ): - FolderMapView(FOLDERMAP).find("qchem", molname="Z3") + FolderMapView(FOLDERMAP).find("qchem", molname=arg) + + @pytest.mark.parametrize("basis", [FULL, DEFAULT]) + def test_find_invalid_parameters_after_full_default(self, basis): + """Test that a ValueError is raised when a parameter provided + does not exist, after a 'full' or 'default' parameter has been provided for a + higher-priority parameter.""" + + with pytest.raises( + ValueError, + match=( + r"bondlength value\(s\) \['0.20', '200'\] are not available. Available values are: \['0.5', '0.6'\]" + ), + ): + FolderMapView(FOLDERMAP).find( + "qchem", molname="O2", basis=basis, bondlength=["0.20", "200"] + ) @pytest.mark.parametrize( "init, key, expect",