Skip to content

Commit b5aabcc

Browse files
authored
Merge pull request #1487 from qiboteam/qfim
Add `quantum_fisher_information_matrix` to `quantum_info.metrics`
2 parents fbc49be + 7fe3d1f commit b5aabcc

File tree

7 files changed

+168
-0
lines changed

7 files changed

+168
-0
lines changed

doc/source/api-reference/qibo.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1951,6 +1951,12 @@ Frame Potential
19511951
.. autofunction:: qibo.quantum_info.frame_potential
19521952

19531953

1954+
Quantum Fisher Information Matrix
1955+
"""""""""""""""""""""""""""""""""
1956+
1957+
.. autofunction:: qibo.quantum_info.quantum_fisher_information_matrix
1958+
1959+
19541960
Linear Algebra Operations
19551961
^^^^^^^^^^^^^^^^^^^^^^^^^
19561962

src/qibo/backends/abstract.py

+7
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,13 @@ def calculate_singular_value_decomposition(self, matrix): # pragma: no cover
376376
"""Calculate the Singular Value Decomposition of ``matrix``."""
377377
raise_error(NotImplementedError)
378378

379+
@abc.abstractmethod
380+
def calculate_jacobian_matrix(
381+
self, circuit, parameters, initial_state=None, return_complex: bool = True
382+
): # pragma: no cover
383+
"""Calculate the Jacobian matrix of ``circuit`` with respect to varables ``params``."""
384+
raise_error(NotImplementedError)
385+
379386
@abc.abstractmethod
380387
def calculate_hamiltonian_matrix_product(
381388
self, matrix1, matrix2

src/qibo/backends/numpy.py

+9
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,15 @@ def calculate_matrix_power(
799799
def calculate_singular_value_decomposition(self, matrix):
800800
return self.np.linalg.svd(matrix)
801801

802+
def calculate_jacobian_matrix(
803+
self, circuit, parameters=None, initial_state=None, return_complex: bool = True
804+
):
805+
raise_error(
806+
NotImplementedError,
807+
"This method is only implemented in backends that allow automatic differentiation, "
808+
+ "e.g. ``PytorchBackend`` and ``TensorflowBackend``.",
809+
)
810+
802811
# TODO: remove this method
803812
def calculate_hamiltonian_matrix_product(self, matrix1, matrix2):
804813
return matrix1 @ matrix2

src/qibo/backends/pytorch.py

+15
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,21 @@ def calculate_matrix_power(
239239
copied = super().calculate_matrix_power(copied, power, precision_singularity)
240240
return self.cast(copied, dtype=copied.dtype)
241241

242+
def calculate_jacobian_matrix(
243+
self, circuit, parameters=None, initial_state=None, return_complex: bool = True
244+
):
245+
copied = circuit.copy(deep=True)
246+
247+
def func(parameters):
248+
"""torch requires object(s) to be wrapped in a function."""
249+
copied.set_parameters(parameters)
250+
state = self.execute_circuit(copied, initial_state=initial_state).state()
251+
if return_complex:
252+
return self.np.real(state), self.np.imag(state)
253+
return self.np.real(state)
254+
255+
return self.np.autograd.functional.jacobian(func, parameters)
256+
242257
def _test_regressions(self, name):
243258
if name == "test_measurementresult_apply_bitflips":
244259
return [

src/qibo/backends/tensorflow.py

+20
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,26 @@ def calculate_singular_value_decomposition(self, matrix):
220220
S, U, V = self.tf.linalg.svd(matrix)
221221
return U, S, self.np.conj(self.np.transpose(V))
222222

223+
def calculate_jacobian_matrix(
224+
self, circuit, parameters=None, initial_state=None, return_complex: bool = True
225+
):
226+
copied = circuit.copy(deep=True)
227+
228+
# necessary for the tape to properly watch the variables
229+
parameters = self.tf.Variable(parameters)
230+
231+
with self.tf.GradientTape(persistent=return_complex) as tape:
232+
copied.set_parameters(parameters)
233+
state = self.execute_circuit(copied, initial_state=initial_state).state()
234+
real = self.np.real(state)
235+
if return_complex:
236+
imag = self.np.imag(state)
237+
238+
if return_complex:
239+
return tape.jacobian(real, parameters), tape.jacobian(imag, parameters)
240+
241+
return tape.jacobian(real, parameters)
242+
223243
def calculate_hamiltonian_matrix_product(self, matrix1, matrix2):
224244
if self.is_sparse(matrix1) or self.is_sparse(matrix2):
225245
raise_error(

src/qibo/quantum_info/metrics.py

+70
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,76 @@ def frame_potential(
845845
return potential / samples**2
846846

847847

848+
def quantum_fisher_information_matrix(
849+
circuit,
850+
parameters=None,
851+
initial_state=None,
852+
return_complex: bool = True,
853+
backend=None,
854+
):
855+
"""Calculate the Quantum Fisher Information Matrix (QFIM) of a parametrized ``circuit``.
856+
857+
Given a set of ``parameters`` :math:`\\theta = \\{\\theta_{k}\\}_{k\\in[M]}` and a
858+
parameterized unitary ``circuit`` :math:`U(\\theta)` acting on an ``initial_state``
859+
:math:`\\ket{\\phi}`, the QFIM is such that its elements can be calculated as
860+
861+
.. math::
862+
\\mathbf{F}_{jk} = 4 \\, \\text{Re}\\left\\{ \\braket{\\partial_{j} \\psi | \\partial_{k}
863+
\\psi} - \\braket{\\partial_{j} \\psi | \\psi}\\!\\braket{\\psi | \\partial_{k} \\psi}
864+
\\right\\} \\, ,
865+
866+
where we have used the short notations :math:`\\ket{\\psi} \\equiv \\ket{\\psi(\\theta)}
867+
= U(\\theta) \\ket{\\phi}`, and :math:`\\ket{\\partial_{k} \\psi} \\equiv \\frac{\\partial}
868+
{\\partial\\theta_{k}} \\ket{\\psi(\\theta)}`.
869+
If the ``initial_state`` :math:`\\ket{\\phi}` is not specified, it defaults to
870+
:math:`\\ket{0}^{\\otimes n}`.
871+
872+
Args:
873+
circuit (:class:`qibo.models.circuit.Circuit`): parametrized circuit :math:`U(\\theta)`.
874+
parameters (ndarray, optional): parameters whose QFIM to calculate.
875+
If ``None``, QFIM is calculated with the paremeters from ``circuit``, i.e.
876+
``parameters = circuit.get_parameters()``. Defaults to ``None``.
877+
initial_state (ndarray, optional): Initial configuration. It can be specified
878+
by the setting the state vector using an array or a circuit. If ``None``,
879+
the initial state is :math:`\\ket{0}^{\\otimes n}`. Defaults to ``None``.
880+
return_complex (bool, optional): If ``True``, calculates the Jacobian matrix
881+
of real and imaginary parts of :math:`\\ket{\\psi(\\theta)}`. If ``False``,
882+
calculates only the Jacobian matrix of the real part. Defaults to ``True``.
883+
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
884+
in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
885+
Defaults to ``None``.
886+
887+
Returns:
888+
ndarray: Quantum Fisher Information :math:`\\mathbf{F}`.
889+
"""
890+
backend = _check_backend(backend)
891+
892+
if parameters is None:
893+
parameters = circuit.get_parameters()
894+
parameters = backend.cast(parameters, dtype=float).flatten()
895+
896+
jacobian = backend.calculate_jacobian_matrix(
897+
circuit, parameters, initial_state, return_complex
898+
)
899+
900+
if return_complex:
901+
jacobian = jacobian[0] + 1j * jacobian[1]
902+
903+
jacobian = backend.cast(jacobian, dtype=np.complex128)
904+
905+
copied = circuit.copy(deep=True)
906+
copied.set_parameters(parameters)
907+
908+
state = backend.execute_circuit(copied, initial_state=initial_state).state()
909+
910+
overlaps = jacobian.T @ state
911+
912+
qfim = jacobian.T @ jacobian
913+
qfim = qfim - backend.np.outer(overlaps, backend.np.conj(overlaps.T))
914+
915+
return 4 * backend.np.real(qfim)
916+
917+
848918
def _check_hermitian(matrix, backend=None):
849919
"""Checks if a given matrix is Hermitian.
850920

