Repository: qiboteam/qibo

Issue:
Move `noise_model.hellinger_shot_error` to `quantum_info.utils`

Comments:

Patch:
diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst
index 3457ad0c0..4fcd781a9 100644
--- a/doc/source/api-reference/qibo.rst
+++ b/doc/source/api-reference/qibo.rst
@@ -2269,6 +2269,12 @@ Hellinger fidelity
 .. autofunction:: qibo.quantum_info.hellinger_fidelity
 
 
+Hellinger shot error
+""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.hellinger_fidelity
+
+
 Haar integral
 """""""""""""
 
diff --git a/src/qibo/noise_model.py b/src/qibo/noise_model.py
index 44311c266..b3be7e84b 100644
--- a/src/qibo/noise_model.py
+++ b/src/qibo/noise_model.py
@@ -1,7 +1,7 @@
 import numpy as np
 
 from qibo import gates, models
-from qibo.quantum_info import hellinger_fidelity
+from qibo.quantum_info.utils import hellinger_fidelity, hellinger_shot_error
 
 
 def noisy_circuit(circuit, params):
@@ -200,28 +200,6 @@ def freq_to_prob(freq):
     return prob
 
 
-def hellinger_shot_error(p, q, nshots):
-    """Hellinger fidelity error caused by using two probability distributions estimated using a finite number of shots.
-    It is calculated propagating the probability error of each state of the system. The complete formula is:
-    :math:`(1 - H^{2}(p, q))/\\sqrt{nshots} * \\sum_{i=1}^{n}(\\sqrt{p_i(1-q_i)}+\\sqrt{q_i(1-p_i)})`
-    where the sum is made all over the possible states and :math:`H(p, q)` is the Hellinger distance.
-
-       Args:
-           p (numpy.ndarray): (discrete) probability distribution :math:`p`.
-           q (numpy.ndarray): (discrete) probability distribution :math:`q`.
-           nshots (int): the number of shots we used to run the circuit to obtain :math:`p` and :math:`q`.
-
-       Returns:
-           (float): The Hellinger fidelity error.
-
-    """
-    hellinger_fid = hellinger_fidelity(p, q)
-    hellinger_fid_e = np.sqrt(hellinger_fid / nshots) * np.sum(
-        np.sqrt(q * (1 - p)) + np.sqrt(p * (1 - q))
-    )
-    return hellinger_fid_e
-
-
 def loss(parameters, *args):
     """The loss function used to be maximized in the fit method of the :class:`qibo.noise_model.CompositeNoiseModel`.
     It is the hellinger fidelity calculated between the probability distribution of the noise model and the experimental target distribution using the :func:`qibo.quantum_info.hellinger_fidelity`.
diff --git a/src/qibo/quantum_info/utils.py b/src/qibo/quantum_info/utils.py
index ec772168e..a162561fd 100644
--- a/src/qibo/quantum_info/utils.py
+++ b/src/qibo/quantum_info/utils.py
@@ -258,14 +258,13 @@ def hellinger_fidelity(prob_dist_p, prob_dist_q, validate: bool = False, backend
     .. math::
         (1 - H^{2}(p, q))^{2} \\, ,
 
-    where :math:`H(p, q)` is the Hellinger distance
-    (:func:`qibo.quantum_info.utils.hellinger_distance`).
+    where :math:`H(p, q)` is the :func:`qibo.quantum_info.utils.hellinger_distance`.
 
     Args:
         prob_dist_p (ndarray or list): discrete probability distribution :math:`p`.
         prob_dist_q (ndarray or list): discrete probability distribution :math:`q`.
-        validate (bool, optional): if True, checks if :math:`p` and :math:`q` are proper
-            probability distributions. Default: False.
+        validate (bool, optional): if ``True``, checks if :math:`p` and :math:`q` are proper
+            probability distributions. Defaults to ``False``.
         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``.
@@ -274,11 +273,62 @@ def hellinger_fidelity(prob_dist_p, prob_dist_q, validate: bool = False, backend
         (float): Hellinger fidelity.
 
     """
+    backend = _check_backend(backend)
+
     distance = hellinger_distance(prob_dist_p, prob_dist_q, validate, backend=backend)
 
     return (1 - distance**2) ** 2
 
 
