Repository: qiboteam/qibo

Issue:
Add `bures_angle` and `bures_distance` to `quantum_info.metrics`

Discussion:

Patch:
diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst
index 3101731a0..3e2cdf047 100644
--- a/doc/source/api-reference/qibo.rst
+++ b/doc/source/api-reference/qibo.rst
@@ -1196,6 +1196,18 @@ Fidelity
 .. autofunction:: qibo.quantum_info.fidelity
 
 
+Bures angle
+"""""""""""
+
+.. autofunction:: qibo.quantum_info.bures_angle
+
+
+Bures distance
+""""""""""""""
+
+.. autofunction:: qibo.quantum_info.bures_distance
+
+
 Process fidelity
 """"""""""""""""
 
diff --git a/src/qibo/quantum_info/metrics.py b/src/qibo/quantum_info/metrics.py
index 816c1a16b..d301e7ae8 100644
--- a/src/qibo/quantum_info/metrics.py
+++ b/src/qibo/quantum_info/metrics.py
@@ -8,7 +8,7 @@ def purity(state):
     """Purity of a quantum state :math:`\\rho`, which is given by :math:`\\text{Tr}(\\rho^{2})`.
 
     Args:
-        state: statevector or density matrix.
+        state (ndarray): statevector or density matrix.
     Returns:
         float: Purity of quantum state :math:`\\rho`.
     """
@@ -41,7 +41,7 @@ def entropy(state, base: float = 2, validate: bool = False, backend=None):
         S(\\rho) = - \\text{Tr}\\left[\\rho \\, \\log(\\rho)\\right]
 
     Args:
-        state: state vector or density matrix.
+        state (ndarray): statevector or density matrix.
         base (float, optional): the base of the log. Default: 2.
         validate (bool, optional): if ``True``, checks if ``state`` is Hermitian. If ``False``,
             it assumes ``state`` is Hermitian . Default: ``False``.
@@ -120,8 +120,8 @@ def trace_distance(state, target, validate: bool = False, backend=None):
     where :math:`\\|\\cdot\\|_{1}` is the Schatten 1-norm.
 
     Args:
-        state: state vector or density matrix.
-        target: state vector or density matrix.
+        state (ndarray): statevector or density matrix.
+        target (ndarray): statevector or density matrix.
         validate (bool, optional): if ``True``, checks if :math:`\\rho - \\sigma` is Hermitian.
             If ``False``, it assumes the difference is Hermitian. Default: ``False``.
         backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
@@ -186,8 +186,8 @@ def hilbert_schmidt_distance(state, target):
             \\text{Tr}\\left((\\rho - \\sigma)^{2}\\right)
 
     Args:
-        state: state vector or density matrix.
-        target: state vector or density matrix.
+        state (ndarray): statevector or density matrix.
+        target (ndarray): statevector or density matrix.
 
     Returns:
         float: Hilbert-Schmidt distance between state :math:`\\rho` and target :math:`\\sigma`.
@@ -228,10 +228,10 @@ def fidelity(state, target, validate: bool = False):
     :math:`\\sigma` is assumed to be pure.
 
     Args:
-        state: state vector or density matrix.
-        target: state vector or density matrix.
+        state (ndarray): statevector or density matrix.
+        target (ndarray): statevector or density matrix.
         validate (bool, optional): if ``True``, checks if one of the
-            input states is pure. Default is ``False``.
+            input states is pure. Defaults to ``False``.
 
     Returns:
         float: Fidelity between state :math:`\\rho` and target :math:`\\sigma`.
@@ -261,8 +261,8 @@ def fidelity(state, target, validate: bool = False):
         ):
             raise_error(
                 ValueError,
-                f"Neither state is pure. Purity state: {purity_state} , "
-                + f"Purity target: {purity_target}.",
+                f"Neither state is pure. Purity state (ndarray): {purity_state} , "
+                + f"Purity target (ndarray): {purity_target}.",
             )
 
     if len(state.shape) == 1 and len(target.shape) == 1:
@@ -275,6 +275,54 @@ def fidelity(state, target, validate: bool = False):
     return fid
 
 
