Skip to content

Commit 6ad0aa8

Browse files
claude[bot]codelion
andcommitted
Fix MAP-Elites algorithm implementation
- Fix _enforce_population_limit to respect MAP-Elites structure - Prioritize removing non-elite programs before elite programs - Fix program replacement in feature cells to properly remove old programs - Add _remove_program_from_database method for clean program removal - Add comprehensive tests to verify MAP-Elites functionality - Preserve diversity by keeping best program in each feature cell - Add logging for MAP-Elites grid occupancy and operations Fixes #150 Co-authored-by: Asankhaya Sharma <[email protected]>
1 parent 7591d04 commit 6ad0aa8

File tree

2 files changed

+338
-45
lines changed

2 files changed

+338
-45
lines changed

openevolve/database.py

Lines changed: 92 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ def add(
192192
# Log significant MAP-Elites events
193193
coords_dict = {self.config.feature_dimensions[i]: feature_coords[i] for i in range(len(feature_coords))}
194194

195+
replaced_program_id = None
195196
if feature_key not in self.feature_map:
196197
# New cell occupation
197198
logger.info("New MAP-Elites cell occupied: %s", coords_dict)
@@ -210,8 +211,14 @@ def add(
210211
existing_fitness = safe_numeric_average(existing_program.metrics)
211212
logger.info("MAP-Elites cell improved: %s (fitness: %.3f -> %.3f)",
212213
coords_dict, existing_fitness, new_fitness)
214+
replaced_program_id = existing_program_id
213215

216+
# Update the feature map with the new program
214217
self.feature_map[feature_key] = program.id
218+
219+
# Remove the replaced program from the database (if it exists and isn't the best program)
220+
if replaced_program_id and replaced_program_id != self.best_program_id:
221+
self._remove_program_from_database(replaced_program_id)
215222

216223
# Add to specific island (not random!)
217224
island_idx = target_island if target_island is not None else self.current_island
@@ -1193,6 +1200,11 @@ def _sample_inspirations(self, parent: Program, n: int = 5) -> List[Program]:
11931200
def _enforce_population_limit(self, exclude_program_id: Optional[str] = None) -> None:
11941201
"""
11951202
Enforce the population size limit by removing worst programs if needed
1203+
1204+
This method respects the MAP-Elites algorithm by:
1205+
1. Prioritizing removal of non-elite programs (not in feature_map)
1206+
2. Only removing elite programs if absolutely necessary
1207+
3. Preserving diversity by keeping the best program in each feature cell
11961208
11971209
Args:
11981210
exclude_program_id: Program ID to never remove (e.g., newly added program)
@@ -1206,62 +1218,54 @@ def _enforce_population_limit(self, exclude_program_id: Optional[str] = None) ->
12061218
logger.info(
12071219
f"Population size ({len(self.programs)}) exceeds limit ({self.config.population_size}), removing {num_to_remove} programs"
12081220
)
1221+
1222+
# Log MAP-Elites grid occupancy for debugging
1223+
total_possible_cells = self.feature_bins ** len(self.config.feature_dimensions)
1224+
grid_occupancy = len(self.feature_map) / total_possible_cells
1225+
logger.info(f"MAP-Elites grid occupancy: {len(self.feature_map)}/{total_possible_cells} ({grid_occupancy:.1%})")
12091226

1210-
# Get programs sorted by fitness (worst first)
1227+
# Identify programs that are in the feature map (elite programs)
1228+
feature_map_program_ids = set(self.feature_map.values())
1229+
1230+
# Get all programs and split into elite and non-elite
12111231
all_programs = list(self.programs.values())
1232+
elite_programs = [p for p in all_programs if p.id in feature_map_program_ids]
1233+
non_elite_programs = [p for p in all_programs if p.id not in feature_map_program_ids]
1234+
1235+
# Sort programs by fitness (worst first)
1236+
non_elite_programs.sort(key=lambda p: safe_numeric_average(p.metrics))
1237+
elite_programs.sort(key=lambda p: safe_numeric_average(p.metrics))
12121238

1213-
# Sort by average metric (worst first)
1214-
sorted_programs = sorted(
1215-
all_programs,
1216-
key=lambda p: safe_numeric_average(p.metrics),
1217-
)
1218-
1219-
# Remove worst programs, but never remove the best program or excluded program
1220-
programs_to_remove = []
1239+
# Protected programs that should never be removed
12211240
protected_ids = {self.best_program_id, exclude_program_id} - {None}
1222-
1223-
for program in sorted_programs:
1241+
1242+
programs_to_remove = []
1243+
1244+
# Phase 1: Remove non-elite programs first (safe to remove)
1245+
logger.debug(f"Phase 1: Removing non-elite programs (safe to remove)")
1246+
for program in non_elite_programs:
12241247
if len(programs_to_remove) >= num_to_remove:
12251248
break
1226-
# Don't remove the best program or excluded program
12271249
if program.id not in protected_ids:
12281250
programs_to_remove.append(program)
1229-
1230-
# If we still need to remove more and only have protected programs,
1231-
# remove from the remaining programs anyway (but keep the protected ones)
1251+
logger.debug(f"Marked non-elite program {program.id} for removal")
1252+
1253+
# Phase 2: If we still need to remove more, remove worst elite programs
1254+
# This should be rare and only happens when population is very small
12321255
if len(programs_to_remove) < num_to_remove:
1233-
remaining_programs = [
1234-
p
1235-
for p in sorted_programs
1236-
if p not in programs_to_remove and p.id not in protected_ids
1237-
]
1238-
additional_removals = remaining_programs[: num_to_remove - len(programs_to_remove)]
1239-
programs_to_remove.extend(additional_removals)
1256+
remaining_to_remove = num_to_remove - len(programs_to_remove)
1257+
logger.info(f"Phase 2: Need to remove {remaining_to_remove} elite programs (may reduce diversity)")
1258+
1259+
for program in elite_programs:
1260+
if len(programs_to_remove) >= num_to_remove:
1261+
break
1262+
if program.id not in protected_ids:
1263+
programs_to_remove.append(program)
1264+
logger.info(f"Marked elite program {program.id} for removal (reducing diversity)")
12401265

1241-
# Remove the selected programs
1266+
# Remove the selected programs using the dedicated method
12421267
for program in programs_to_remove:
1243-
program_id = program.id
1244-
1245-
# Remove from main programs dict
1246-
if program_id in self.programs:
1247-
del self.programs[program_id]
1248-
1249-
# Remove from feature map
1250-
keys_to_remove = []
1251-
for key, pid in self.feature_map.items():
1252-
if pid == program_id:
1253-
keys_to_remove.append(key)
1254-
for key in keys_to_remove:
1255-
del self.feature_map[key]
1256-
1257-
# Remove from islands
1258-
for island in self.islands:
1259-
island.discard(program_id)
1260-
1261-
# Remove from archive
1262-
self.archive.discard(program_id)
1263-
1264-
logger.debug(f"Removed program {program_id} due to population limit")
1268+
self._remove_program_from_database(program.id)
12651269

12661270
logger.info(f"Population size after cleanup: {len(self.programs)}")
12671271

@@ -1714,6 +1718,49 @@ def _load_artifact_dir(self, artifact_dir: str) -> Dict[str, Union[str, bytes]]:
17141718
logger.warning(f"Failed to list artifact directory {artifact_dir}: {e}")
17151719

17161720
return artifacts
1721+
1722+
def _remove_program_from_database(self, program_id: str) -> None:
1723+
"""
1724+
Remove a program from all database structures
1725+
1726+
This method provides a clean way to remove a program from:
1727+
- Main programs dictionary
1728+
- Feature map
1729+
- Islands
1730+
- Archive
1731+
- Island best programs references
1732+
1733+
Args:
1734+
program_id: ID of the program to remove
1735+
"""
1736+
if program_id not in self.programs:
1737+
logger.debug(f"Program {program_id} not found in database, skipping removal")
1738+
return
1739+
1740+
# Remove from main programs dict
1741+
del self.programs[program_id]
1742+
1743+
# Remove from feature map
1744+
keys_to_remove = []
1745+
for key, pid in self.feature_map.items():
1746+
if pid == program_id:
1747+
keys_to_remove.append(key)
1748+
for key in keys_to_remove:
1749+
del self.feature_map[key]
1750+
1751+
# Remove from islands
1752+
for island in self.islands:
1753+
island.discard(program_id)
1754+
1755+
# Remove from archive
1756+
self.archive.discard(program_id)
1757+
1758+
# Remove from island best programs references
1759+
for i, best_id in enumerate(self.island_best_programs):
1760+
if best_id == program_id:
1761+
self.island_best_programs[i] = None
1762+
1763+
logger.debug(f"Removed program {program_id} from all database structures")
17171764

17181765
def log_prompt(
17191766
self,

0 commit comments

Comments
 (0)