Skip to content

Commit 48ca6d0

Browse files
codelionclaude
andcommitted
Exclude booleans from fitness math; drop dead format helpers; README badges; bump 0.3.2
Builds on the boolean display fix in this PR. `bool` is a subclass of `int`, so the naive `isinstance(value, (int, float))` check treats True/False as 1.0/0.0. The PR fixed that for formatting; the same trap was still live in the FITNESS math, which matters more: - openevolve/utils/metrics_utils.py: exclude bools in both safe_numeric_average() and get_fitness_score(). openevolve/evaluator.py returns {"error": 0.0, "timeout": True} when an evaluation times out, which averaged to 0.5 - handing a program that failed outright a mid-range fitness and letting it compete for survival in the database. It now correctly scores 0.0. - openevolve/controller.py: remove the module-level _format_metrics/_format_improvement helpers. They were dead code (defined, never called - the controller imports the shared format_utils versions), and confusingly they already contained the bool fix that the live code path lacked. With format_utils corrected they are redundant. - tests/test_boolean_metrics.py: covers both display and fitness. The fitness cases fail without the metrics_utils change (0.5 != 0.0), and the suite pins that combined_score precedence, feature-dimension exclusion, and ordinary numeric metrics are unaffected. README: swap the PyPI downloads badge for the pepy.tech monthly badge, and drop the GitHub stars badge (GitHub already displays the star count). Bump version to 0.3.2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7a92c28 commit 48ca6d0

5 files changed

Lines changed: 83 additions & 33 deletions

File tree

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
*Turn your LLMs into autonomous code optimizers that discover breakthrough algorithms*
1010

1111
<p align="center">
12-
<a href="https://github.com/algorithmicsuperintelligence/openevolve/stargazers"><img src="https://img.shields.io/github/stars/algorithmicsuperintelligence/openevolve?style=social" alt="GitHub stars"></a>
1312
<a href="https://pypi.org/project/openevolve/"><img src="https://img.shields.io/pypi/v/openevolve" alt="PyPI version"></a>
14-
<a href="https://pypi.org/project/openevolve/"><img src="https://img.shields.io/pypi/dm/openevolve" alt="PyPI downloads"></a>
13+
<a href="https://pepy.tech/projects/openevolve"><img src="https://static.pepy.tech/personalized-badge/openevolve?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads%2Fmonth" alt="PyPI Downloads"></a>
1514
<a href="https://github.com/algorithmicsuperintelligence/openevolve/blob/main/LICENSE"><img src="https://img.shields.io/github/license/algorithmicsuperintelligence/openevolve" alt="License"></a>
1615
</p>
1716

openevolve/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version information for openevolve package."""
22

3-
__version__ = "0.3.1"
3+
__version__ = "0.3.2"

openevolve/controller.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,6 @@
2525
logger = logging.getLogger(__name__)
2626

2727

28-
def _format_metrics(metrics: Dict[str, Any]) -> str:
29-
"""Safely format metrics, handling both numeric and string values"""
30-
formatted_parts = []
31-
for name, value in metrics.items():
32-
if isinstance(value, (int, float)) and not isinstance(value, bool):
33-
try:
34-
formatted_parts.append(f"{name}={value:.4f}")
35-
except (ValueError, TypeError):
36-
formatted_parts.append(f"{name}={value}")
37-
else:
38-
formatted_parts.append(f"{name}={value}")
39-
return ", ".join(formatted_parts)
40-
41-
42-
def _format_improvement(improvement: Dict[str, Any]) -> str:
43-
"""Safely format improvement metrics"""
44-
formatted_parts = []
45-
for name, diff in improvement.items():
46-
if isinstance(diff, (int, float)) and not isinstance(diff, bool):
47-
try:
48-
formatted_parts.append(f"{name}={diff:+.4f}")
49-
except (ValueError, TypeError):
50-
formatted_parts.append(f"{name}={diff}")
51-
else:
52-
formatted_parts.append(f"{name}={diff}")
53-
return ", ".join(formatted_parts)
54-
55-
5628
class OpenEvolve:
5729
"""
5830
Main controller for OpenEvolve

openevolve/utils/metrics_utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ def safe_numeric_average(metrics: Dict[str, Any]) -> float:
2121

