diff --git a/doc/code/qml_qinfo.rst b/doc/code/qml_qinfo.rst index 80501373d86..04563167e2c 100644 --- a/doc/code/qml_qinfo.rst +++ b/doc/code/qml_qinfo.rst @@ -18,3 +18,5 @@ Transforms :skip: metric_tensor :skip: adjoint_metric_tensor :skip: transform + :skip: classical_fisher + :skip: quantum_fisher diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst index 3d2681d43e2..df57bdb6981 100644 --- a/doc/development/deprecations.rst +++ b/doc/development/deprecations.rst @@ -9,6 +9,12 @@ deprecations are listed below. Pending deprecations -------------------- +* The functions ``qml.qinfo.classical_fisher`` and ``qml.qinfo.quantum_fisher`` are deprecated since they are being migrated + to the ``qml.gradients`` module. Therefore, ``qml.gradients.classical_fisher`` and ``qml.gradients.quantum_fisher`` should be used instead. + + - Deprecated and Duplicated in v0.38 + - Will be removed in v0.39 + * The ``simplify`` argument in ``qml.Hamiltonian`` and ``qml.ops.LinearCombination`` is deprecated. Instead, ``qml.simplify()`` can be called on the constructed operator. diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 5f4aea48141..6543ac9c4b2 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -49,6 +49,10 @@

Deprecations 👋

