Skip to content

Commit 22d5194

Browse files
authored
Merge pull request #1454 from qiboteam/matrix_power
Add `matrix_power` to `quantum_info.linalg_operations`
2 parents ee6286c + f96efdf commit 22d5194

File tree

9 files changed

+417
-325
lines changed

9 files changed

+417
-325
lines changed

doc/source/api-reference/qibo.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1964,6 +1964,12 @@ Matrix exponentiation
19641964
.. autofunction:: qibo.quantum_info.matrix_exponentiation
19651965

19661966

1967+
Matrix power
1968+
""""""""""""
1969+
1970+
.. autofunction:: qibo.quantum_info.matrix_power
1971+
1972+
19671973
Quantum Networks
19681974
^^^^^^^^^^^^^^^^
19691975

poetry.lock

+318-292
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/qibo/backends/abstract.py

+16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
from typing import Union
23

34
from qibo.config import raise_error
45

@@ -355,6 +356,21 @@ def calculate_matrix_exp(
355356
"""
356357
raise_error(NotImplementedError)
357358

359+
@abc.abstractmethod
360+
def calculate_matrix_power(
361+
self, matrix, power: Union[float, int]
362+
): # pragma: no cover
363+
"""Calculate the (fractional) ``power`` :math:`\\alpha` of ``matrix`` :math:`A`,
364+
i.e. :math:`A^{\\alpha}`.
365+
366+
.. note::
367+
For the ``pytorch`` backend, this method relies on a copy of the original tensor.
368+
This may break the gradient flow. For the GPU backends (i.e. ``cupy`` and
369+
``cuquantum``), this method falls back to CPU whenever ``power`` is not
370+
an integer.
371+
"""
372+
raise_error(NotImplementedError)
373+
358374
@abc.abstractmethod
359375
def calculate_hamiltonian_matrix_product(
360376
self, matrix1, matrix2

src/qibo/backends/numpy.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import collections
22
import math
3+
from typing import Union
34

45
import numpy as np
56
from scipy import sparse
6-
from scipy.linalg import block_diag
7+
from scipy.linalg import block_diag, fractional_matrix_power
78

89
from qibo import __version__
910
from qibo.backends import einsum_utils
@@ -768,6 +769,14 @@ def calculate_matrix_exp(self, a, matrix, eigenvectors=None, eigenvalues=None):
768769
ud = self.np.transpose(np.conj(eigenvectors))
769770
return self.np.matmul(eigenvectors, self.np.matmul(expd, ud))
770771

772+
def calculate_matrix_power(self, matrix, power: Union[float, int]):
773+
if not isinstance(power, (float, int)):
774+
raise_error(
775+
TypeError,
776+
f"``power`` must be either float or int, but it is type {type(power)}.",
777+
)
778+
return fractional_matrix_power(matrix, power)
779+
771780
# TODO: remove this method
772781
def calculate_hamiltonian_matrix_product(self, matrix1, matrix2):
773782
return matrix1 @ matrix2

src/qibo/backends/pytorch.py

+5
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,11 @@ def calculate_matrix_exp(self, a, matrix, eigenvectors=None, eigenvalues=None):
199199
ud = self.np.conj(eigenvectors).T
200200
return self.np.matmul(eigenvectors, self.np.matmul(expd, ud))
201201

202+
def calculate_matrix_power(self, matrix, power):
203+
copied = self.to_numpy(self.np.copy(matrix))
204+
copied = super().calculate_matrix_power(copied, power)
205+
return self.cast(copied, dtype=copied.dtype)
206+
202207
def _test_regressions(self, name):
203208
if name == "test_measurementresult_apply_bitflips":
204209
return [

src/qibo/quantum_info/entropies.py

+7-25
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
from typing import Union
44

55
import numpy as np
6-
from scipy.linalg import fractional_matrix_power
76

87
from qibo.backends import _check_backend
9-
from qibo.backends.pytorch import PyTorchBackend
108
from qibo.config import PRECISION_TOL, raise_error
11-
from qibo.quantum_info.linalg_operations import partial_trace
9+
from qibo.quantum_info.linalg_operations import matrix_power, partial_trace
1210
from qibo.quantum_info.metrics import _check_hermitian, purity
1311

1412

@@ -724,7 +722,7 @@ def renyi_entropy(state, alpha: Union[float, int], base: float = 2, backend=None
724722
/ np.log2(base)
725723
)
726724

727-
log = backend.np.log2(backend.np.trace(_matrix_power(state, alpha, backend)))
725+
log = backend.np.log2(backend.np.trace(matrix_power(state, alpha, backend)))
728726

729727
return (1 / (1 - alpha)) * log / np.log2(base)
730728

@@ -823,17 +821,17 @@ def relative_renyi_entropy(
823821
return relative_von_neumann_entropy(state, target, base, backend=backend)
824822

825823
if alpha == np.inf:
826-
new_state = _matrix_power(state, 0.5, backend)
827-
new_target = _matrix_power(target, 0.5, backend)
824+
new_state = matrix_power(state, 0.5, backend)
825+
new_target = matrix_power(target, 0.5, backend)
828826

829827
log = backend.np.log2(
830828
backend.calculate_norm_density_matrix(new_state @ new_target, order=1)
831829
)
832830

833831
return -2 * log / np.log2(base)
834832

835-
log = _matrix_power(state, alpha, backend)
836-
log = log @ _matrix_power(target, 1 - alpha, backend)
833+
log = matrix_power(state, alpha, backend)
834+
log = log @ matrix_power(target, 1 - alpha, backend)
837835
log = backend.np.log2(backend.np.trace(log))
838836

839837
return (1 / (alpha - 1)) * log / np.log2(base)
@@ -891,7 +889,7 @@ def tsallis_entropy(state, alpha: float, base: float = 2, backend=None):
891889
return von_neumann_entropy(state, base=base, backend=backend)
892890

893891
return (1 / (1 - alpha)) * (
894-
backend.np.trace(_matrix_power(state, alpha, backend)) - 1
892+
backend.np.trace(matrix_power(state, alpha, backend)) - 1
895893
)
896894

897895

@@ -953,19 +951,3 @@ def entanglement_entropy(
953951
)
954952

955953
return entropy_entanglement
956-
957-
958-
def _matrix_power(matrix, alpha, backend):
959-
"""Calculates ``matrix ** alpha`` according to backend."""
960-
if backend.__class__.__name__ in [
961-
"CupyBackend",
962-
"CuQuantumBackend",
963-
]: # pragma: no cover
964-
new_matrix = backend.to_numpy(matrix)
965-
else:
966-
new_matrix = backend.np.copy(matrix)
967-
968-
if len(new_matrix.shape) == 1:
969-
new_matrix = backend.np.outer(new_matrix, backend.np.conj(new_matrix))
970-
971-
return backend.cast(fractional_matrix_power(backend.to_numpy(new_matrix), alpha))

src/qibo/quantum_info/linalg_operations.py

+18
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,21 @@ def matrix_exponentiation(
196196
backend = _check_backend(backend)
197197

198198
return backend.calculate_matrix_exp(phase, matrix, eigenvectors, eigenvalues)
199+
200+
201+
def matrix_power(matrix, power: Union[float, int], backend=None):
202+
"""Given a ``matrix`` :math:`A` and power :math:`\\alpha`, calculate :math:`A^{\\alpha}`.
203+
204+
Args:
205+
matrix (ndarray): matrix whose power to calculate.
206+
power (float or int): power to raise ``matrix`` to.
207+
backend (:class:`qibo.backends.abstract.Backend`, optional): backend
208+
to be used in the execution. If ``None``, it uses
209+
:class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
210+
211+
Returns:
212+
ndarray: matrix power :math:`A^{\\alpha}`.
213+
"""
214+
backend = _check_backend(backend)
215+
216+
return backend.calculate_matrix_power(matrix, power)

tests/test_quantum_info_entropies.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import numpy as np
22
import pytest
3-
from scipy.linalg import sqrtm
43

54
from qibo.config import PRECISION_TOL
65
from qibo.quantum_info.entropies import (
7-
_matrix_power,
86
classical_mutual_information,
97
classical_relative_entropy,
108
classical_relative_renyi_entropy,
@@ -19,6 +17,7 @@
1917
tsallis_entropy,
2018
von_neumann_entropy,
2119
)
20+
from qibo.quantum_info.linalg_operations import matrix_power
2221
from qibo.quantum_info.random_ensembles import (
2322
random_density_matrix,
2423
random_statevector,
@@ -646,8 +645,18 @@ def test_relative_renyi_entropy(backend, alpha, base, state_flag, target_flag):
646645
if alpha == 1.0:
647646
log = relative_von_neumann_entropy(state, target, base, backend=backend)
648647
elif alpha == np.inf:
649-
new_state = _matrix_power(state, 0.5, backend)
650-
new_target = _matrix_power(target, 0.5, backend)
648+
state_outer = (
649+
backend.np.outer(state, backend.np.conj(state.T))
650+
if state_flag
651+
else state
652+
)
653+
target_outer = (
654+
backend.np.outer(target, backend.np.conj(target.T))
655+
if target_flag
656+
else target
657+
)
658+
new_state = matrix_power(state_outer, 0.5, backend)
659+
new_target = matrix_power(target_outer, 0.5, backend)
651660

652661
log = backend.np.log2(
653662
backend.calculate_norm_density_matrix(
@@ -663,8 +672,8 @@ def test_relative_renyi_entropy(backend, alpha, base, state_flag, target_flag):
663672
if len(target.shape) == 1:
664673
target = backend.np.outer(target, backend.np.conj(target))
665674

666-
log = _matrix_power(state, alpha, backend)
667-
log = log @ _matrix_power(target, 1 - alpha, backend)
675+
log = matrix_power(state, alpha, backend)
676+
log = log @ matrix_power(target, 1 - alpha, backend)
668677
log = backend.np.log2(backend.np.trace(log))
669678

670679
log = (1 / (alpha - 1)) * log / np.log2(base)
@@ -710,7 +719,7 @@ def test_tsallis_entropy(backend, alpha, base):
710719
target = von_neumann_entropy(state, base=base, backend=backend)
711720
else:
712721
target = (1 / (1 - alpha)) * (
713-
backend.np.trace(_matrix_power(state, alpha, backend)) - 1
722+
backend.np.trace(matrix_power(state, alpha, backend)) - 1
714723
)
715724

716725
backend.assert_allclose(

tests/test_quantum_info_operations.py

+21
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
from qibo.quantum_info.linalg_operations import (
66
anticommutator,
77
commutator,
8+
matrix_power,
89
partial_trace,
910
)
11+
from qibo.quantum_info.metrics import purity
1012
from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector
1113

1214

@@ -109,3 +111,22 @@ def test_partial_trace(backend, density_matrix):
109111
Id = backend.identity_density_matrix(1, normalize=True)
110112

111113
backend.assert_allclose(traced, Id)
114+
115+
116+
@pytest.mark.parametrize("power", [2, 2.0, "2"])
117+
def test_matrix_power(backend, power):
118+
nqubits = 2
119+
dims = 2**nqubits
120+
121+
state = random_density_matrix(dims, backend=backend)
122+
123+
if isinstance(power, str):
124+
with pytest.raises(TypeError):
125+
test = matrix_power(state, power, backend)
126+
else:
127+
power = matrix_power(state, power, backend)
128+
129+
backend.assert_allclose(
130+
float(backend.np.real(backend.np.trace(power))),
131+
purity(state, backend=backend),
132+
)

0 commit comments

Comments
 (0)