Skip to content

Commit 2144add

Browse files
authored
Merge pull request #1473 from qiboteam/svd
Add `calculate_singular_value_decompositon` as a backend method
2 parents 85f929b + af57061 commit 2144add

9 files changed

+99
-10
lines changed

doc/source/api-reference/qibo.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1981,6 +1981,12 @@ Matrix power
19811981
.. autofunction:: qibo.quantum_info.matrix_power
19821982

19831983

1984+
Singular value decomposition
1985+
""""""""""""""""""""""""""""
1986+
1987+
.. autofunction:: qibo.quantum_info.singular_value_decomposition
1988+
1989+
19841990
Quantum Networks
19851991
^^^^^^^^^^^^^^^^
19861992

src/qibo/backends/abstract.py

+5
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,11 @@ def calculate_matrix_power(
371371
"""
372372
raise_error(NotImplementedError)
373373

374+
@abc.abstractmethod
375+
def calculate_singular_value_decomposition(self, matrix): # pragma: no cover
376+
"""Calculate the Singular Value Decomposition of ``matrix``."""
377+
raise_error(NotImplementedError)
378+
374379
@abc.abstractmethod
375380
def calculate_hamiltonian_matrix_product(
376381
self, matrix1, matrix2

src/qibo/backends/numpy.py

+3
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,9 @@ def calculate_matrix_power(self, matrix, power: Union[float, int]):
777777
)
778778
return fractional_matrix_power(matrix, power)
779779

780+
def calculate_singular_value_decomposition(self, matrix):
781+
return self.np.linalg.svd(matrix)
782+
780783
# TODO: remove this method
781784
def calculate_hamiltonian_matrix_product(self, matrix1, matrix2):
782785
return matrix1 @ matrix2

