Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ dev-pytest = [
"pytest-mock == 3.14.0",
"pytest-asyncio == 0.23.7",
"async-solipsism == 0.6",
"hypothesis == 6.103.2",
]
dev = [
"frequenz-core[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
Expand Down
25 changes: 25 additions & 0 deletions src/frequenz/core/math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# License: MIT
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH

"""Internal math tools."""

import math


def is_close_to_zero(value: float, abs_tol: float = 1e-9) -> bool:
"""Check if a floating point value is close to zero.

A value of 1e-9 is a commonly used absolute tolerance to balance precision
and robustness for floating-point numbers comparisons close to zero. Note
that this is also the default value for the relative tolerance.
For more technical details, see https://peps.python.org/pep-0485/#behavior-near-zero

Args:
value: the floating point value to compare to.
abs_tol: the minimum absolute tolerance. Defaults to 1e-9.

Returns:
whether the floating point value is close to zero.
"""
zero: float = 0.0
return math.isclose(a=value, b=zero, abs_tol=abs_tol)
55 changes: 55 additions & 0 deletions tests/test_math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# License: MIT
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH

"""Tests for the math module."""

from hypothesis import given
from hypothesis import strategies as st

from frequenz.core.math import is_close_to_zero

# We first do some regular test cases to avoid mistakes using hypothesis and having
# basic cases not working.


def test_is_close_to_zero_default_tolerance() -> None:
"""Test the is_close_to_zero function with the default tolerance."""
assert is_close_to_zero(0.0)
assert is_close_to_zero(1e-10)
assert not is_close_to_zero(1e-8)


def test_is_close_to_zero_custom_tolerance() -> None:
"""Test the is_close_to_zero function with a custom tolerance."""
assert is_close_to_zero(0.0, abs_tol=1e-8)
assert is_close_to_zero(1e-8, abs_tol=1e-8)
assert not is_close_to_zero(1e-7, abs_tol=1e-8)


def test_is_close_to_zero_negative_values() -> None:
"""Test the is_close_to_zero function with negative values."""
assert is_close_to_zero(-1e-10)
assert not is_close_to_zero(-1e-8)


@given(st.floats(allow_nan=False, allow_infinity=False))
def test_is_close_to_zero_default_tolerance_hypothesis(value: float) -> None:
"""Test the is_close_to_zero function with the default tolerance for many values."""
if -1e-9 <= value <= 1e-9:
assert is_close_to_zero(value)
else:
assert not is_close_to_zero(value)


@given(
st.floats(allow_nan=False, allow_infinity=False),
st.floats(allow_nan=False, allow_infinity=False, min_value=0.0, max_value=2.0),
)
def test_is_close_to_zero_custom_tolerance_hypothesis(
value: float, abs_tol: float
) -> None:
"""Test the is_close_to_zero function with a custom tolerance with many values/tolerance."""
if -abs_tol <= value <= abs_tol:
assert is_close_to_zero(value, abs_tol=abs_tol)
else:
assert not is_close_to_zero(value, abs_tol=abs_tol)