Skip to content

Commit

Permalink
Merge branch 'master' into remove_qml_utils_expand
Browse files Browse the repository at this point in the history
  • Loading branch information
antalszava committed Jun 10, 2022
2 parents f19a40c + 5d83fdf commit 97a08af
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 33 deletions.
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>`_):
.. 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::
``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:

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

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

0 comments on commit 97a08af

Please sign in to comment.