From d7c86e5c925193d9f6530d267ced0f3bf7acffd6 Mon Sep 17 00:00:00 2001 From: "Alexander V. Hopp" Date: Thu, 26 Mar 2026 10:43:09 +0100 Subject: [PATCH 1/6] Add is_close function for intervals --- baybe/utils/interval.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/baybe/utils/interval.py b/baybe/utils/interval.py index 4b1f4c0dca..fc6fdbd5fa 100644 --- a/baybe/utils/interval.py +++ b/baybe/utils/interval.py @@ -161,6 +161,21 @@ def contains(self, number: float) -> bool: or (self.lower < number < self.upper) ) + def is_close(self, other: Interval, /) -> bool: + """Check whether the interval is close to another interval. + + Uses :func:`numpy.isclose` for comparison of both bounds. + + Args: + other: The interval to compare against. + + Returns: + Whether both bounds are close. + """ + return bool(np.isclose(self.lower, other.lower)) and bool( + np.isclose(self.upper, other.upper) + ) + def __add__(self, other: float | int) -> Interval: """Shift bounds via scalar addition.""" return Interval(self.lower + other, self.upper + other) From b121a890046fe646a35b91d27be2c698dd3130e5 Mon Sep 17 00:00:00 2001 From: "Alexander V. Hopp" Date: Thu, 26 Mar 2026 10:43:41 +0100 Subject: [PATCH 2/6] Use is_close for comparison --- baybe/targets/numerical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/baybe/targets/numerical.py b/baybe/targets/numerical.py index 5aee308d53..c32127cb6e 100644 --- a/baybe/targets/numerical.py +++ b/baybe/targets/numerical.py @@ -587,7 +587,7 @@ def normalized_sigmoid( def is_normalized(self) -> UncertainBool: """Boolean flag indicating if the target is normalized to the unit interval.""" return UncertainBool.from_erroneous_callable( - lambda: self.get_image() == Interval(0, 1) + lambda: self.get_image().is_close(Interval(0, 1)) ) def get_codomain(self, interval: Interval | None = None, /) -> Interval: From a47869dc9cd22fa2e4d27b172344df95890eab58 Mon Sep 17 00:00:00 2001 From: "Alexander V. Hopp" Date: Thu, 26 Mar 2026 10:49:58 +0100 Subject: [PATCH 3/6] Add tests for is_close function of Interval --- tests/validation/test_interval_validation.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/validation/test_interval_validation.py b/tests/validation/test_interval_validation.py index d232273758..588d439399 100644 --- a/tests/validation/test_interval_validation.py +++ b/tests/validation/test_interval_validation.py @@ -24,3 +24,18 @@ def test_invalid_range(request, bounds): return with pytest.raises(ValueError): Interval(*bounds[::-1]) + + +@pytest.mark.parametrize( + ("other", "expected"), + [ + param(Interval(0, 1), True, id="exact_match"), + param(Interval(0, 0.9999999999999999), True, id="upper_float_imprecision"), + param(Interval(1e-16, 1 - 1e-16), True, id="both_float_imprecision"), + param(Interval(0, 0.5), False, id="different_upper"), + param(Interval(0.5, 1), False, id="different_lower"), + ], +) +def test_isclose(other, expected): + """Intervals that are close up to floating-point precision are detected.""" + assert Interval(0, 1).is_close(other) == expected From f6bd8fe2218bfca48d5591a33d1181a98990b761 Mon Sep 17 00:00:00 2001 From: "Alexander V. Hopp" Date: Thu, 26 Mar 2026 10:50:05 +0100 Subject: [PATCH 4/6] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9931d0d0e..0362017051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `identify_non_dominated_configurations` method to `Campaign` and `Objective` for determining the Pareto front - Interpoint constraints for continuous search spaces +- `Interval.is_close` method for fuzzy comparison of interval bounds using `numpy.isclose` ### Breaking Changes - `ContinuousLinearConstraint.to_botorch` now returns a collection of constraint tuples @@ -21,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - `SHAPInsight` breaking with `numpy>=2.4` due to no longer accepted implicit array to scalar conversion +- `NumericalTarget.is_normalized` returning `False` for targets that are normalized up to + floating-point precision (e.g. after `.clamp(...).normalize()` chains) ### Removed - `parallel_runs` argument from `simulate_scenarios`, since parallelization From c81db9e8f1e0e29d82e9e4da93b9faf7cb8200b5 Mon Sep 17 00:00:00 2001 From: "Alexander V. Hopp" Date: Fri, 27 Mar 2026 10:25:18 +0100 Subject: [PATCH 5/6] Use cmp_using instead of explicit is_close function --- CHANGELOG.md | 5 ++--- baybe/targets/numerical.py | 2 +- baybe/utils/interval.py | 19 +++---------------- tests/validation/test_interval_validation.py | 4 ++-- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0362017051..e032538629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `identify_non_dominated_configurations` method to `Campaign` and `Objective` for determining the Pareto front - Interpoint constraints for continuous search spaces -- `Interval.is_close` method for fuzzy comparison of interval bounds using `numpy.isclose` ### Breaking Changes - `ContinuousLinearConstraint.to_botorch` now returns a collection of constraint tuples @@ -22,8 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - `SHAPInsight` breaking with `numpy>=2.4` due to no longer accepted implicit array to scalar conversion -- `NumericalTarget.is_normalized` returning `False` for targets that are normalized up to - floating-point precision (e.g. after `.clamp(...).normalize()` chains) +- Using `np.isclose` for assessing equality of interval bounds instead of hard equality + check ### Removed - `parallel_runs` argument from `simulate_scenarios`, since parallelization diff --git a/baybe/targets/numerical.py b/baybe/targets/numerical.py index c32127cb6e..5aee308d53 100644 --- a/baybe/targets/numerical.py +++ b/baybe/targets/numerical.py @@ -587,7 +587,7 @@ def normalized_sigmoid( def is_normalized(self) -> UncertainBool: """Boolean flag indicating if the target is normalized to the unit interval.""" return UncertainBool.from_erroneous_callable( - lambda: self.get_image().is_close(Interval(0, 1)) + lambda: self.get_image() == Interval(0, 1) ) def get_codomain(self, interval: Interval | None = None, /) -> Interval: diff --git a/baybe/utils/interval.py b/baybe/utils/interval.py index fc6fdbd5fa..ad54c882de 100644 --- a/baybe/utils/interval.py +++ b/baybe/utils/interval.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Union import numpy as np -from attrs import define, field +from attrs import cmp_using, define, field from baybe.serialization import SerialMixin, converter from baybe.settings import active_settings @@ -39,6 +39,7 @@ class Interval(SerialMixin): default=float("-inf"), converter=lambda x: float("-inf") if x is None else float(x), validator=non_nan_float, + eq=cmp_using(eq=np.isclose), ) """The lower end of the interval.""" @@ -46,6 +47,7 @@ class Interval(SerialMixin): default=float("inf"), converter=lambda x: float("inf") if x is None else float(x), validator=non_nan_float, + eq=cmp_using(eq=np.isclose), ) """The upper end of the interval.""" @@ -161,21 +163,6 @@ def contains(self, number: float) -> bool: or (self.lower < number < self.upper) ) - def is_close(self, other: Interval, /) -> bool: - """Check whether the interval is close to another interval. - - Uses :func:`numpy.isclose` for comparison of both bounds. - - Args: - other: The interval to compare against. - - Returns: - Whether both bounds are close. - """ - return bool(np.isclose(self.lower, other.lower)) and bool( - np.isclose(self.upper, other.upper) - ) - def __add__(self, other: float | int) -> Interval: """Shift bounds via scalar addition.""" return Interval(self.lower + other, self.upper + other) diff --git a/tests/validation/test_interval_validation.py b/tests/validation/test_interval_validation.py index 588d439399..5636f50466 100644 --- a/tests/validation/test_interval_validation.py +++ b/tests/validation/test_interval_validation.py @@ -36,6 +36,6 @@ def test_invalid_range(request, bounds): param(Interval(0.5, 1), False, id="different_lower"), ], ) -def test_isclose(other, expected): +def test_close_interval_bounds(other, expected): """Intervals that are close up to floating-point precision are detected.""" - assert Interval(0, 1).is_close(other) == expected + assert (Interval(0, 1) == other) == expected From 343d7f8786db1e59e06a74c5706f4c9cb21b051b Mon Sep 17 00:00:00 2001 From: "Alexander V. Hopp" Date: Fri, 27 Mar 2026 11:01:35 +0100 Subject: [PATCH 6/6] Use lambda and bool conversion to satisfy mypy --- baybe/utils/interval.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/baybe/utils/interval.py b/baybe/utils/interval.py index ad54c882de..2945c18dcc 100644 --- a/baybe/utils/interval.py +++ b/baybe/utils/interval.py @@ -39,7 +39,7 @@ class Interval(SerialMixin): default=float("-inf"), converter=lambda x: float("-inf") if x is None else float(x), validator=non_nan_float, - eq=cmp_using(eq=np.isclose), + eq=cmp_using(eq=lambda a, b: bool(np.isclose(a, b))), ) """The lower end of the interval.""" @@ -47,7 +47,7 @@ class Interval(SerialMixin): default=float("inf"), converter=lambda x: float("inf") if x is None else float(x), validator=non_nan_float, - eq=cmp_using(eq=np.isclose), + eq=cmp_using(eq=lambda a, b: bool(np.isclose(a, b))), ) """The upper end of the interval."""