2222
numeric_values = []
2323
for value in metrics.values():
24-
if isinstance(value, (int, float)):
24+
# bool is a subclass of int, but a flag is not a score. Averaging it in
25+
# would let e.g. {"error": 0.0, "timeout": True} score 0.5 instead of 0.0,
26+
# giving a program that timed out a mid-range fitness.
27+
if isinstance(value, (int, float)) and not isinstance(value, bool):
2528
try:
2629
# Convert to float and check if it's a valid number
2730
float_val = float(value)
@@ -99,7 +102,8 @@ def get_fitness_score(
99102
for key, value in metrics.items():
100103
# Exclude MAP feature dimensions from fitness calculation
101104
if key not in feature_dimensions:
102-
if isinstance(value, (int, float)):
105+
# Booleans are flags, not scores - see the note in safe_numeric_average.
106+
if isinstance(value, (int, float)) and not isinstance(value, bool):
103107
try:
104108
float_val = float(value)
105109
if not (float_val != float_val): # Check for NaN

tests/test_boolean_metrics.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Booleans in a metrics dict are FLAGS, not scores.
3+
4+
`bool` is a subclass of `int`, so a naive `isinstance(value, (int, float))` check
5+
silently treats True/False as 1.0/0.0. That matters in two places:
6+
7+
* display - `timeout=1.0000` instead of `timeout=True`
8+
* FITNESS - a program that timed out returns {"error": 0.0, "timeout": True}
9+
(openevolve/evaluator.py), which averaged to 0.5 instead of 0.0,
10+
handing a failed program a mid-range score.
11+
12+
OpenEvolve's own evaluator emits `timeout: True` in several places, so this is not
13+
a hypothetical input.
14+
"""
15+
16+
import os
17+
import unittest
18+
19+
os.environ.setdefault("OPENAI_API_KEY", "test")
20+
21+
from openevolve.utils.format_utils import format_improvement_safe, format_metrics_safe
22+
from openevolve.utils.metrics_utils import get_fitness_score, safe_numeric_average
23+
24+
25+
class TestBooleanMetricsFormatting(unittest.TestCase):
26+
def test_bool_rendered_as_true_false(self):
27+
self.assertEqual(
28+
format_metrics_safe({"valid": True, "timeout": False, "score": 0.25}),
29+
"valid=True, timeout=False, score=0.2500",
30+
)
31+
32+
def test_bool_excluded_from_improvement(self):
33+
"""A boolean flipping False->True is not a '+1.0000' improvement."""
34+
self.assertEqual(
35+
format_improvement_safe(
36+
{"valid": False, "score": 0.25},
37+
{"valid": True, "score": 0.5},
38+
),
39+
"score=+0.2500",
40+
)
41+
42+
def test_numeric_formatting_unchanged(self):
43+
self.assertEqual(format_metrics_safe({"score": 0.5, "n": 3}), "score=0.5000, n=3.0000")
44+
45+
46+
class TestBooleanMetricsExcludedFromFitness(unittest.TestCase):
47+
def test_timed_out_program_scores_zero(self):
48+
"""The exact dict openevolve/evaluator.py returns on timeout."""
49+
metrics = {"error": 0.0, "timeout": True}
50+
# Before the fix both of these returned 0.5.
51+
self.assertEqual(safe_numeric_average(metrics), 0.0)
52+
self.assertEqual(get_fitness_score(metrics), 0.0)
53+
54+
def test_bool_does_not_inflate_average(self):
55+
# Without the guard this would be (0.4 + 1.0) / 2 = 0.7
56+
self.assertAlmostEqual(safe_numeric_average({"score": 0.4, "valid": True}), 0.4)
57+
58+
def test_all_boolean_metrics_average_to_zero(self):
59+
self.assertEqual(safe_numeric_average({"valid": True, "timeout": False}), 0.0)
60+
61+
def test_combined_score_still_takes_precedence(self):
62+
self.assertAlmostEqual(get_fitness_score({"combined_score": 0.9, "timeout": True}), 0.9)
63+
64+
def test_ordinary_metrics_unaffected(self):
65+
self.assertAlmostEqual(safe_numeric_average({"a": 0.8, "b": 0.6}), 0.7)
66+
self.assertAlmostEqual(get_fitness_score({"a": 0.8, "b": 0.6}), 0.7)
67+
68+
def test_feature_dimensions_still_excluded(self):
69+
"""Bool handling must not disturb the existing feature-dimension exclusion."""
70+
metrics = {"score": 0.8, "complexity": 100.0}
71+
self.assertAlmostEqual(get_fitness_score(metrics, ["complexity"]), 0.8)
72+
73+
74+
if __name__ == "__main__":
75+
unittest.main()

0 commit comments

Comments
 (0)