+def bures_angle(state, target, validate: bool = False):
+    """Calculates the Bures angle :math:`D_{A}` between a ``state`` :math:`\\rho`
+    and a ``target`` state :math:`\\sigma`. This is given by
+
+    .. math::
+        D_{A}(\\rho, \\, \\sigma) = \\text{arccos}\\left(\\sqrt{F(\\rho, \\, \\sigma)}\\right) \\, ,
+
+    where :math:`F(\\rho, \\sigma)` is the :func:`qibo.quantum_info.fidelity`
+    between `state` and `target`.
+
+    Args:
+        state (ndarray): statevector or density matrix.
+        target (ndarray): statevector or density matrix.
+        validate (bool, optional): if ``True``, checks if one of the
+            input states is pure. Defaults to ``False``.
+
+    Returns:
+        float: Bures angle between ``state`` and ``target``.
+    """
+    angle = np.arccos(np.sqrt(fidelity(state, target, validate=validate)))
+
+    return angle
+
+
+def bures_distance(state, target, validate: bool = False):
+    """Calculates the Bures distance :math:`D_{B}` between a ``state`` :math:`\\rho`
+    and a ``target`` state :math:`\\sigma`. This is given by
+
+    .. math::
+        D_{B}(\\rho, \\, \\sigma) = \\sqrt{2 \\, (1 - \\sqrt{F(\\rho, \\, \\sigma)})}
+
+    where :math:`F(\\rho, \\sigma)` is the :func:`qibo.quantum_info.fidelity`
+    between `state` and `target`.
+
+    Args:
+        state (ndarray): statevector or density matrix.
+        target (ndarray): statevector or density matrix.
+        validate (bool, optional): if ``True``, checks if one of the
+            input states is pure. Defaults to ``False``.
+
+    Returns:
+        float: Bures distance between ``state`` and ``target``.
+    """
+    distance = np.sqrt(2 * (1 - np.sqrt(fidelity(state, target, validate=validate))))
+
+    return distance
+
+
 def process_fidelity(channel, target=None, validate: bool = False, backend=None):
     """Process fidelity between two quantum channels (when at least one channel is` unitary),
 
@@ -349,7 +397,7 @@ def average_gate_fidelity(channel, target=None, backend=None):
     Args:
         channel: quantum channel :math:`\\mathcal{E}`.
         target (optional): quantum channel :math:`\\mathcal{U}`.
-            If ``None``, target is the Identity channel. Default is ``None``.
+            If ``None``, target is the Identity channel. Defaults to ``None``.
         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``.
@@ -380,7 +428,7 @@ def gate_error(channel, target=None, backend=None):
     Args:
         channel: quantum channel :math:`\\mathcal{E}`.
         target (optional): quantum channel :math:`\\mathcal{U}`. If ``None``,
-            target is the Identity channel. Default is ``None``.
+            target is the Identity channel. Defaults to ``None``.
         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``.


Test patch:
diff --git a/tests/test_quantum_info_basis.py b/tests/test_quantum_info_basis.py
index 7a4159f82..9c2aee0cc 100644
--- a/tests/test_quantum_info_basis.py
+++ b/tests/test_quantum_info_basis.py
@@ -42,15 +42,15 @@ def test_pauli_basis(
         basis_test = [reduce(np.kron, matrices) for matrices in basis_test]
 
     if vectorize:
-        basis_test = [vectorization(matrix, order=order) for matrix in basis_test]
+        basis_test = [
+            vectorization(matrix, order=order, backend=backend) for matrix in basis_test
+        ]
 
-    basis_test = np.array(basis_test)
+    basis_test = backend.cast(basis_test)
 
     if normalize:
         basis_test /= np.sqrt(2**nqubits)
 
-    basis_test = backend.cast(basis_test, dtype=basis_test.dtype)
-
     if vectorize and sparse:
         elements, indexes = [], []
         for row in basis_test:
diff --git a/tests/test_quantum_info_metrics.py b/tests/test_quantum_info_metrics.py
index 83b4ce212..4084f642a 100644
--- a/tests/test_quantum_info_metrics.py
+++ b/tests/test_quantum_info_metrics.py
@@ -172,7 +172,7 @@ def test_hilbert_schmidt_distance(backend):
     backend.assert_allclose(hilbert_schmidt_distance(state, target), 2.0)
 
 
-def test_fidelity(backend):
+def test_fidelity_and_bures(backend):
     with pytest.raises(TypeError):
         state = np.random.rand(2, 2)
         target = np.random.rand(4, 4)
@@ -197,18 +197,24 @@ def test_fidelity(backend):
     state = backend.cast(state, dtype=state.dtype)
     target = backend.cast(target, dtype=target.dtype)
     backend.assert_allclose(fidelity(state, target), 1.0)
+    backend.assert_allclose(bures_angle(state, target), 0.0)
+    backend.assert_allclose(bures_distance(state, target), 0.0)
 
     state = np.outer(np.conj(state), state)
     target = np.outer(np.conj(target), target)
     state = backend.cast(state, dtype=state.dtype)
     target = backend.cast(target, dtype=target.dtype)
     backend.assert_allclose(fidelity(state, target), 1.0)
+    backend.assert_allclose(bures_angle(state, target), 0.0)
+    backend.assert_allclose(bures_distance(state, target), 0.0)
 
     state = np.array([0.0, 1.0, 0.0, 0.0])
     target = np.array([0.0, 0.0, 0.0, 1.0])
     state = backend.cast(state, dtype=state.dtype)
     target = backend.cast(target, dtype=target.dtype)
     backend.assert_allclose(fidelity(state, target), 0.0)
+    backend.assert_allclose(bures_angle(state, target), np.arccos(0.0))
+    backend.assert_allclose(bures_distance(state, target), np.sqrt(2))
 
 
 def test_process_fidelity(backend):