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

Add calculate_singular_value_decompositon as a backend method #1473

Merged
merged 13 commits into from
Oct 8, 2024
6 changes: 6 additions & 0 deletions doc/source/api-reference/qibo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1981,6 +1981,12 @@ Matrix power
.. autofunction:: qibo.quantum_info.matrix_power


Singular value decomposition
""""""""""""""""""""""""""""

.. autofunction:: qibo.quantum_info.singular_value_decomposition


Quantum Networks
^^^^^^^^^^^^^^^^

Expand Down
5 changes: 5 additions & 0 deletions src/qibo/backends/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,11 @@ def calculate_matrix_power(
"""
raise_error(NotImplementedError)

@abc.abstractmethod
def calculate_singular_value_decomposition(self, matrix): # pragma: no cover
"""Calculate the Singular Value Decomposition of ``matrix``."""
raise_error(NotImplementedError)

@abc.abstractmethod
def calculate_hamiltonian_matrix_product(
self, matrix1, matrix2
Expand Down
3 changes: 3 additions & 0 deletions src/qibo/backends/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,9 @@ def calculate_matrix_power(self, matrix, power: Union[float, int]):
)
return fractional_matrix_power(matrix, power)

def calculate_singular_value_decomposition(self, matrix):
return self.np.linalg.svd(matrix)

# TODO: remove this method
def calculate_hamiltonian_matrix_product(self, matrix1, matrix2):
return matrix1 @ matrix2
Expand Down
4 changes: 2 additions & 2 deletions src/qibo/backends/pytorch.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class TorchMatrices(NumpyMatrices):
"""

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

super().__init__(dtype)
self.np = torch
Expand All @@ -38,7 +38,7 @@ def Unitary(self, u):
class PyTorchBackend(NumpyBackend):
def __init__(self):
super().__init__()
import torch # pylint: disable=import-outside-toplevel
import torch # pylint: disable=import-outside-toplevel # type: ignore

# Global variable to enable or disable gradient calculation
self.gradients = True
Expand Down
9 changes: 7 additions & 2 deletions src/qibo/backends/tensorflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TensorflowMatrices(NumpyMatrices):
def __init__(self, dtype):
super().__init__(dtype)
import tensorflow as tf # pylint: disable=import-error
import tensorflow.experimental.numpy as tnp # pylint: disable=import-error
import tensorflow.experimental.numpy as tnp # pylint: disable=import-error # type: ignore

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

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

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

def calculate_singular_value_decomposition(self, matrix):
# needed to unify order of return
S, U, V = self.tf.linalg.svd(matrix)
return U, S, self.np.conj(self.np.transpose(V))

def calculate_hamiltonian_matrix_product(self, matrix1, matrix2):
if self.is_sparse(matrix1) or self.is_sparse(matrix2):
raise_error(
Expand Down
26 changes: 26 additions & 0 deletions src/qibo/quantum_info/linalg_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,29 @@ def matrix_power(matrix, power: Union[float, int], backend=None):
backend = _check_backend(backend)

return backend.calculate_matrix_power(matrix, power)


def singular_value_decomposition(matrix, backend=None):
"""Calculate the Singular Value Decomposition (SVD) of ``matrix``.

Given an :math:`M \\times N` complex matrix :math:`A`, its SVD is given by

.. math:
A = U \\, S \\, V^{\\dagger} \\, ,

where :math:`U` and :math:`V` are, respectively, an :math:`M \\times M`
and an :math:`N \\times N` complex unitary matrices, and :math:`S` is an
:math:`M \\times N` diagonal matrix with the singular values of :math:`A`.

Args:
matrix (ndarray): matrix whose SVD to calculate.
backend (:class:`qibo.backends.abstract.Backend`, optional): backend
to be used in the execution. If ``None``, it uses
:class:`qibo.backends.GlobalBackend`. Defaults to ``None``.