+* `pennylane.qinfo.classical_fisher` and `pennylane.qinfo.quantum_fisher` have been deprecated. + Instead, use `pennylane.gradients.classical_fisher` and `pennylane.gradients.quantum_fisher`. + [(#5985)](https://github.com/PennyLaneAI/pennylane/pull/5985) +

Documentation 📝

* Improves the docstring for `QuantumScript.expand` and `qml.tape.tape.expand_tape`. diff --git a/pennylane/gradients/__init__.py b/pennylane/gradients/__init__.py index 00f753f5b03..9027c869393 100644 --- a/pennylane/gradients/__init__.py +++ b/pennylane/gradients/__init__.py @@ -85,6 +85,8 @@ batch_jvp jvp classical_jacobian + classical_fisher + quantum_fisher Registering autodifferentiation gradients @@ -341,6 +343,7 @@ def my_custom_gradient(tape: qml.tape.QuantumTape, **kwargs) -> (Sequence[qml.ta from .adjoint_metric_tensor import adjoint_metric_tensor from .classical_jacobian import classical_jacobian from .finite_difference import finite_diff, finite_diff_coeffs +from .fisher import classical_fisher, quantum_fisher from .general_shift_rules import ( eigvals_to_frequencies, generate_multi_shift_rule, diff --git a/pennylane/gradients/fisher.py b/pennylane/gradients/fisher.py new file mode 100644 index 00000000000..3944ba5de97 --- /dev/null +++ b/pennylane/gradients/fisher.py @@ -0,0 +1,393 @@ +# Copyright 2018-2024 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. +"""Contains functions for computing classical and quantum fisher information matrices.""" +# pylint: disable=import-outside-toplevel, not-callable +from collections.abc import Callable, Sequence +from functools import partial + +import pennylane as qml +from pennylane import transform +from pennylane.devices import DefaultQubit, DefaultQubitLegacy +from pennylane.gradients import adjoint_metric_tensor + + +# TODO: create qml.math.jacobian and replace it here +def _torch_jac(circ): + """Torch jacobian as a callable function""" + import torch + + def wrapper(*args, **kwargs): + loss = partial(circ, **kwargs) + if len(args) > 1: + return torch.autograd.functional.jacobian(loss, args, create_graph=True) + return torch.autograd.functional.jacobian(loss, *args, create_graph=True) + + return wrapper + + +# TODO: create qml.math.jacobian and replace it here +def _tf_jac(circ): + """TF jacobian as a callable function""" + import tensorflow as tf + + def wrapper(*args, **kwargs): + with tf.GradientTape() as tape: + loss = circ(*args, **kwargs) + return tape.jacobian(loss, args) + + return wrapper + + +def _compute_cfim(p, dp): + r"""Computes the (num_params, num_params) classical fisher information matrix from the probabilities and its derivatives + I.e. it computes :math:`classical_fisher_{ij} = \sum_\ell (\partial_i p_\ell) (\partial_i p_\ell) / p_\ell` + """ + # Exclude values where p=0 and calculate 1/p + nonzeros_p = qml.math.where(p > 0, p, qml.math.ones_like(p)) + one_over_p = qml.math.where(p > 0, qml.math.ones_like(p), qml.math.zeros_like(p)) + one_over_p = one_over_p / nonzeros_p + + # Multiply dp and p + # Note that casting and being careful about dtypes is necessary as interfaces + # typically treat derivatives (dp) with float32, while standard execution (p) comes in float64 + dp = qml.math.cast_like(dp, p) + dp = qml.math.reshape( + dp, (len(p), -1) + ) # Squeeze does not work, as you could have shape (num_probs, num_params) with num_params = 1 + dp_over_p = qml.math.transpose(dp) * one_over_p # creates (n_params, n_probs) array + + # (n_params, n_probs) @ (n_probs, n_params) = (n_params, n_params) + return dp_over_p @ dp + + +@transform +def _make_probs(tape: qml.tape.QuantumTape) -> tuple[Sequence[qml.tape.QuantumTape], Callable]: + """Ignores the return types of the provided circuit and creates a new one + that outputs probabilities""" + qscript = qml.tape.QuantumScript(tape.operations, [qml.probs(tape.wires)], shots=tape.shots) + + def post_processing_fn(res): + # only a single probs measurement, so no stacking needed + return res[0] + + return [qscript], post_processing_fn + + +def classical_fisher(qnode, argnums=0): + r"""Returns a function that computes the classical fisher information matrix (CFIM) of a given :class:`.QNode` or + quantum tape. + + Given a parametrized (classical) probability distribution :math:`p(\bm{\theta})`, the classical fisher information + matrix quantifies how changes to the parameters :math:`\bm{\theta}` are reflected in the probability distribution. + For a parametrized quantum state, we apply the concept of classical fisher information to the computational + basis measurement. + More explicitly, this function implements eq. (15) in `arxiv:2103.15191 `_: + + .. math:: + + \text{CFIM}_{i, j} = \sum_{\ell=0}^{2^N-1} \frac{1}{p_\ell(\bm{\theta})} \frac{\partial p_\ell(\bm{\theta})}{ + \partial \theta_i} \frac{\partial p_\ell(\bm{\theta})}{\partial \theta_j} + + for :math:`N` qubits. + + Args: + tape (:class:`.QNode` or qml.QuantumTape): A :class:`.QNode` or quantum tape that may have arbitrary return types. + argnums (Optional[int or List[int]]): Arguments to be differentiated in case interface ``jax`` is used. + + Returns: + func: The function that computes the classical fisher information matrix. This function accepts the same + signature as the :class:`.QNode`. If the signature contains one differentiable variable ``params``, the function + returns a matrix of size ``(len(params), len(params))``. For multiple differentiable arguments ``x, y, z``, + it returns a list of sizes ``[(len(x), len(x)), (len(y), len(y)), (len(z), len(z))]``. + + + .. seealso:: :func:`~.pennylane.metric_tensor`, :func:`~.pennylane.gradient.transforms.quantum_fisher` + + **Example** + + First, let us define a parametrized quantum state and return its (classical) probability distribution for all + computational basis elements: + + .. code-block:: python + + import pennylane.numpy as pnp + + dev = qml.device("default.qubit") + + @qml.qnode(dev) + def circ(params): + qml.RX(params[0], wires=0) + qml.CNOT([0, 1]) + qml.CRY(params[1], wires=[1, 0]) + qml.Hadamard(1) + return qml.probs(wires=[0, 1]) + + Executing this circuit yields the ``2**2=4`` elements of :math:`p_\ell(\bm{\theta})` + + >>> pnp.random.seed(25) + >>> params = pnp.random.random(2) + >>> circ(params) + [0.41850088 0.41850088 0.08149912 0.08149912] + + We can obtain its ``(2, 2)`` classical fisher information matrix (CFIM) by simply calling the function returned + by ``classical_fisher()``: + + >>> cfim_func = qml.gradient.classical_fisher(circ) + >>> cfim_func(params) + [[ 0.901561 -0.125558] + [-0.125558 0.017486]] + + This function has the same signature as the :class:`.QNode`. Here is a small example with multiple arguments: + + .. code-block:: python + + @qml.qnode(dev) + def circ(x, y): + qml.RX(x, wires=0) + qml.RY(y, wires=0) + return qml.probs(wires=range(n_wires)) + + >>> x, y = pnp.array([0.5, 0.6], requires_grad=True) + >>> circ(x, y) + [0.86215007 0. 0.13784993 0. ] + >>> qml.gradient.classical_fisher(circ)(x, y) + [array([[0.32934729]]), array([[0.51650396]])] + + Note how in the case of multiple variables we get a list of matrices with sizes + ``[(n_params0, n_params0), (n_params1, n_params1)]``, which in this case is simply two ``(1, 1)`` matrices. + + + A typical setting where the classical fisher information matrix is used is in variational quantum algorithms. + Closely related to the `quantum natural gradient `_, which employs the + `quantum` fisher information matrix, we can compute a rescaled gradient using the CFIM. In this scenario, + typically a Hamiltonian objective function :math:`\langle H \rangle` is minimized: + + .. code-block:: python + + H = qml.Hamiltonian(coeffs=[0.5, 0.5], observables=[qml.Z(0), qml.Z(1)]) + + @qml.qnode(dev) + def circ(params): + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=0) + qml.RX(params[2], wires=1) + qml.RY(params[3], wires=1) + qml.CNOT(wires=(0,1)) + return qml.expval(H) + + params = pnp.random.random(4) + + We can compute both the gradient of :math:`\langle H \rangle` and the CFIM with the same :class:`.QNode` ``circ`` + in this example since ``classical_fisher()`` ignores the return types and assumes ``qml.probs()`` for all wires. + + >>> grad = qml.grad(circ)(params) + >>> cfim = qml.gradient.classical_fisher(circ)(params) + >>> print(grad.shape, cfim.shape) + (4,) (4, 4) + + Combined together, we can get a rescaled gradient to be employed for optimization schemes like natural gradient + descent. + + >>> rescaled_grad = cfim @ grad + >>> print(rescaled_grad) + [-0.66772533 -0.16618756 -0.05865127 -0.06696078] + + The ``classical_fisher`` matrix itself is again differentiable: + + .. code-block:: python + + @qml.qnode(dev) + def circ(params): + qml.RX(qml.math.cos(params[0]), wires=0) + qml.RX(qml.math.cos(params[0]), wires=1) + qml.RX(qml.math.cos(params[1]), wires=0) + qml.RX(qml.math.cos(params[1]), wires=1) + return qml.probs(wires=range(2)) + + params = pnp.random.random(2) + + >>> qml.gradient.classical_fisher(circ)(params) + [[4.18575068e-06 2.34443943e-03] + [2.34443943e-03 1.31312079e+00]] + >>> qml.jacobian(qml.gradient.classical_fisher(circ))(params) + array([[[9.98030491e-01, 3.46944695e-18], + [1.36541817e-01, 5.15248592e-01]], + [[1.36541817e-01, 5.15248592e-01], + [2.16840434e-18, 2.81967252e-01]]])) + + """ + new_qnode = _make_probs(qnode) + + def wrapper(*args, **kwargs): + old_interface = qnode.interface + + if old_interface == "auto": + qnode.interface = qml.math.get_interface(*args, *list(kwargs.values())) + + interface = qnode.interface + + if interface in ("jax", "jax-jit"): + import jax + + jac = jax.jacobian(new_qnode, argnums=argnums) + + if interface == "torch": + jac = _torch_jac(new_qnode) + + if interface == "autograd": + jac = qml.jacobian(new_qnode) + + if interface == "tf": + jac = _tf_jac(new_qnode) + + j = jac(*args, **kwargs) + p = new_qnode(*args, **kwargs) + + if old_interface == "auto": + qnode.interface = "auto" + + # In case multiple variables are used, we create a list of cfi matrices + if isinstance(j, tuple): + res = [] + for j_i in j: + res.append(_compute_cfim(p, j_i)) + + if len(j) == 1: + return res[0] + + return res + + return _compute_cfim(p, j) + + return wrapper + + +@partial(transform, is_informative=True) +def quantum_fisher( + tape: qml.tape.QuantumTape, device, *args, **kwargs +) -> tuple[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}` + are reflected in the quantum state. The metric used to induce the QFIM is the fidelity :math:`f = |\langle \psi | \psi' \rangle|^2` between two (pure) quantum states. + This leads to the following definition of the QFIM (see eq. (27) in `arxiv:2103.15191 `_): + + .. math:: + + \text{QFIM}_{i, j} = 4 \text{Re}\left[ \langle \partial_i \psi(\bm{\theta}) | \partial_j \psi(\bm{\theta}) \rangle + - \langle \partial_i \psi(\bm{\theta}) | \psi(\bm{\theta}) \rangle \langle \psi(\bm{\theta}) | \partial_j \psi(\bm{\theta}) \rangle \right] + + with short notation :math:`| \partial_j \psi(\bm{\theta}) \rangle := \frac{\partial}{\partial \theta_j}| \psi(\bm{\theta}) \rangle`. + + .. seealso:: + :func:`~.pennylane.metric_tensor`, :func:`~.pennylane.adjoint_metric_tensor`, :func:`~.pennylane.gradient.transforms.classical_fisher` + + Args: + tape (QNode or QuantumTape or Callable): A quantum circuit that may have arbitrary return types. + *args: In case finite shots are used, further arguments according to :func:`~.pennylane.metric_tensor` may be passed. + + Returns: + qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: + + The transformed circuit as described in :func:`qml.transform `. Executing this circuit + will provide the quantum Fisher information in the form of a tensor. + + .. note:: + + ``quantum_fisher`` coincides with the ``metric_tensor`` with a prefactor of :math:`4`. Internally, :func:`~.pennylane.adjoint_metric_tensor` is used when executing on a device with + exact expectations (``shots=None``) that inherits from ``"default.qubit"``. In all other cases, i.e. if a device with finite shots is used, the hardware compatible transform :func:`~.pennylane.metric_tensor` is used. + Please refer to their respective documentations for details on the arguments. + + **Example** + + The quantum Fisher information matrix (QIFM) can be used to compute the `natural` gradient for `Quantum Natural Gradient Descent `_. + A typical scenario is optimizing the expectation value of a Hamiltonian: + + .. code-block:: python + + n_wires = 2 + + dev = qml.device("default.qubit", wires=n_wires) + + H = 1.*qml.X(0) @ qml.X(1) - 0.5 * qml.Z(1) + + @qml.qnode(dev) + def circ(params): + qml.RY(params[0], wires=1) + qml.CNOT(wires=(1,0)) + qml.RY(params[1], wires=1) + qml.RZ(params[2], wires=1) + return qml.expval(H) + + params = pnp.array([0.5, 1., 0.2], requires_grad=True) + + The natural gradient is then simply the QFIM multiplied by the gradient: + + >>> grad = qml.grad(circ)(params) + >>> grad + [ 0.59422561 -0.02615095 -0.05146226] + >>> qfim = qml.gradient.quantum_fisher(circ)(params) + >>> qfim + [[1. 0. 0. ] + [0. 1. 0. ] + [0. 0. 0.77517241]] + >>> qfim @ grad + tensor([ 0.59422561, -0.02615095, -0.03989212], requires_grad=True) + + When using real hardware or finite shots, ``quantum_fisher`` is internally calling :func:`~.pennylane.metric_tensor`. + To obtain the full QFIM, we need an auxilary wire to perform the Hadamard test. + + >>> dev = qml.device("default.qubit", wires=n_wires+1, shots=1000) + >>> @qml.qnode(dev) + ... def circ(params): + ... qml.RY(params[0], wires=1) + ... qml.CNOT(wires=(1,0)) + ... qml.RY(params[1], wires=1) + ... qml.RZ(params[2], wires=1) + ... return qml.expval(H) + >>> qfim = qml.gradient.quantum_fisher(circ)(params) + + Alternatively, we can fall back on the block-diagonal QFIM without the additional wire. + + >>> qfim = qml.gradient.quantum_fisher(circ, approx="block-diag")(params) + + """ + + if device.shots or not isinstance(device, (DefaultQubitLegacy, DefaultQubit)): + tapes, processing_fn = qml.gradients.metric_tensor(tape, *args, **kwargs) + + def processing_fn_multiply(res): + res = qml.execute(res, device=device) + return 4 * processing_fn(res) + + return tapes, processing_fn_multiply + + res = adjoint_metric_tensor(tape, *args, **kwargs) + + 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) diff --git a/pennylane/qinfo/transforms.py b/pennylane/qinfo/transforms.py index f6260a5a51b..6edaf8c3c04 100644 --- a/pennylane/qinfo/transforms.py +++ b/pennylane/qinfo/transforms.py @@ -13,6 +13,7 @@ # limitations under the License. """QNode transforms for the quantum information quantities.""" # pylint: disable=import-outside-toplevel, not-callable +import warnings from functools import partial from typing import Callable, Sequence @@ -458,68 +459,6 @@ def vn_entanglement_entropy( ) -# TODO: create qml.math.jacobian and replace it here -def _torch_jac(circ): - """Torch jacobian as a callable function""" - import torch - - def wrapper(*args, **kwargs): - loss = partial(circ, **kwargs) - if len(args) > 1: - return torch.autograd.functional.jacobian(loss, args, create_graph=True) - return torch.autograd.functional.jacobian(loss, *args, create_graph=True) - - return wrapper - - -# TODO: create qml.math.jacobian and replace it here -def _tf_jac(circ): - """TF jacobian as a callable function""" - import tensorflow as tf - - def wrapper(*args, **kwargs): - with tf.GradientTape() as tape: - loss = circ(*args, **kwargs) - return tape.jacobian(loss, args) - - return wrapper - - -def _compute_cfim(p, dp): - r"""Computes the (num_params, num_params) classical fisher information matrix from the probabilities and its derivatives - I.e. it computes :math:`classical_fisher_{ij} = \sum_\ell (\partial_i p_\ell) (\partial_i p_\ell) / p_\ell` - """ - # Exclude values where p=0 and calculate 1/p - nonzeros_p = qml.math.where(p > 0, p, qml.math.ones_like(p)) - one_over_p = qml.math.where(p > 0, qml.math.ones_like(p), qml.math.zeros_like(p)) - one_over_p = one_over_p / nonzeros_p - - # Multiply dp and p - # Note that casting and being careful about dtypes is necessary as interfaces - # typically treat derivatives (dp) with float32, while standard execution (p) comes in float64 - dp = qml.math.cast_like(dp, p) - dp = qml.math.reshape( - dp, (len(p), -1) - ) # Squeeze does not work, as you could have shape (num_probs, num_params) with num_params = 1 - dp_over_p = qml.math.transpose(dp) * one_over_p # creates (n_params, n_probs) array - - # (n_params, n_probs) @ (n_probs, n_params) = (n_params, n_params) - return dp_over_p @ dp - - -@transform -def _make_probs(tape: qml.tape.QuantumTape) -> (Sequence[qml.tape.QuantumTape], Callable): - """Ignores the return types of the provided circuit and creates a new one - that outputs probabilities""" - qscript = qml.tape.QuantumScript(tape.operations, [qml.probs(tape.wires)], shots=tape.shots) - - def post_processing_fn(res): - # only a single probs measurement, so no stacking needed - return res[0] - - return [qscript], post_processing_fn - - def classical_fisher(qnode, argnums=0): r"""Returns a function that computes the classical fisher information matrix (CFIM) of a given :class:`.QNode` or quantum tape. @@ -547,6 +486,9 @@ def classical_fisher(qnode, argnums=0): returns a matrix of size ``(len(params), len(params))``. For multiple differentiable arguments ``x, y, z``, it returns a list of sizes ``[(len(x), len(x)), (len(y), len(y)), (len(z), len(z))]``. + .. warning:: + ``pennylane.qinfo.classical_fisher`` is being migrated to a different module and will + removed in version 0.39. Instead, use :func:`pennylane.gradients.classical_fisher`. .. seealso:: :func:`~.pennylane.metric_tensor`, :func:`~.pennylane.qinfo.transforms.quantum_fisher` @@ -663,50 +605,13 @@ def circ(params): [2.16840434e-18, 2.81967252e-01]]])) """ - new_qnode = _make_probs(qnode) - - def wrapper(*args, **kwargs): - old_interface = qnode.interface - - if old_interface == "auto": - qnode.interface = qml.math.get_interface(*args, *list(kwargs.values())) - - interface = qnode.interface - - if interface in ("jax", "jax-jit"): - import jax - - jac = jax.jacobian(new_qnode, argnums=argnums) - - if interface == "torch": - jac = _torch_jac(new_qnode) - - if interface == "autograd": - jac = qml.jacobian(new_qnode) - - if interface == "tf": - jac = _tf_jac(new_qnode) - - j = jac(*args, **kwargs) - p = new_qnode(*args, **kwargs) - - if old_interface == "auto": - qnode.interface = "auto" - - # In case multiple variables are used, we create a list of cfi matrices - if isinstance(j, tuple): - res = [] - for j_i in j: - res.append(_compute_cfim(p, j_i)) - - if len(j) == 1: - return res[0] - - return res - - return _compute_cfim(p, j) + warnings.warn( + "pennylane.qinfo.classical_fisher is being migrated to a different module and will " + "removed in version 0.39. Instead, use pennylane.gradients.classical_fisher.", + qml.PennyLaneDeprecationWarning, + ) - return wrapper + return qml.gradients.classical_fisher(qnode, argnums=argnums) @partial(transform, is_informative=True) @@ -739,6 +644,10 @@ def quantum_fisher( The transformed circuit as described in :func:`qml.transform `. Executing this circuit will provide the quantum Fisher information in the form of a tensor. + .. warning:: + ``pennylane.qinfo.quantum_fisher`` is being migrated to a different module and will + removed in version 0.39. Instead, use :func:`pennylane.gradients.quantum_fisher`. + .. note:: ``quantum_fisher`` coincides with the ``metric_tensor`` with a prefactor of :math:`4`. Internally, :func:`~.pennylane.adjoint_metric_tensor` is used when executing on a device with @@ -799,6 +708,11 @@ def circ(params): >>> qfim = qml.qinfo.quantum_fisher(circ, approx="block-diag")(params) """ + warnings.warn( + "pennylane.qinfo.quantum_fisher is being migrated to a different module and will " + "removed in version 0.39. Instead, use pennylane.gradients.quantum_fisher.", + qml.PennyLaneDeprecationWarning, + ) if device.shots or not isinstance(device, (DefaultQubitLegacy, DefaultQubit)): tapes, processing_fn = metric_tensor(tape, *args, **kwargs) diff --git a/tests/qinfo/test_fisher.py b/tests/gradients/core/test_fisher.py similarity index 93% rename from tests/qinfo/test_fisher.py rename to tests/gradients/core/test_fisher.py index 7db6526cde4..a3dba786083 100644 --- a/tests/qinfo/test_fisher.py +++ b/tests/gradients/core/test_fisher.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Tests for the classical fisher information matrix in the pennylane.qinfo +Tests for the classical and quantum fisher information matrix in the pennylane.qinfo """ import numpy as np @@ -21,8 +21,7 @@ import pennylane as qml import pennylane.numpy as pnp -from pennylane.qinfo import classical_fisher, quantum_fisher -from pennylane.qinfo.transforms import _compute_cfim, _make_probs +from pennylane.gradients.fisher import _compute_cfim, _make_probs, classical_fisher, quantum_fisher class TestMakeProbs: @@ -60,7 +59,7 @@ def test_make_probs(self, shots): assert new_tape[0].shots == tape.shots -class TestComputeclassicalFisher: +class TestComputeClassicalFisher: """Testing that given p and dp, _compute_cfim() computes the correct outputs""" @pytest.mark.parametrize("n_params", np.arange(1, 10)) @@ -141,7 +140,7 @@ def circ(params): return qml.probs(wires=range(n_wires)) params = pnp.zeros(n_params, requires_grad=True) - res = qml.qinfo.classical_fisher(circ)(params) + res = classical_fisher(circ)(params) assert np.allclose(res, n_wires * np.ones((n_params, n_params)), atol=1) @pytest.mark.parametrize( @@ -282,7 +281,7 @@ def circ(x, y, z): x = jnp.pi / 8 * jnp.ones(2) y = jnp.pi / 8 * jnp.ones(10) z = jnp.ones(1) - cfim = qml.qinfo.classical_fisher(circ, argnums=(0, 1, 2))(x, y, z) + cfim = classical_fisher(circ, argnums=(0, 1, 2))(x, y, z) assert qml.math.allclose(cfim[0], 2.0 / 3.0 * np.ones((2, 2))) assert qml.math.allclose(cfim[1], 2.0 / 3.0 * np.ones((10, 10))) assert qml.math.allclose(cfim[2], np.zeros((1, 1))) @@ -351,7 +350,7 @@ def circ(x, y, z): x = np.pi / 8 * torch.ones(2, requires_grad=True) y = np.pi / 8 * torch.ones(10, requires_grad=True) z = torch.ones(1, requires_grad=True) - cfim = qml.qinfo.classical_fisher(circ)(x, y, z) + cfim = classical_fisher(circ)(x, y, z) assert np.allclose(cfim[0].detach().numpy(), 2.0 / 3.0 * np.ones((2, 2))) assert np.allclose(cfim[1].detach().numpy(), 2.0 / 3.0 * np.ones((10, 10))) assert np.allclose(cfim[2].detach().numpy(), np.zeros((1, 1))) @@ -420,7 +419,7 @@ def circ(x, y, z): x = tf.Variable(np.pi / 8 * np.ones(2), trainable=True) y = tf.Variable(np.pi / 8 * np.ones(10), trainable=True) z = tf.Variable([1.0], trainable=True) - cfim = qml.qinfo.classical_fisher(circ)(x, y, z) + cfim = classical_fisher(circ)(x, y, z) assert np.allclose(cfim[0], 2.0 / 3.0 * np.ones((2, 2))) assert np.allclose(cfim[1], 2.0 / 3.0 * np.ones((10, 10))) assert np.allclose(cfim[2], np.zeros((1, 1))) @@ -441,10 +440,10 @@ def circ(params): params = pnp.array(np.pi / 4, requires_grad=True) - assert np.allclose(qml.qinfo.classical_fisher(circ)(params), 1) + assert np.allclose(classical_fisher(circ)(params), 1) result = np.zeros((1, 1, 1), dtype="float64") - result_calc = qml.jacobian(qml.qinfo.classical_fisher(circ))(params) + result_calc = qml.jacobian(classical_fisher(circ))(params) assert np.allclose(result, result_calc, atol=1e-6) @@ -466,10 +465,10 @@ def circ(params): params = jnp.array(np.pi / 4) - assert qml.math.allclose(qml.qinfo.classical_fisher(circ)(params), 1.0) + assert qml.math.allclose(classical_fisher(circ)(params), 1.0) result = np.zeros((1, 1, 1), dtype="float64") - result_calc = jax.jacobian(qml.qinfo.classical_fisher(circ))(params) + result_calc = jax.jacobian(classical_fisher(circ))(params) assert np.allclose(result, result_calc, atol=1e-6) @@ -488,11 +487,11 @@ def circ(params): params = tf.Variable(np.pi / 4) - assert np.allclose(qml.qinfo.classical_fisher(circ)(params), 1) + assert np.allclose(classical_fisher(circ)(params), 1) result = np.zeros((1, 1, 1), dtype="float64") with tf.GradientTape() as tape: - loss = qml.qinfo.classical_fisher(circ)(params) + loss = classical_fisher(circ)(params) result_calc = tape.jacobian(loss, params) @@ -512,10 +511,10 @@ def circ(params): params = torch.tensor(np.pi / 4, requires_grad=True) - assert np.allclose(qml.qinfo.classical_fisher(circ)(params).detach().numpy(), 1) + assert np.allclose(classical_fisher(circ)(params).detach().numpy(), 1) result = np.zeros((1, 1, 1), dtype="float64") - result_calc = torch.autograd.functional.jacobian(qml.qinfo.classical_fisher(circ), params) + result_calc = torch.autograd.functional.jacobian(classical_fisher(circ), params) assert np.allclose(result, result_calc, atol=1e-6) @@ -550,22 +549,20 @@ def qfunc(weights): weights = torch.tensor( [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], requires_grad=True ) - grad_torch = torch.autograd.functional.jacobian( - qml.qinfo.classical_fisher(circuit), weights - ) + grad_torch = torch.autograd.functional.jacobian(classical_fisher(circuit), weights) circuit = qml.QNode(qfunc, dev) weights = pnp.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], requires_grad=True) - grad_autograd = qml.jacobian(qml.qinfo.classical_fisher(circuit))(weights) + grad_autograd = qml.jacobian(classical_fisher(circuit))(weights) circuit = qml.QNode(qfunc, dev) weights = jnp.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) - grad_jax = jax.jacobian(qml.qinfo.classical_fisher(circuit))(weights) + grad_jax = jax.jacobian(classical_fisher(circuit))(weights) circuit = qml.QNode(qfunc, dev) weights = tf.Variable([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) with tf.GradientTape() as tape: - loss = qml.qinfo.classical_fisher(circuit)(weights) + loss = classical_fisher(circuit)(weights) grad_tf = tape.jacobian(loss, weights) # Evaluate and compare diff --git a/tests/gradients/core/test_gradient_transform.py b/tests/gradients/core/test_gradient_transform.py index 61cd765f85a..44b2ade9e6a 100644 --- a/tests/gradients/core/test_gradient_transform.py +++ b/tests/gradients/core/test_gradient_transform.py @@ -31,10 +31,13 @@ def test_supported_gradient_kwargs(): """Test that all keyword arguments of gradient transforms are registered as supported gradient kwargs, and no others.""" # Collect all gradient transforms + + # Non-diff_methods to skip + methods_to_skip = ("metric_tensor", "classical_fisher", "quantum_fisher") + grad_transforms = [] for attr in qml.gradients.__dir__(): - if attr == "metric_tensor": - # Skip metric_tensor because it is not a diff_method + if attr in methods_to_skip: continue obj = getattr(qml.gradients, attr) if isinstance(obj, TransformDispatcher): diff --git a/tests/qinfo/test_fisher_deprecation.py b/tests/qinfo/test_fisher_deprecation.py new file mode 100644 index 00000000000..b5412f56a82 --- /dev/null +++ b/tests/qinfo/test_fisher_deprecation.py @@ -0,0 +1,47 @@ +# Copyright 2018-2024 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 the deprecation of the classical and quantum fisher information matrix in the pennylane.qinfo +""" +import pytest + +import pennylane as qml +import pennylane.numpy as pnp +from pennylane.qinfo import classical_fisher, quantum_fisher + + +@pytest.mark.parametrize("fn", (classical_fisher, quantum_fisher)) +def test_qinfo_fisher_fns_raises_warning(fn): + n_wires = 3 + n_params = 3 + + dev = qml.device("default.qubit", shots=10000) + + @qml.qnode(dev) + def circ(params): + for i in range(n_wires): + qml.Hadamard(wires=i) + + for x in params: + for j in range(n_wires): + qml.RX(x, wires=j) + qml.RY(x, wires=j) + qml.RZ(x, wires=j) + + return qml.probs(wires=range(n_wires)) + + params = pnp.zeros(n_params, requires_grad=True) + + with pytest.warns(qml.PennyLaneDeprecationWarning, match=f"{fn.__name__} is being migrated"): + fn(circ)(params)