@@ -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