+def hellinger_shot_error(
+    prob_dist_p, prob_dist_q, nshots: int, validate: bool = False, backend=None
+):
+    """Calculates the Hellinger fidelity error between two discrete probability distributions estimated from finite statistics.
+
+    It is calculated propagating the probability error of each state of the system.
+    The complete formula is:
+
+    .. math::
+        \\frac{1 - H^{2}(p, q)}{\\sqrt{nshots}} \\, \\sum_{k} \\,
+            \\left(\\sqrt{p_{k} \\, (1 - q_{k})} + \\sqrt{q_{k} \\, (1 - p_{k})}\\right)
+
+    where :math:`H(p, q)` is the :func:`qibo.quantum_info.utils.hellinger_distance`,
+    and :math:`1 - H^{2}(p, q)` is the square root of the
+    :func:`qibo.quantum_info.utils.hellinger_fidelity`.
+
+    Args:
+        prob_dist_p (ndarray or list): discrete probability distribution :math:`p`.
+        prob_dist_q (ndarray or list): discrete probability distribution :math:`q`.
+        nshots (int): number of shots we used to run the circuit to obtain :math:`p` and :math:`q`.
+        validate (bool, optional): if ``True``, checks if :math:`p` and :math:`q` are proper
+            probability distributions. Defaults to ``False``.
+        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:
+        (float): Hellinger fidelity error.
+
+    """
+    backend = _check_backend(backend)
+
+    if isinstance(prob_dist_p, list):
+        prob_dist_p = backend.cast(prob_dist_p, dtype=np.float64)
+
+    if isinstance(prob_dist_q, list):
+        prob_dist_q = backend.cast(prob_dist_q, dtype=np.float64)
+
+    hellinger_error = hellinger_fidelity(
+        prob_dist_p, prob_dist_q, validate=validate, backend=backend
+    )
+    hellinger_error = np.sqrt(hellinger_error / nshots) * np.sum(
+        np.sqrt(prob_dist_q * (1 - prob_dist_p))
+        + np.sqrt(prob_dist_p * (1 - prob_dist_q))
+    )
+
+    return hellinger_error
+
+
 def haar_integral(
     nqubits: int,
     power_t: int,


Test patch:
diff --git a/tests/test_quantum_info_utils.py b/tests/test_quantum_info_utils.py
index 049f80027..0d845cf2f 100644
--- a/tests/test_quantum_info_utils.py
+++ b/tests/test_quantum_info_utils.py
@@ -4,9 +4,10 @@ from re import finditer
 import numpy as np
 import pytest
 
-from qibo import Circuit, matrices
+from qibo import Circuit, gates, matrices
 from qibo.config import PRECISION_TOL
 from qibo.quantum_info.metrics import fidelity
+from qibo.quantum_info.random_ensembles import random_clifford
 from qibo.quantum_info.utils import (
     haar_integral,
     hadamard_transform,
@@ -14,6 +15,7 @@ from qibo.quantum_info.utils import (
     hamming_weight,
     hellinger_distance,
     hellinger_fidelity,
+    hellinger_shot_error,
     pqc_integral,
 )
 
@@ -175,6 +177,34 @@ def test_hellinger(backend, validate, kind):
     assert fidelity == (1 - target**2) ** 2
 
 
+@pytest.mark.parametrize("kind", [None, list])
+@pytest.mark.parametrize("validate", [False, True])
+def test_hellinger_shot_error(backend, validate, kind):
+    nqubits, nshots = 5, 1000
+
+    circuit = random_clifford(nqubits, seed=1, backend=backend)
+    circuit.add(gates.M(qubit) for qubit in range(nqubits))
+
+    circuit_2 = random_clifford(nqubits, seed=2, backend=backend)
+    circuit_2.add(gates.M(qubit) for qubit in range(nqubits))
+
+    prob_dist_p = backend.execute_circuit(circuit, nshots=nshots).probabilities()
+    prob_dist_q = backend.execute_circuit(circuit_2, nshots=nshots).probabilities()
+
+    if kind is not None:
+        prob_dist_p = kind(prob_dist_p)
+        prob_dist_q = kind(prob_dist_q)
+
+    hellinger_error = hellinger_shot_error(
+        prob_dist_p, prob_dist_q, nshots, validate=validate, backend=backend
+    )
+    hellinger_fid = hellinger_fidelity(
+        prob_dist_p, prob_dist_q, validate=validate, backend=backend
+    )
+
+    assert 2 * hellinger_error < hellinger_fid
+
+
 def test_haar_integral_errors(backend):
     with pytest.raises(TypeError):
         nqubits, power_t, samples = 0.5, 2, 10