Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[QIM-8] quantum fisher information (new) #2684

Merged
merged 16 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/code/qml_qinfo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ Transforms
:no-heading:
:no-inheritance-diagram:
:no-inherited-members:
:skip: metric_tensor
:skip: adjoint_metric_tensor
44 changes: 22 additions & 22 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
[(#2631)](https://github.com/PennyLaneAI/pennylane/pull/2631)
[(#2640)](https://github.com/PennyLaneAI/pennylane/pull/2640)
[(#2663)](https://github.com/PennyLaneAI/pennylane/pull/2663)
[(#2684)](https://github.com/PennyLaneAI/pennylane/pull/2684)

A `reduced_dm` function that can handle both state vectors and density matrix, to return a reduced density matrix:

Expand Down Expand Up @@ -231,40 +232,39 @@
1.3862943611198906
```

Support for the classical Fisher information matrix is also added:
Support for the classical and quantum Fisher information matrices, `qml.qinfo.classical_fisher` and `qml.qinfo.quantum_fisher` is also added:

First, let us define a parametrized quantum state and return its (classical) probability distribution for all
computational basis elements:
These are typically employed in variational optimization schemes to tilt the gradient in a more favorable direction, see [2103.15191](https://arxiv.org/abs/2103.15191) and [1909.02108](https://arxiv.org/abs/1909.02108). Here is a very simple example of a Hamiltonian loss function:

```python3
n_wires = 2
n_wires = 3

dev = qml.device("default.qubit", wires=n_wires)

@qml.qnode(dev)
def circ(params):
qml.RX(params[0], wires=0)
qml.RX(params[1], wires=0)
qml.CNOT(wires=(0,1))
return qml.probs(wires=range(n_wires))
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(1.*qml.PauliX(0) @ qml.PauliX(1) - 0.5 * qml.PauliZ(1))

params = pnp.array([0.5, 1., 0.2], requires_grad=True)
```
Executing this circuit yields the ``2**n_wires`` elements of the probability vector.

From this circuit we can directly obtain the gradient of the expectation value, as well as the classical fisher information matrix (cfim) and quantum fisher information matrix (qfim) of the variational state.
```pycon
>>> import pennylane.numpy as np
>>> params = np.random.random(2)
>>> circ(params)
tensor([0.77708372, 0. , 0. , 0.22291628], requires_grad=True)
>>> grad = qml.grad(circ)(params)
>>> cfim = qml.qinfo.classical_fisher(circ)(params)
>>> qfim = qml.qinfo.quantum_fisher(circ)(params)
```

We can obtain its ``(2, 2)`` classical fisher information matrix (CFIM) by simply calling the function returned
by ``classical_fisher()``:

From this we can compute the tilted (natural) gradients:
```pycon
>>> cfim_func = qml.qinfo.classical_fisher(circ)
>>> cfim_func(params)
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
>>> c_grad = cfim @ grad
>>> q_grad = qfim @ grad
>>> print(f"Gradient: {grad} \n c_grad: {c_grad} \n q_grad: {q_grad}")
Gradient: [ 0.59422561 -0.02615095 -0.05146226]
c_grad: [ 5.94225615e-01 -2.61509542e-02 -1.18674655e-18]
q_grad: [ 0.59422561 -0.02615095 -0.03989212]
```

The support for calculating the fidelity between two arbitrary states is added as `qml.math.fidelity` for state
Expand Down
9 changes: 8 additions & 1 deletion pennylane/qinfo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@
# limitations under the License.
"""Differentiable quantum information module"""

from .transforms import reduced_dm, vn_entropy, mutual_info, classical_fisher, fidelity
from .transforms import (
reduced_dm,
vn_entropy,
mutual_info,
classical_fisher,
quantum_fisher,
fidelity,
)
99 changes: 93 additions & 6 deletions pennylane/qinfo/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# pylint: disable=import-outside-toplevel, not-callable
import functools
import pennylane as qml
from pennylane.transforms import batch_transform
from pennylane.transforms import batch_transform, metric_tensor, adjoint_metric_tensor


def reduced_dm(qnode, wires):
Expand Down Expand Up @@ -258,17 +258,19 @@ def classical_fisher(qnode, argnums=0):

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))]``.
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))]``.

.. warning::

The ``classical_fisher()`` matrix is currently not differentiable.

.. seealso:: :func:`~.pennylane.metric_tensor`
.. seealso:: :func:`~.pennylane.metric_tensor`, :func:`~.pennylane.qinfo.transforms.quantum_fisher`

**Example**

Expand Down Expand Up @@ -398,6 +400,91 @@ def wrapper(*args, **kwargs):
return wrapper


def quantum_fisher(qnode, *args, hardware=False, **kwargs):
r"""Returns a function that computes the quantum fisher information matrix (QFIM) of a given :class:`.QNode` or quantum tape.

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 <https://arxiv.org/abs/2103.15191>`_):
rmoyard marked this conversation as resolved.
Show resolved Hide resolved

.. 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.qinfo.transforms.classical_fisher`

Args:
qnode (:class:`.QNode` or qml.QuantumTape): A :class:`.QNode` or quantum tape that may have arbitrary return types.
hardware (bool): Indicate if execution needs to be hardware compatible (True)

Returns:
func: The function that computes the quantum fisher information matrix.

.. note::

Qottmann marked this conversation as resolved.
Show resolved Hide resolved
``quantum_fisher`` coincides with the ``metric_tensor`` with a prefactor of :math:`4`. In case of ``hardware=True``, the hardware compatible transform :func:`~.pennylane.metric_tensor` is used.
In case of ``hardware=False``, :func:`~.pennylane.adjoint_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 <https://arxiv.org/abs/1909.02108>`_.
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.PauliX(0) @ qml.PauliX(1) - 0.5 * qml.PauliZ(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)
[ 0.59422561, -0.02615095, -0.05146226]

>>> qfim = qml.qinfo.quantum_fisher(circ)(params)
np.diag([1., 1., 0.77517241])

>>> q_nat_grad = qfim @ grad
[ 0.59422561 -0.02615095 -0.03989212]

When using real hardware with finite shots, we have to specify ``hardware=True`` in order to compute the QFIM.
Additionally, we need to provide a device that has a spare wire for the Hadamard test, otherwise it will just be able to compute the block diagonal terms.

>>> dev = qml.device("default.qubit", wires=n_wires+1, shots=1000)
>>> circ = qml.QNode(circ, dev)
>>> qfim = qml.qinfo.quantum_fisher(circ, hardware=True)(params)

"""
# TODO: ``hardware`` argument will be obsolete in future releases when ``shots`` can be inferred.
if hardware:
Comment on lines +474 to +475
Copy link
Member

@josh146 josh146 Jun 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eddddddy / @Qottmann: I think we should be able to infer this :) Via something like:

if qnode.device.shots is not None:

I think it might be best to make this change in the feature freeze week, for two reasons:

  • Consistency with the rest of PennyLane (where there is no hardware option)
  • Because, more accurately, the logic here is more about 'are shots finite', rather than 'is the device hardware'. There will be cases where you are using a simulator, but will be using finite shots, so this may confuse the reader!


def wrapper(*args0, **kwargs0):
return 4 * metric_tensor(qnode, *args, **kwargs)(*args0, **kwargs0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is assumed here that qnode is a QNode, so you'll need to update the docstring to remove mention of the tape!


else:

def wrapper(*args0, **kwargs0):
return 4 * adjoint_metric_tensor(qnode, *args, **kwargs)(*args0, **kwargs0)

return wrapper


def fidelity(qnode0, qnode1, wires0, wires1):
r"""Compute the fidelity for two :class:`.QNode` returning a :func:`~.state` (a state can be a state vector
or a density matrix, depending on the device) acting on quantum systems with the same size.
Expand Down
10 changes: 10 additions & 0 deletions pennylane/transforms/metric_tensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ def expand_fn(tape, approx=None, allow_nonunitary=True, aux_wire=None, device_wi
def metric_tensor(tape, approx=None, allow_nonunitary=True, aux_wire=None, device_wires=None):
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:

.. math::

\text{metric_tensor}_{i, j} = \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`.
It is closely related to the quantum fisher information matrix, see :func:`~.pennylane.qinfo.transforms.quantum_fisher` and eq. (27) in `arxiv:2103.15191 <https://arxiv.org/abs/2103.15191>`_.

.. note::

Only gates that have a single parameter and define a ``generator`` are supported.
Expand Down
36 changes: 32 additions & 4 deletions tests/qinfo/test_fisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
"""
Tests for the classical fisher information matrix in the pennylane.qinfo
"""
# pylint: disable=no-self-use, import-outside-toplevel, no-member, import-error, too-few-public-methods
# pylint: disable=no-self-use, import-outside-toplevel, no-member, import-error, too-few-public-methods, bad-continuation
import pytest

import pennylane as qml
import pennylane.numpy as pnp
import numpy as np


from pennylane.qinfo import classical_fisher
from pennylane.qinfo import classical_fisher, quantum_fisher
from pennylane.qinfo.transforms import _make_probs, _compute_cfim


Expand Down Expand Up @@ -96,7 +96,7 @@ def test_compute_cfim_trivial_distribution(self, n_params, n_wires):


class TestIntegration:
"""Integration test of classical fisher information matrix CFIM"""
"""Integration test of classical and quantum fisher information matrices"""

@pytest.mark.parametrize("n_wires", np.arange(1, 5))
@pytest.mark.parametrize("n_params", np.arange(1, 5))
Expand All @@ -121,7 +121,9 @@ def circ(params):
res = classical_fisher(circ)(params)
assert np.allclose(res, n_wires * np.ones((n_params, n_params)))

def test_hardware_compatibility_classical_fisher(self):
def test_hardware_compatibility_classical_fisher(
self,
):
"""Testing that classical_fisher can be computed with finite shots"""
n_wires = 3
n_params = 3
Expand All @@ -145,6 +147,32 @@ def circ(params):
res = qml.qinfo.classical_fisher(circ)(params)
assert np.allclose(res, n_wires * np.ones((n_params, n_params)), atol=1)

def test_quantum_fisher_info(
self,
):
"""Integration test of quantum fisher information matrix CFIM. This is just calling ``qml.metric_tensor`` or ``qml.adjoint_metric_tensor`` and multiplying by a factor of 4"""

n_wires = 2

dev = qml.device("default.qubit", wires=n_wires)

@qml.qnode(dev)
def circ(params):
qml.RX(params[0], wires=0)
qml.RX(params[1], wires=0)
qml.CNOT(wires=(0, 1))
return qml.state()

params = pnp.random.random(2)

QFIM_hard = quantum_fisher(circ, hardware=True)(params)
QFIM1_hard = 4.0 * qml.metric_tensor(circ)(params)

QFIM = quantum_fisher(circ, hardware=False)(params)
QFIM1 = 4.0 * qml.adjoint_metric_tensor(circ)(params)
assert np.allclose(QFIM, QFIM1)
assert np.allclose(QFIM_hard, QFIM1_hard)


class TestInterfacesClassicalFisher:
"""Integration tests for the classical fisher information matrix CFIM"""
Expand Down