Source code for pyphi.numerics
# numerics.py
"""Tolerant scalar comparison of φ, Φ, and α values.
Floating-point results that are mathematically equal can differ by
roughly 1e-15 when computed through different code paths: distinct
algebraic routes to the same value produce different bit patterns, and
summation order is not associative. Integrated-information theory
treats ties between candidates (partitions, purviews, states, systems)
as meaningful, so detecting them requires comparison up to a tolerance
rather than exact equality.
These predicates are the only tolerant scalar comparisons in the
library. Values themselves are plain :class:`float`\\ s with exact
comparison semantics; tolerance applies where a comparison decides an
outcome. Selection among competing φ-objects goes through
:mod:`pyphi.resolve_ties`, which clusters candidates with :func:`eq`.
The tolerance is ``10**-precision`` with ``precision`` read from
``config.numerics.precision`` at call time (default 13).
"""
import math
import numpy as np
from .conf import config
def _epsilon() -> float:
return 10 ** (-int(config.numerics.precision))
[docs]
def eq(x: float, y: float) -> bool:
"""Return whether two values are equal up to ``config.numerics.precision``."""
epsilon = _epsilon()
return math.isclose(x, y, rel_tol=epsilon, abs_tol=epsilon)
[docs]
def lt(x: float, y: float) -> bool:
"""Return whether ``x`` is less than ``y`` beyond
``config.numerics.precision``: strictly less and not within the
tolerance of equal. For the reversed comparisons use ``lt(y, x)`` /
``le(y, x)``."""
# Need ``bool`` to cast from numpy to native Boolean
return not eq(x, y) and bool(x < y)
[docs]
def le(x: float, y: float) -> bool:
"""Return whether ``x`` is less than ``y`` or equal to it up to
``config.numerics.precision``."""
# Need ``bool`` to cast from numpy to native Boolean
return eq(x, y) or bool(x < y)
[docs]
def is_zero(x: float) -> bool:
"""Return whether ``x`` is zero up to ``config.numerics.precision``."""
return eq(x, 0.0)
[docs]
def is_positive(x: float) -> bool:
"""Return whether ``x`` is positive up to ``config.numerics.precision``."""
# Need ``bool`` to cast from numpy to native Boolean
return not eq(x, 0) and bool(x > 0)
[docs]
def is_nonpositive(x: float) -> bool:
"""Return whether ``x`` is nonpositive (exact)."""
# Need ``bool`` to cast from numpy to native Boolean
return bool(x <= 0)
[docs]
def eq_mask(array: np.ndarray, value: float) -> np.ndarray:
"""Return a boolean mask of the elements equal to ``value`` up to
``config.numerics.precision``.
Elementwise-equivalent to :func:`eq`. The comparison replicates
``math.isclose`` — a symmetric relative tolerance with an absolute
floor, and non-finite values equal only to themselves — which differs
from ``np.isclose``'s asymmetric additive form.
"""
epsilon = _epsilon()
a = np.asarray(array, dtype=float)
if not math.isfinite(value):
return a == value
tol = np.maximum(epsilon * np.maximum(np.abs(a), abs(value)), epsilon)
return np.isfinite(a) & (np.abs(a - value) <= tol)
[docs]
def positive_mask(array: np.ndarray) -> np.ndarray:
"""Return a boolean mask of the elements positive up to
``config.numerics.precision``.
Equivalent to applying :func:`is_positive` elementwise. Values within
the tolerance of zero (for example, the surprisal ``-log2(p)`` of a
probability that is 1 up to floating-point noise) are masked out.
"""
epsilon = _epsilon()
a = np.asarray(array)
return (a > 0) & ~np.isclose(a, 0.0, rtol=epsilon, atol=epsilon)
[docs]
def round_to_precision(x: float) -> float:
"""Return ``x`` rounded to ``config.numerics.precision`` decimal places."""
return round(x, int(config.numerics.precision))