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)