Skip to content

Commit 92c7808

Browse files
committed
Allow custom metrics to override built-in features
Feature coordinate calculation now prioritizes custom metrics from the evaluator over built-in features like 'complexity' and 'diversity'. Added tests to verify that user-supplied metrics override default calculations when present.
1 parent 3abf767 commit 92c7808

File tree

2 files changed

+95
-11
lines changed

2 files changed

+95
-11
lines changed

openevolve/database.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,20 @@ def _calculate_feature_coords(self, program: Program) -> List[int]:
806806
coords = []
807807

808808
for dim in self.config.feature_dimensions:
809-
if dim == "complexity":
809+
# PRIORITY 1: Check if this is a custom metric from the evaluator
810+
# This allows users to override built-in features with their own implementations
811+
if dim in program.metrics:
812+
# Use custom metric from evaluator
813+
score = program.metrics[dim]
814+
# Update stats and scale
815+
self._update_feature_stats(dim, score)
816+
scaled_value = self._scale_feature_value(dim, score)
817+
num_bins = self.feature_bins_per_dim.get(dim, self.feature_bins)
818+
bin_idx = int(scaled_value * num_bins)
819+
bin_idx = max(0, min(num_bins - 1, bin_idx))
820+
coords.append(bin_idx)
821+
# PRIORITY 2: Fall back to built-in features if not in metrics
822+
elif dim == "complexity":
810823
# Use code length as complexity measure
811824
complexity = len(program.code)
812825
bin_idx = self._calculate_complexity_bin(complexity)
@@ -833,21 +846,12 @@ def _calculate_feature_coords(self, program: Program) -> List[int]:
833846
bin_idx = int(scaled_value * num_bins)
834847
bin_idx = max(0, min(num_bins - 1, bin_idx))
835848
coords.append(bin_idx)
836-
elif dim in program.metrics:
837-
# Use specific metric
838-
score = program.metrics[dim]
839-
# Update stats and scale
840-
self._update_feature_stats(dim, score)
841-
scaled_value = self._scale_feature_value(dim, score)
842-
num_bins = self.feature_bins_per_dim.get(dim, self.feature_bins)
843-
bin_idx = int(scaled_value * num_bins)
844-
bin_idx = max(0, min(num_bins - 1, bin_idx))
845-
coords.append(bin_idx)
846849
else:
847850
# Feature not found - this is an error
848851
raise ValueError(
849852
f"Feature dimension '{dim}' specified in config but not found in program metrics. "
850853
f"Available metrics: {list(program.metrics.keys())}. "
854+
f"Built-in features: 'complexity', 'diversity', 'score'. "
851855
f"Either remove '{dim}' from feature_dimensions or ensure your evaluator returns it."
852856
)
853857
# Only log coordinates at debug level for troubleshooting

tests/test_map_elites_features.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,86 @@ def test_missing_feature_dimension_error(self):
268268
self.assertIn("not found in program metrics", str(context.exception))
269269
self.assertIn("score", str(context.exception)) # Should show available metrics
270270

271+
def test_custom_features_override_builtin(self):
272+
"""Test that custom complexity and diversity from evaluator override built-in calculations"""
273+
# Create database with complexity and diversity as feature dimensions
274+
config = Config()
275+
config.database.in_memory = True
276+
config.database.feature_dimensions = ["complexity", "diversity"]
277+
config.database.feature_bins = 10
278+
db = ProgramDatabase(config.database)
279+
280+
# Add a program with custom complexity and diversity metrics from evaluator
281+
# The evaluator is providing its own definition of complexity and diversity
282+
program = Program(
283+
id="custom_override",
284+
code="x" * 1000, # 1000 chars - built-in would use this
285+
language="python",
286+
metrics={
287+
"complexity": 42.5, # Custom complexity from evaluator (NOT code length)
288+
"diversity": 99.9, # Custom diversity from evaluator (NOT code structure)
289+
"score": 0.8,
290+
},
291+
)
292+
293+
# Add program to trigger feature coordinate calculation
294+
db.add(program)
295+
296+
# Manually calculate what bins the custom values should map to
297+
# The custom values should be used, not the built-in calculations
298+
299+
# For complexity: custom value is 42.5
300+
db._update_feature_stats("complexity", 42.5)
301+
custom_complexity_scaled = db._scale_feature_value("complexity", 42.5)
302+
expected_complexity_bin = int(custom_complexity_scaled * 10)
303+
expected_complexity_bin = max(0, min(9, expected_complexity_bin))
304+
305+
# For diversity: custom value is 99.9
306+
db._update_feature_stats("diversity", 99.9)
307+
custom_diversity_scaled = db._scale_feature_value("diversity", 99.9)
308+
expected_diversity_bin = int(custom_diversity_scaled * 10)
309+
expected_diversity_bin = max(0, min(9, expected_diversity_bin))
310+
311+
# Get actual coordinates
312+
coords = db._calculate_feature_coords(program)
313+
314+
# Verify custom metrics were used
315+
# If built-in was used for complexity, it would use len(code) = 1000
316+
# If built-in was used for diversity, it would calculate code structure diversity
317+
# With custom metrics, we should see the bins for 42.5 and 99.9
318+
self.assertEqual(coords[0], expected_complexity_bin,
319+
"Custom complexity metric should override built-in code length")
320+
self.assertEqual(coords[1], expected_diversity_bin,
321+
"Custom diversity metric should override built-in code diversity")
322+
323+
# Additional verification: test with multiple programs to ensure consistency
324+
program2 = Program(
325+
id="custom_override_2",
326+
code="y" * 500, # Different code length
327+
language="python",
328+
metrics={
329+
"complexity": 10.0, # Much lower than code length
330+
"diversity": 5.0, # Custom diversity
331+
"score": 0.6,
332+
},
333+
)
334+
db.add(program2)
335+
coords2 = db._calculate_feature_coords(program2)
336+
337+
# Calculate expected bins for second program
338+
db._update_feature_stats("complexity", 10.0)
339+
custom_complexity_scaled_2 = db._scale_feature_value("complexity", 10.0)
340+
expected_complexity_bin_2 = int(custom_complexity_scaled_2 * 10)
341+
expected_complexity_bin_2 = max(0, min(9, expected_complexity_bin_2))
342+
343+
db._update_feature_stats("diversity", 5.0)
344+
custom_diversity_scaled_2 = db._scale_feature_value("diversity", 5.0)
345+
expected_diversity_bin_2 = int(custom_diversity_scaled_2 * 10)
346+
expected_diversity_bin_2 = max(0, min(9, expected_diversity_bin_2))
347+
348+
self.assertEqual(coords2[0], expected_complexity_bin_2)
349+
self.assertEqual(coords2[1], expected_diversity_bin_2)
350+
271351

272352
if __name__ == "__main__":
273353
unittest.main()

0 commit comments

Comments
 (0)