tests/test_quantum_info_metrics.py

+41
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from qibo import Circuit, gates
55
from qibo.config import PRECISION_TOL
6+
from qibo.models.encodings import _generate_rbs_angles, unary_encoder
67
from qibo.quantum_info.metrics import (
78
average_gate_fidelity,
89
bures_angle,
@@ -18,6 +19,7 @@
1819
process_fidelity,
1920
process_infidelity,
2021
purity,
22+
quantum_fisher_information_matrix,
2123
trace_distance,
2224
)
2325
from qibo.quantum_info.random_ensembles import (
@@ -386,3 +388,42 @@ def test_frame_potential(backend, nqubits, power_t, samples):
386388
)
387389

388390
backend.assert_allclose(potential, potential_haar, rtol=1e-2, atol=1e-2)
391+
392+
393+
@pytest.mark.parametrize("params_flag", [None, True])
394+
@pytest.mark.parametrize("return_complex", [False, True])
395+
@pytest.mark.parametrize("nqubits", [4, 8])
396+
def test_qfim(backend, nqubits, return_complex, params_flag):
397+
if backend.name not in ["pytorch", "tensorflow"]:
398+
circuit = Circuit(nqubits)
399+
params = np.random.rand(3)
400+
params = backend.cast(params, dtype=params.dtype)
401+
with pytest.raises(NotImplementedError):
402+
test = quantum_fisher_information_matrix(circuit, params, backend=backend)
403+
else:
404+
# QFIM from https://arxiv.org/abs/2405.20408 is known analytically
405+
data = np.random.rand(nqubits)
406+
data = backend.cast(data, dtype=data.dtype)
407+
408+
params = _generate_rbs_angles(data, nqubits, "diagonal")
409+
params = backend.cast(params, dtype=np.float64)
410+
411+
target = [1]
412+
for param in params[:-1]:
413+
elem = float(target[-1] * backend.np.sin(param) ** 2)
414+
target.append(elem)
415+
target = 4 * backend.np.diag(backend.cast(target, dtype=np.float64))
416+
417+
# numerical qfim from quantum_info
418+
circuit = unary_encoder(data, "diagonal")
419+
420+
if params_flag is not None:
421+
circuit.set_parameters(params)
422+
else:
423+
params = params_flag
424+
425+
qfim = quantum_fisher_information_matrix(
426+
circuit, params, return_complex=return_complex, backend=backend
427+
)
428+
429+
backend.assert_allclose(qfim, target, atol=1e-6)

0 commit comments

Comments
 (0)