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 quantum_info.utils.total_variation_distance and improve documentation of quantum_info.utils #1175

Merged
merged 18 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/source/api-reference/qibo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2041,6 +2041,12 @@ Shannon entropy
.. autofunction:: qibo.quantum_info.shannon_entropy


Total Variation distance
""""""""""""""""""""""""

.. autofunction:: qibo.quantum_info.total_variation_distance


Hellinger distance
""""""""""""""""""

Expand Down
105 changes: 86 additions & 19 deletions src/qibo/quantum_info/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down Expand Up @@ -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}) \\, ,
Expand All @@ -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.")
Expand Down Expand Up @@ -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::
renatomello marked this conversation as resolved.
Show resolved Hide resolved
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=np.float64)
if isinstance(prob_dist_q, list):
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(
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.")
renatomello marked this conversation as resolved.
Show resolved Hide resolved

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))
renatomello marked this conversation as resolved.
Show resolved Hide resolved

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}} \\, \\|
Expand All @@ -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``.
Expand All @@ -220,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(
Expand Down Expand Up @@ -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::
renatomello marked this conversation as resolved.
Show resolved Hide resolved
(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
Expand Down
75 changes: 70 additions & 5 deletions tests/test_quantum_info_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
hellinger_fidelity,
pqc_integral,
shannon_entropy,
total_variation_distance,
)


Expand Down Expand Up @@ -130,7 +131,58 @@ def test_shannon_entropy(backend, base):
backend.assert_allclose(result, 1.0)


def test_hellinger(backend):
@pytest.mark.parametrize("kind", [None, list])
@pytest.mark.parametrize("validate", [False, True])
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)
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))
renatomello marked this conversation as resolved.
Show resolved Hide resolved

if kind is not None:
prob_p, prob_q = kind(prob_p), kind(prob_q)

distance = total_variation_distance(prob_p, prob_q, validate, backend=backend)

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, kind):
with pytest.raises(TypeError):
prob = np.random.rand(1, 2)
prob_q = np.random.rand(1, 5)
Expand Down Expand Up @@ -162,10 +214,23 @@ 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)
)

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)

assert distance == target
assert fidelity == (1 - target**2) ** 2


def test_haar_integral_errors(backend):
Expand Down
Loading