diff --git a/openevolve/database.py b/openevolve/database.py index 0f487c0ec..11ce0daa8 100644 --- a/openevolve/database.py +++ b/openevolve/database.py @@ -30,7 +30,7 @@ ProgramState, ) from openevolve.utils.code_utils import calculate_edit_distance -from openevolve.utils.metrics_utils import safe_numeric_average, get_fitness_score +from openevolve.utils.metrics_utils import get_fitness_score, safe_numeric_average logger = logging.getLogger(__name__) @@ -1136,6 +1136,7 @@ def _llm_judge_novelty(self, program: Program, similar_program: Program) -> bool Use LLM to judge if a program is novel compared to a similar existing program """ import asyncio + from openevolve.novelty_judge import NOVELTY_SYSTEM_MSG, NOVELTY_USER_MSG user_msg = NOVELTY_USER_MSG.format( @@ -1811,8 +1812,14 @@ def _sample_inspirations( for _ in range(remaining_slots * 3): # Try more times to find nearby programs # Perturb coordinates perturbed_coords = [ - max(0, min(self.feature_bins - 1, c + random.randint(-2, 2))) - for c in feature_coords + max( + 0, + min( + self.feature_bins_per_dim.get(dim, self.feature_bins) - 1, + c + random.randint(-2, 2), + ), + ) + for dim, c in zip(self.config.feature_dimensions, feature_coords) ] cell_key = self._feature_coords_to_key(perturbed_coords) diff --git a/tests/test_inspiration_feature_bounds.py b/tests/test_inspiration_feature_bounds.py new file mode 100644 index 000000000..4ac05716e --- /dev/null +++ b/tests/test_inspiration_feature_bounds.py @@ -0,0 +1,47 @@ +"""Neighborhood sampling must use the bounds of each MAP-Elites dimension.""" + +import unittest +from unittest.mock import patch + +from openevolve.config import DatabaseConfig +from openevolve.database import Program, ProgramDatabase + + +class TestInspirationFeatureBounds(unittest.TestCase): + def test_neighbor_in_wide_dimension_is_selected_before_random_fallback(self): + db = ProgramDatabase( + DatabaseConfig( + in_memory=True, + num_islands=1, + feature_dimensions=["x", "y"], + feature_bins={"x": 20, "y": 2}, + elite_selection_ratio=0.2, + ) + ) + db.feature_stats = { + dim: {"min": 0.0, "max": 1.0, "values": [0.0, 1.0]} for dim in ("x", "y") + } + parent = Program(id="parent", code="parent", metrics={"score": 0.1, "x": 0.9, "y": 0.75}) + neighbor = Program( + id="neighbor", code="neighbor", metrics={"score": 0.2, "x": 0.99, "y": 0.75} + ) + best = Program(id="best", code="best", metrics={"score": 1.0, "x": 0.0, "y": 0.0}) + far = Program(id="far", code="far", metrics={"score": 0.3, "x": 0.0, "y": 0.75}) + db.programs = {p.id: p for p in (parent, neighbor, best, far)} + db.islands = [set(db.programs)] + db.island_best_programs = [best.id] + self.assertEqual(db._calculate_feature_coords(parent), [18, 1]) + self.assertEqual(db._calculate_feature_coords(neighbor), [19, 1]) + + with ( + patch("openevolve.database.random.randint", return_value=2), + patch("openevolve.database.random.sample", return_value=[far.id]) as fallback, + ): + inspirations = db._sample_inspirations(parent, n=2, island_id=0) + + self.assertEqual([p.id for p in inspirations], [best.id, neighbor.id]) + fallback.assert_not_called() + + +if __name__ == "__main__": + unittest.main()