src/qibo/backends/pytorch.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class TorchMatrices(NumpyMatrices):
1616
"""
1717

1818
def __init__(self, dtype, requires_grad):
19-
import torch # pylint: disable=import-outside-toplevel
19+
import torch # pylint: disable=import-outside-toplevel # type: ignore
2020

2121
super().__init__(dtype)
2222
self.np = torch
@@ -38,7 +38,7 @@ def Unitary(self, u):
3838
class PyTorchBackend(NumpyBackend):
3939
def __init__(self):
4040
super().__init__()
41-
import torch # pylint: disable=import-outside-toplevel
41+
import torch # pylint: disable=import-outside-toplevel # type: ignore
4242

4343
# Global variable to enable or disable gradient calculation
4444
self.gradients = True

src/qibo/backends/tensorflow.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class TensorflowMatrices(NumpyMatrices):
1515
def __init__(self, dtype):
1616
super().__init__(dtype)
1717
import tensorflow as tf # pylint: disable=import-error
18-
import tensorflow.experimental.numpy as tnp # pylint: disable=import-error
18+
import tensorflow.experimental.numpy as tnp # pylint: disable=import-error # type: ignore
1919

2020
self.tf = tf
2121
self.np = tnp
@@ -35,7 +35,7 @@ def __init__(self):
3535
os.environ["TF_CPP_MIN_LOG_LEVEL"] = str(TF_LOG_LEVEL)
3636

3737
import tensorflow as tf # pylint: disable=import-error
38-
import tensorflow.experimental.numpy as tnp # pylint: disable=import-error
38+
import tensorflow.experimental.numpy as tnp # pylint: disable=import-error # type: ignore
3939

4040
if TF_LOG_LEVEL >= 2:
4141
tf.get_logger().setLevel("ERROR")
@@ -192,6 +192,11 @@ def calculate_matrix_exp(self, a, matrix, eigenvectors=None, eigenvalues=None):
192192
return self.tf.linalg.expm(-1j * a * matrix)
193193
return super().calculate_matrix_exp(a, matrix, eigenvectors, eigenvalues)
194194

195+
def calculate_singular_value_decomposition(self, matrix):
196+
# needed to unify order of return
197+
S, U, V = self.tf.linalg.svd(matrix)
198+
return U, S, self.np.conj(self.np.transpose(V))
199+
195200
def calculate_hamiltonian_matrix_product(self, matrix1, matrix2):
196201
if self.is_sparse(matrix1) or self.is_sparse(matrix2):
197202
raise_error(

src/qibo/quantum_info/linalg_operations.py

+26
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,29 @@ def matrix_power(matrix, power: Union[float, int], backend=None):
293293
backend = _check_backend(backend)
294294

295295
return backend.calculate_matrix_power(matrix, power)
296+
297+
298+
def singular_value_decomposition(matrix, backend=None):
299+
"""Calculate the Singular Value Decomposition (SVD) of ``matrix``.
300+
301+
Given an :math:`M \\times N` complex matrix :math:`A`, its SVD is given by
302+
303+
.. math:
304+
A = U \\, S \\, V^{\\dagger} \\, ,
305+
306+
where :math:`U` and :math:`V` are, respectively, an :math:`M \\times M`
307+
and an :math:`N \\times N` complex unitary matrices, and :math:`S` is an
308+
:math:`M \\times N` diagonal matrix with the singular values of :math:`A`.
309+
310+
Args:
311+
matrix (ndarray): matrix whose SVD to calculate.
312+
backend (:class:`qibo.backends.abstract.Backend`, optional): backend
313+
to be used in the execution. If ``None``, it uses
314+
:class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
315+
316+
Returns:
317+
ndarray, ndarray, ndarray: Singular value decomposition of :math:`A`.
318+
"""
319+
backend = _check_backend(backend)
320+
321+
return backend.calculate_singular_value_decomposition(matrix)

src/qibo/quantum_info/superoperator_transformations.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from qibo.gates.abstract import Gate
1212
from qibo.gates.gates import Unitary
1313
from qibo.gates.special import FusedGate
14+
from qibo.quantum_info.linalg_operations import singular_value_decomposition
1415

1516

1617
def vectorization(state, order: str = "row", backend=None):
@@ -483,10 +484,12 @@ def choi_to_kraus(
483484
warnings.warn("Input choi_super_op is a non-completely positive map.")
484485

485486
# using singular value decomposition because choi_super_op is non-CP
486-
U, coefficients, V = np.linalg.svd(backend.to_numpy(choi_super_op))
487-
U = np.transpose(U)
488-
coefficients = np.sqrt(coefficients)
489-
V = np.conj(V)
487+
U, coefficients, V = singular_value_decomposition(
488+
choi_super_op, backend=backend
489+
)
490+
U = U.T
491+
coefficients = backend.np.sqrt(coefficients)
492+
V = backend.np.conj(V)
490493

491494
kraus_left, kraus_right = [], []
492495
for coeff, eigenvector_left, eigenvector_right in zip(coefficients, U, V):

tests/test_quantum_info_operations.py

+32
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
matrix_power,
99
partial_trace,
1010
partial_transpose,
11+
singular_value_decomposition,
1112
)
1213
from qibo.quantum_info.metrics import purity
1314
from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector
@@ -218,3 +219,34 @@ def test_matrix_power(backend, power):
218219
float(backend.np.real(backend.np.trace(power))),
219220
purity(state, backend=backend),
220221
)
222+
223+
224+
def test_singular_value_decomposition(backend):
225+
zero = np.array([1, 0], dtype=complex)
226+
one = np.array([0, 1], dtype=complex)
227+
plus = (zero + one) / np.sqrt(2)
228+
minus = (zero - one) / np.sqrt(2)
229+
plus = backend.cast(plus, dtype=plus.dtype)
230+
minus = backend.cast(minus, dtype=minus.dtype)
231+
base = [plus, minus]
232+
233+
coeffs = np.random.rand(4)
234+
coeffs /= np.sum(coeffs)
235+
coeffs = backend.cast(coeffs, dtype=coeffs.dtype)
236+
237+
state = np.zeros((4, 4), dtype=complex)
238+
state = backend.cast(state, dtype=state.dtype)
239+
for k, coeff in enumerate(coeffs):
240+
bitstring = f"{k:0{2}b}"
241+
a, b = int(bitstring[0]), int(bitstring[1])
242+
ket = backend.np.kron(base[a], base[b])
243+
state = state + coeff * backend.np.outer(ket, ket.T)
244+
245+
_, S, _ = singular_value_decomposition(state, backend=backend)
246+
247+
S_sorted = backend.np.sort(S)
248+
coeffs_sorted = backend.np.sort(coeffs)
249+
if backend.name == "pytorch":
250+
S_sorted, coeffs_sorted = S_sorted[0], coeffs_sorted[0]
251+
252+
backend.assert_allclose(S_sorted, coeffs_sorted)

tests/test_quantum_info_superoperator_transformations.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import numpy as np
2-
import pytest
2+
import pytest # type: ignore
33

44
from qibo import matrices
55
from qibo.config import PRECISION_TOL
@@ -377,14 +377,22 @@ def test_choi_to_pauli(backend, normalize, order, pauli_order, test_superop):
377377
backend.assert_allclose(test_pauli, pauli_op, atol=PRECISION_TOL)
378378

379379

380+
@pytest.mark.parametrize("test_non_CP", [test_non_CP])
380381
@pytest.mark.parametrize("test_kraus_right", [test_kraus_right])
381382
@pytest.mark.parametrize("test_kraus_left", [test_kraus_left])
382383
@pytest.mark.parametrize("test_a1", [test_a1])
383384
@pytest.mark.parametrize("test_a0", [test_a0])
384385
@pytest.mark.parametrize("validate_cp", [False, True])
385386
@pytest.mark.parametrize("order", ["row", "column"])
386387
def test_choi_to_kraus(
387-
backend, order, validate_cp, test_a0, test_a1, test_kraus_left, test_kraus_right
388+
backend,
389+
order,
390+
validate_cp,
391+
test_a0,
392+
test_a1,
393+
test_kraus_left,
394+
test_kraus_right,
395+
test_non_CP,
388396
):
389397
axes = [1, 2] if order == "row" else [0, 3]
390398
test_choi = backend.cast(
@@ -425,6 +433,7 @@ def test_choi_to_kraus(
425433
backend.assert_allclose(evolution_a1, test_evolution_a1, atol=2 * PRECISION_TOL)
426434

427435
if validate_cp and order == "row":
436+
test_non_CP = backend.cast(test_non_CP, dtype=test_non_CP.dtype)
428437
(kraus_left, kraus_right), _ = choi_to_kraus(
429438
test_non_CP, order=order, validate_cp=validate_cp, backend=backend
430439
)

0 commit comments

Comments
 (0)