From 9d3c10ba70d4c06a6ea4540ac6d4178737478421 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 30 Jan 2024 10:11:03 +0400 Subject: [PATCH 01/12] `total_variation_distance` --- src/qibo/quantum_info/utils.py | 99 ++++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 16 deletions(-) diff --git a/src/qibo/quantum_info/utils.py b/src/qibo/quantum_info/utils.py index eb5a447d8d..12808dcb31 100644 --- a/src/qibo/quantum_info/utils.py +++ b/src/qibo/quantum_info/utils.py @@ -17,7 +17,7 @@ def hamming_weight(bitstring, return_indexes: bool = False): """Calculates the Hamming weight of a bitstring. Args: - bitstring (int or str or tuple or list or ndarray): bitstring to calculate the + bitstring (int or str or tuple or list or ndarray): bitstring to Calculates the weight, either in binary or integer representation. return_indexes (bool, optional): If ``True``, returns the indexes of the non-zero elements. Defaults to ``False``. @@ -128,7 +128,7 @@ def hadamard_transform(array, implementation: str = "fast", backend=None): def shannon_entropy(probability_array, base: float = 2, backend=None): - """Calculate the Shannon entropy of a probability array :math:`\\mathbf{p}`, which is given by + """Calculates the Shannon entropy of a probability array :math:`\\mathbf{p}`, which is given by .. math:: H(\\mathbf{p}) = - \\sum_{k = 0}^{d^{2} - 1} \\, p_{k} \\, \\log_{b}(p_{k}) \\, , @@ -193,10 +193,72 @@ def shannon_entropy(probability_array, base: float = 2, backend=None): return complex(entropy).real +def total_variation_distance( + prob_dist_p, prob_dist_q, validate: bool = False, backend=None +): + """Calculates the Total Variation (TV) distance between two discrete probability distributions. + + For probabilities :math:`\\mathbf{p}` and :math:`\\mathbf{q}`, it is defined as + + ..math:: + d_{\\text{TV}}(\\mathbf{p} \\, , \\, \\mathbf{q}) = \\frac{1}{2} + \\, \\| \\mathbf{p} - \\mathbf{q} \\|_{1} \\, , + + where :math:`\\| \\cdot \\|_{1}` is the :math:`\\mathcal{l}_{1}`-norm. + + 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. 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: Total variation distance between :math:`\\mathbf{p}` and :math:`\\mathbf{q}`. + """ + if backend is None: # pragma: no cover + backend = GlobalBackend() + + if isinstance(prob_dist_p, list): + prob_dist_p = backend.cast(prob_dist_p, dtype=float) + if isinstance(prob_dist_q, list): + prob_dist_q = backend.cast(prob_dist_q, dtype=float) + + if (len(prob_dist_p.shape) != 1) or (len(prob_dist_q.shape) != 1): + raise_error( + TypeError, + "Probability arrays must have dims (k,) but have " + + f"dims {prob_dist_p.shape} and {prob_dist_q.shape}.", + ) + + if (len(prob_dist_p) == 0) or (len(prob_dist_q) == 0): + raise_error(TypeError, "At least one of the arrays is empty.") + + if validate: + if (any(prob_dist_p < 0) or any(prob_dist_p > 1.0)) or ( + any(prob_dist_q < 0) or any(prob_dist_q > 1.0) + ): + raise_error( + ValueError, + "All elements of the probability array must be between 0. and 1..", + ) + if np.abs(np.sum(prob_dist_p) - 1.0) > PRECISION_TOL: + raise_error(ValueError, "First probability array must sum to 1.") + + if np.abs(np.sum(prob_dist_q) - 1.0) > PRECISION_TOL: + raise_error(ValueError, "Second probability array must sum to 1.") + + total_variation = (1 / 2) * np.sum(np.abs(prob_dist_p - prob_dist_q)) + + return total_variation + + def hellinger_distance(prob_dist_p, prob_dist_q, validate: bool = False, backend=None): - """Calculate the Hellinger distance :math:`H(p, q)` between - two discrete probability distributions, :math:`\\mathbf{p}` and :math:`\\mathbf{q}`. - It is defined as + """Calculates the Hellinger distance :math:`H` between two discrete probability distributions. + + For probabilities :math:`\\mathbf{p}` and :math:`\\mathbf{q}`, it is defined as .. math:: H(\\mathbf{p} \\, , \\, \\mathbf{q}) = \\frac{1}{\\sqrt{2}} \\, \\| @@ -205,10 +267,10 @@ def hellinger_distance(prob_dist_p, prob_dist_q, validate: bool = False, backend where :math:`\\|\\cdot\\|_{2}` is the Euclidean norm. Args: - prob_dist_p: (discrete) probability distribution :math:`p`. - prob_dist_q: (discrete) probability distribution :math:`q`. - validate (bool): if True, checks if :math:`p` and :math:`q` are proper - probability distributions. Default: False. + 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. 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``. @@ -256,15 +318,20 @@ def hellinger_distance(prob_dist_p, prob_dist_q, validate: bool = False, backend def hellinger_fidelity(prob_dist_p, prob_dist_q, validate: bool = False, backend=None): - """Calculate the Hellinger fidelity between two discrete - probability distributions, :math:`p` and :math:`q`. The fidelity is - defined as :math:`(1 - H^{2}(p, q))^{2}`, where :math:`H(p, q)` - is the Hellinger distance. + """Calculates the Hellinger fidelity between two discrete probability distributions. + + For probabilities :math:`p` and :math:`q`, the fidelity is defined as + + ..math:: + (1 - H^{2}(p, q))^{2} \\, , + + where :math:`H(p, q)` is the Hellinger distance + (:func:`qibo.quantum_info.utils.hellinger_distance`). Args: - prob_dist_p: (discrete) probability distribution :math:`p`. - prob_dist_q: (discrete) probability distribution :math:`q`. - validate (bool): if True, checks if :math:`p` and :math:`q` are proper + 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. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses From 0fa60cfab4e241738fccaff1870c3b56ee67e485 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 30 Jan 2024 10:11:11 +0400 Subject: [PATCH 02/12] tests --- tests/test_quantum_info_utils.py | 66 +++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/tests/test_quantum_info_utils.py b/tests/test_quantum_info_utils.py index f873078bb4..743aebe20c 100644 --- a/tests/test_quantum_info_utils.py +++ b/tests/test_quantum_info_utils.py @@ -15,6 +15,7 @@ hellinger_fidelity, pqc_integral, shannon_entropy, + total_variation_distance, ) @@ -130,7 +131,52 @@ def test_shannon_entropy(backend, base): backend.assert_allclose(result, 1.0) -def test_hellinger(backend): +@pytest.mark.parametrize("validate", [False, True]) +def test_total_variation_distance(backend, validate): + with pytest.raises(TypeError): + prob = np.random.rand(1, 2) + prob_q = np.random.rand(1, 5) + prob = backend.cast(prob, dtype=prob.dtype) + prob_q = backend.cast(prob_q, dtype=prob_q.dtype) + test = total_variation_distance(prob, prob_q, backend=backend) + with pytest.raises(TypeError): + prob = np.random.rand(1, 2)[0] + prob_q = np.array([]) + prob = backend.cast(prob, dtype=prob.dtype) + prob_q = backend.cast(prob_q, dtype=prob_q.dtype) + test = total_variation_distance(prob, prob_q, backend=backend) + with pytest.raises(ValueError): + prob = np.array([-1, 2.0]) + prob_q = np.random.rand(1, 5)[0] + prob = backend.cast(prob, dtype=prob.dtype) + prob_q = backend.cast(prob_q, dtype=prob_q.dtype) + test = total_variation_distance(prob, prob_q, validate=True, backend=backend) + with pytest.raises(ValueError): + prob = np.random.rand(1, 2)[0] + prob_q = np.array([1.0, 0.0]) + prob = backend.cast(prob, dtype=prob.dtype) + prob_q = backend.cast(prob_q, dtype=prob_q.dtype) + test = total_variation_distance(prob, prob_q, validate=True, backend=backend) + with pytest.raises(ValueError): + prob = np.array([1.0, 0.0]) + prob_q = np.random.rand(1, 2)[0] + prob = backend.cast(prob, dtype=prob.dtype) + prob_q = backend.cast(prob_q, dtype=prob_q.dtype) + test = total_variation_distance(prob, prob_q, validate=True, backend=backend) + + prob_p = np.random.rand(10) + prob_q = np.random.rand(10) + prob_p /= np.sum(prob_p) + prob_q /= np.sum(prob_q) + + target = (1 / 2) * np.sum(np.abs(prob_p - prob_q)) + distance = total_variation_distance(prob_p, prob_q, validate, backend=backend) + + assert distance == target + + +@pytest.mark.parametrize("validate", [False, True]) +def test_hellinger(backend, validate): with pytest.raises(TypeError): prob = np.random.rand(1, 2) prob_q = np.random.rand(1, 5) @@ -162,10 +208,20 @@ def test_hellinger(backend): prob_q = backend.cast(prob_q, dtype=prob_q.dtype) test = hellinger_distance(prob, prob_q, validate=True, backend=backend) - prob = [1.0, 0.0] - prob_q = [1.0, 0.0] - backend.assert_allclose(hellinger_distance(prob, prob_q, backend=backend), 0.0) - backend.assert_allclose(hellinger_fidelity(prob, prob_q, backend=backend), 1.0) + prob_p = np.random.rand(10) + prob_q = np.random.rand(10) + prob_p /= np.sum(prob_p) + prob_q /= np.sum(prob_q) + + target = float( + backend.calculate_norm(np.sqrt(prob_p) - np.sqrt(prob_q)) / np.sqrt(2) + ) + + distance = hellinger_distance(prob_p, prob_q, validate=validate, backend=backend) + fidelity = hellinger_fidelity(prob_p, prob_q, validate=validate, backend=backend) + + assert distance == target + assert fidelity == (1 - target**2) ** 2 def test_haar_integral_errors(backend): From 0532c73e7bf739c4de316d28a1b257aa26079487 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 30 Jan 2024 10:17:23 +0400 Subject: [PATCH 03/12] api reference --- doc/source/api-reference/qibo.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index 2f8e7d9d92..e51015cfe7 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -2041,6 +2041,12 @@ Shannon entropy .. autofunction:: qibo.quantum_info.shannon_entropy +Total Variation distance +"""""""""""""""""""""""" + +.. autofunction:: qibo.quantum_info.total_variation_distance + + Hellinger distance """""""""""""""""" From 8e7f4c8572e1c1f76af5be7e6f4d81f4f1c29019 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 30 Jan 2024 10:45:06 +0400 Subject: [PATCH 04/12] fix coverage --- tests/test_quantum_info_utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_quantum_info_utils.py b/tests/test_quantum_info_utils.py index 743aebe20c..e25dc49fa6 100644 --- a/tests/test_quantum_info_utils.py +++ b/tests/test_quantum_info_utils.py @@ -131,8 +131,9 @@ def test_shannon_entropy(backend, base): backend.assert_allclose(result, 1.0) +@pytest.mark.parametrize("kind", [None, list]) @pytest.mark.parametrize("validate", [False, True]) -def test_total_variation_distance(backend, validate): +def test_total_variation_distance(backend, validate, kind): with pytest.raises(TypeError): prob = np.random.rand(1, 2) prob_q = np.random.rand(1, 5) @@ -170,6 +171,10 @@ def test_total_variation_distance(backend, validate): prob_q /= np.sum(prob_q) target = (1 / 2) * np.sum(np.abs(prob_p - prob_q)) + + if kind is not None: + prob_p, prob_q = kind(prob_q), kind(prob_q) + distance = total_variation_distance(prob_p, prob_q, validate, backend=backend) assert distance == target From bae77b15f6d97db67221067934e000a12882794d Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 30 Jan 2024 12:23:52 +0400 Subject: [PATCH 05/12] fix test --- tests/test_quantum_info_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_quantum_info_utils.py b/tests/test_quantum_info_utils.py index e25dc49fa6..8cbb239ce5 100644 --- a/tests/test_quantum_info_utils.py +++ b/tests/test_quantum_info_utils.py @@ -173,7 +173,7 @@ def test_total_variation_distance(backend, validate, kind): target = (1 / 2) * np.sum(np.abs(prob_p - prob_q)) if kind is not None: - prob_p, prob_q = kind(prob_q), kind(prob_q) + prob_p, prob_q = kind(prob_p), kind(prob_q) distance = total_variation_distance(prob_p, prob_q, validate, backend=backend) From c473ee911dcc477fd6e8145571dbfa7fac5a90d9 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 30 Jan 2024 12:46:03 +0400 Subject: [PATCH 06/12] fix test precision --- tests/test_quantum_info_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_quantum_info_utils.py b/tests/test_quantum_info_utils.py index 8cbb239ce5..85ce559c6c 100644 --- a/tests/test_quantum_info_utils.py +++ b/tests/test_quantum_info_utils.py @@ -177,7 +177,7 @@ def test_total_variation_distance(backend, validate, kind): distance = total_variation_distance(prob_p, prob_q, validate, backend=backend) - assert distance == target + backend.assert_allclose(distance, target, atol=1e-5) @pytest.mark.parametrize("validate", [False, True]) From b3f08c58f41fb16d153fccf111c836a0475f15d3 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 30 Jan 2024 14:39:04 +0400 Subject: [PATCH 07/12] fix `tensorflow` precision bug --- src/qibo/quantum_info/utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/qibo/quantum_info/utils.py b/src/qibo/quantum_info/utils.py index 12808dcb31..2190c5cb2f 100644 --- a/src/qibo/quantum_info/utils.py +++ b/src/qibo/quantum_info/utils.py @@ -151,7 +151,7 @@ def shannon_entropy(probability_array, base: float = 2, backend=None): backend = GlobalBackend() if isinstance(probability_array, list): - probability_array = backend.cast(probability_array, dtype=float) + probability_array = backend.cast(probability_array, dtype=np.float64) if base <= 0: raise_error(ValueError, "log base must be non-negative.") @@ -222,9 +222,9 @@ def total_variation_distance( backend = GlobalBackend() if isinstance(prob_dist_p, list): - prob_dist_p = backend.cast(prob_dist_p, dtype=float) + 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=float) + prob_dist_q = backend.cast(prob_dist_q, dtype=np.float64) if (len(prob_dist_p.shape) != 1) or (len(prob_dist_q.shape) != 1): raise_error( @@ -282,9 +282,9 @@ def hellinger_distance(prob_dist_p, prob_dist_q, validate: bool = False, backend backend = GlobalBackend() if isinstance(prob_dist_p, list): - prob_dist_p = backend.cast(prob_dist_p, dtype=float) + 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=float) + prob_dist_q = backend.cast(prob_dist_q, dtype=np.float64) if (len(prob_dist_p.shape) != 1) or (len(prob_dist_q.shape) != 1): raise_error( From 28aaa96a9bb234bd4578e67eeb5efee885c1d0a1 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 30 Jan 2024 15:05:48 +0400 Subject: [PATCH 08/12] fix coverage --- tests/test_quantum_info_utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_quantum_info_utils.py b/tests/test_quantum_info_utils.py index 85ce559c6c..3c5f79b778 100644 --- a/tests/test_quantum_info_utils.py +++ b/tests/test_quantum_info_utils.py @@ -180,8 +180,9 @@ def test_total_variation_distance(backend, validate, kind): backend.assert_allclose(distance, target, atol=1e-5) +@pytest.mark.parametrize("kind", [None, list]) @pytest.mark.parametrize("validate", [False, True]) -def test_hellinger(backend, validate): +def test_hellinger(backend, validate, kind): with pytest.raises(TypeError): prob = np.random.rand(1, 2) prob_q = np.random.rand(1, 5) @@ -222,6 +223,9 @@ def test_hellinger(backend, validate): backend.calculate_norm(np.sqrt(prob_p) - np.sqrt(prob_q)) / np.sqrt(2) ) + if kind is not None: + prob_p, prob_q = list(prob_p), list(prob_q) + distance = hellinger_distance(prob_p, prob_q, validate=validate, backend=backend) fidelity = hellinger_fidelity(prob_p, prob_q, validate=validate, backend=backend) From baba3cffb223e88ea7c736e31c35c3e2a6e58f49 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 31 Jan 2024 08:29:06 +0000 Subject: [PATCH 09/12] Update src/qibo/quantum_info/utils.py Co-authored-by: BrunoLiegiBastonLiegi <45011234+BrunoLiegiBastonLiegi@users.noreply.github.com> --- src/qibo/quantum_info/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/utils.py b/src/qibo/quantum_info/utils.py index 2190c5cb2f..2cf2d19889 100644 --- a/src/qibo/quantum_info/utils.py +++ b/src/qibo/quantum_info/utils.py @@ -250,7 +250,7 @@ def total_variation_distance( if np.abs(np.sum(prob_dist_q) - 1.0) > PRECISION_TOL: raise_error(ValueError, "Second probability array must sum to 1.") - total_variation = (1 / 2) * np.sum(np.abs(prob_dist_p - prob_dist_q)) + total_variation = 0.5 * np.sum(np.abs(prob_dist_p - prob_dist_q)) return total_variation From 050399e401adce889fcf3b172a83d9cffc825ff1 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 31 Jan 2024 08:29:13 +0000 Subject: [PATCH 10/12] Update tests/test_quantum_info_utils.py Co-authored-by: BrunoLiegiBastonLiegi <45011234+BrunoLiegiBastonLiegi@users.noreply.github.com> --- tests/test_quantum_info_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_quantum_info_utils.py b/tests/test_quantum_info_utils.py index 3c5f79b778..e24e50e6ed 100644 --- a/tests/test_quantum_info_utils.py +++ b/tests/test_quantum_info_utils.py @@ -170,7 +170,7 @@ def test_total_variation_distance(backend, validate, kind): prob_p /= np.sum(prob_p) prob_q /= np.sum(prob_q) - target = (1 / 2) * np.sum(np.abs(prob_p - prob_q)) + target = 0.5 * np.sum(np.abs(prob_p - prob_q)) if kind is not None: prob_p, prob_q = kind(prob_p), kind(prob_q) From 521120357ecf7a8da2be3ac6260381efcfdc198b Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Sat, 3 Feb 2024 04:14:27 +0000 Subject: [PATCH 11/12] Update src/qibo/quantum_info/utils.py Co-authored-by: Alejandro Sopena <44305203+AlejandroSopena@users.noreply.github.com> --- src/qibo/quantum_info/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/utils.py b/src/qibo/quantum_info/utils.py index bbbd7c1674..a307ea1c72 100644 --- a/src/qibo/quantum_info/utils.py +++ b/src/qibo/quantum_info/utils.py @@ -263,7 +263,7 @@ def total_variation_distance( For probabilities :math:`\\mathbf{p}` and :math:`\\mathbf{q}`, it is defined as - ..math:: + .. math:: d_{\\text{TV}}(\\mathbf{p} \\, , \\, \\mathbf{q}) = \\frac{1}{2} \\, \\| \\mathbf{p} - \\mathbf{q} \\|_{1} \\, , From 73299a76ee0ab662fff85caa814d22dc613c3e55 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Sat, 3 Feb 2024 04:14:33 +0000 Subject: [PATCH 12/12] Update src/qibo/quantum_info/utils.py Co-authored-by: Alejandro Sopena <44305203+AlejandroSopena@users.noreply.github.com> --- src/qibo/quantum_info/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/utils.py b/src/qibo/quantum_info/utils.py index a307ea1c72..24a6998569 100644 --- a/src/qibo/quantum_info/utils.py +++ b/src/qibo/quantum_info/utils.py @@ -385,7 +385,7 @@ def hellinger_fidelity(prob_dist_p, prob_dist_q, validate: bool = False, backend For probabilities :math:`p` and :math:`q`, the fidelity is defined as - ..math:: + .. math:: (1 - H^{2}(p, q))^{2} \\, , where :math:`H(p, q)` is the Hellinger distance