From cbd50fe0598cfd641064e6ae7ee0a4588b6cce19 Mon Sep 17 00:00:00 2001 From: Yushu Li Date: Sun, 1 Nov 2026 21:33:42 -0800 Subject: [PATCH] fix(metrics): avoid overflow in finite fitness averages --- openevolve/utils/metrics_utils.py | 9 ++++- tests/test_numeric_average_overflow.py | 49 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/test_numeric_average_overflow.py diff --git a/openevolve/utils/metrics_utils.py b/openevolve/utils/metrics_utils.py index 5c176e0e2..351001849 100644 --- a/openevolve/utils/metrics_utils.py +++ b/openevolve/utils/metrics_utils.py @@ -2,6 +2,7 @@ Safe calculation utilities for metrics containing mixed types """ +import math from typing import Any, Dict, List, Optional @@ -37,7 +38,13 @@ def safe_numeric_average(metrics: Dict[str, Any]) -> float: if not numeric_values: return 0.0 - return sum(numeric_values) / len(numeric_values) + total = sum(numeric_values) + if not math.isfinite(total) and all(math.isfinite(value) for value in numeric_values): + # A finite average can still have an overflowing intermediate sum. + # Rescale only that case, retaining ordinary rounding and explicit infinities. + scale = max(abs(value) for value in numeric_values) + return (sum(value / scale for value in numeric_values) / len(numeric_values)) * scale + return total / len(numeric_values) def safe_numeric_sum(metrics: Dict[str, Any]) -> float: diff --git a/tests/test_numeric_average_overflow.py b/tests/test_numeric_average_overflow.py new file mode 100644 index 000000000..a8bfe5089 --- /dev/null +++ b/tests/test_numeric_average_overflow.py @@ -0,0 +1,49 @@ +"""Finite fitness averages should not overflow while summing their inputs.""" + +import math +import sys +import unittest + +from openevolve.utils.metrics_utils import get_fitness_score, safe_numeric_average + + +class TestNumericAverageOverflow(unittest.TestCase): + def test_large_same_sign_metrics_have_a_finite_average(self): + for value in (1e308, -1e308, sys.float_info.max, -sys.float_info.max): + for count in (2, 3, 10): + with self.subTest(value=value, count=count): + metrics = {str(i): value for i in range(count)} + self.assertEqual(safe_numeric_average(metrics), value) + + def test_finite_average_survives_intermediate_overflow_and_cancellation(self): + maximum = sys.float_info.max + for values in ( + [maximum, maximum, -maximum], + [-maximum, -maximum, maximum], + [maximum, maximum, -maximum, -maximum], + ): + with self.subTest(values=values): + metrics = dict(enumerate(values)) + expected = (sum(value / maximum for value in values) / len(values)) * maximum + self.assertEqual(safe_numeric_average(metrics), expected) + + def test_fallback_fitness_excludes_features_and_flags(self): + metrics = {"a": 1e308, "b": 1e308, "complexity": 3.0, "valid": True} + self.assertEqual(get_fitness_score(metrics, ["complexity"]), 1e308) + self.assertEqual(get_fitness_score({**metrics, "combined_score": 0.25}), 0.25) + + def test_ordinary_metrics_and_invalid_values_keep_existing_behavior(self): + self.assertEqual(safe_numeric_average({"a": 1.0, "b": 3.0}), 2.0) + self.assertEqual(safe_numeric_average({"nan": math.nan, "a": 2, "valid": True}), 2.0) + self.assertEqual(safe_numeric_average({"a": 0.0, "b": -0.0}), 0.0) + self.assertEqual(safe_numeric_average({}), 0.0) + self.assertEqual(safe_numeric_average({"too_large": 10**400, "a": 2}), 2.0) + + def test_explicit_infinity_policy_is_unchanged(self): + self.assertEqual(safe_numeric_average({"a": math.inf, "b": 1}), math.inf) + self.assertEqual(safe_numeric_average({"a": -math.inf, "b": 1}), -math.inf) + self.assertTrue(math.isnan(safe_numeric_average({"a": math.inf, "b": -math.inf}))) + + +if __name__ == "__main__": + unittest.main()