Returns:
ndarray, ndarray, ndarray: Singular value decomposition of :math:`A`.
"""
backend = _check_backend(backend)

return backend.calculate_singular_value_decomposition(matrix)
11 changes: 7 additions & 4 deletions src/qibo/quantum_info/superoperator_transformations.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from qibo.gates.abstract import Gate
from qibo.gates.gates import Unitary
from qibo.gates.special import FusedGate
from qibo.quantum_info.linalg_operations import singular_value_decomposition


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

# using singular value decomposition because choi_super_op is non-CP
U, coefficients, V = np.linalg.svd(backend.to_numpy(choi_super_op))
U = np.transpose(U)
coefficients = np.sqrt(coefficients)
V = np.conj(V)
U, coefficients, V = singular_value_decomposition(
choi_super_op, backend=backend
)
U = U.T
coefficients = backend.np.sqrt(coefficients)
V = backend.np.conj(V)

kraus_left, kraus_right = [], []
for coeff, eigenvector_left, eigenvector_right in zip(coefficients, U, V):
Expand Down
32 changes: 32 additions & 0 deletions tests/test_quantum_info_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
matrix_power,
partial_trace,
partial_transpose,
singular_value_decomposition,
)
from qibo.quantum_info.metrics import purity
from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector
Expand Down Expand Up @@ -218,3 +219,34 @@ def test_matrix_power(backend, power):
float(backend.np.real(backend.np.trace(power))),
purity(state, backend=backend),
)


def test_singular_value_decomposition(backend):
zero = np.array([1, 0], dtype=complex)
one = np.array([0, 1], dtype=complex)
plus = (zero + one) / np.sqrt(2)
minus = (zero - one) / np.sqrt(2)
plus = backend.cast(plus, dtype=plus.dtype)
minus = backend.cast(minus, dtype=minus.dtype)
base = [plus, minus]

coeffs = np.random.rand(4)
coeffs /= np.sum(coeffs)
coeffs = backend.cast(coeffs, dtype=coeffs.dtype)

state = np.zeros((4, 4), dtype=complex)
state = backend.cast(state, dtype=state.dtype)
for k, coeff in enumerate(coeffs):
bitstring = f"{k:0{2}b}"
a, b = int(bitstring[0]), int(bitstring[1])
ket = backend.np.kron(base[a], base[b])
state = state + coeff * backend.np.outer(ket, ket.T)

_, S, _ = singular_value_decomposition(state, backend=backend)

S_sorted = backend.np.sort(S)
coeffs_sorted = backend.np.sort(coeffs)
if backend.name == "pytorch":
S_sorted, coeffs_sorted = S_sorted[0], coeffs_sorted[0]

backend.assert_allclose(S_sorted, coeffs_sorted)
13 changes: 11 additions & 2 deletions tests/test_quantum_info_superoperator_transformations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import numpy as np
import pytest
import pytest # type: ignore

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


@pytest.mark.parametrize("test_non_CP", [test_non_CP])
@pytest.mark.parametrize("test_kraus_right", [test_kraus_right])
@pytest.mark.parametrize("test_kraus_left", [test_kraus_left])
@pytest.mark.parametrize("test_a1", [test_a1])
@pytest.mark.parametrize("test_a0", [test_a0])
@pytest.mark.parametrize("validate_cp", [False, True])
@pytest.mark.parametrize("order", ["row", "column"])
def test_choi_to_kraus(
backend, order, validate_cp, test_a0, test_a1, test_kraus_left, test_kraus_right
backend,
order,
validate_cp,
test_a0,
test_a1,
test_kraus_left,
test_kraus_right,
test_non_CP,
):
axes = [1, 2] if order == "row" else [0, 3]
test_choi = backend.cast(
Expand Down Expand Up @@ -425,6 +433,7 @@ def test_choi_to_kraus(
backend.assert_allclose(evolution_a1, test_evolution_a1, atol=2 * PRECISION_TOL)

if validate_cp and order == "row":
test_non_CP = backend.cast(test_non_CP, dtype=test_non_CP.dtype)
(kraus_left, kraus_right), _ = choi_to_kraus(
test_non_CP, order=order, validate_cp=validate_cp, backend=backend
)
Expand Down