@@ -122,6 +122,9 @@ def __init__(self, config: DatabaseConfig):
122122
123123 # Track the absolute best program separately
124124 self .best_program_id : Optional [str ] = None
125+
126+ # Track best program per island for proper island-based evolution
127+ self .island_best_programs : List [Optional [str ]] = [None ] * config .num_islands
125128
126129 # Track the last iteration number (for resuming)
127130 self .last_iteration : int = 0
@@ -205,6 +208,9 @@ def add(
205208
206209 # Update the absolute best program tracking (after population enforcement)
207210 self ._update_best_program (program )
211+
212+ # Update island-specific best program tracking
213+ self ._update_island_best_program (program , island_idx )
208214
209215 # Save to disk if configured
210216 if self .config .db_path :
@@ -315,31 +321,47 @@ def get_best_program(self, metric: Optional[str] = None) -> Optional[Program]:
315321
316322 return sorted_programs [0 ] if sorted_programs else None
317323
318- def get_top_programs (self , n : int = 10 , metric : Optional [str ] = None ) -> List [Program ]:
324+ def get_top_programs (self , n : int = 10 , metric : Optional [str ] = None , island_idx : Optional [ int ] = None ) -> List [Program ]:
319325 """
320326 Get the top N programs based on a metric
321327
322328 Args:
323329 n: Number of programs to return
324330 metric: Metric to use for ranking (uses average if None)
331+ island_idx: If specified, only return programs from this island
325332
326333 Returns:
327334 List of top programs
328335 """
329336 if not self .programs :
330337 return []
331338
339+ # Get candidate programs
340+ if island_idx is not None :
341+ # Island-specific query
342+ island_programs = [
343+ self .programs [pid ] for pid in self .islands [island_idx ]
344+ if pid in self .programs
345+ ]
346+ candidates = island_programs
347+ else :
348+ # Global query
349+ candidates = list (self .programs .values ())
350+
351+ if not candidates :
352+ return []
353+
332354 if metric :
333355 # Sort by specific metric
334356 sorted_programs = sorted (
335- [p for p in self . programs . values () if metric in p .metrics ],
357+ [p for p in candidates if metric in p .metrics ],
336358 key = lambda p : p .metrics [metric ],
337359 reverse = True ,
338360 )
339361 else :
340362 # Sort by average of all numeric metrics
341363 sorted_programs = sorted (
342- self . programs . values () ,
364+ candidates ,
343365 key = lambda p : safe_numeric_average (p .metrics ),
344366 reverse = True ,
345367 )
@@ -379,6 +401,7 @@ def save(self, path: Optional[str] = None, iteration: int = 0) -> None:
379401 "islands" : [list (island ) for island in self .islands ],
380402 "archive" : list (self .archive ),
381403 "best_program_id" : self .best_program_id ,
404+ "island_best_programs" : self .island_best_programs ,
382405 "last_iteration" : iteration or self .last_iteration ,
383406 "current_island" : self .current_island ,
384407 "island_generations" : self .island_generations ,
@@ -412,6 +435,7 @@ def load(self, path: str) -> None:
412435 saved_islands = metadata .get ("islands" , [])
413436 self .archive = set (metadata .get ("archive" , []))
414437 self .best_program_id = metadata .get ("best_program_id" )
438+ self .island_best_programs = metadata .get ("island_best_programs" , [None ] * len (saved_islands ))
415439 self .last_iteration = metadata .get ("last_iteration" , 0 )
416440 self .current_island = metadata .get ("current_island" , 0 )
417441 self .island_generations = metadata .get ("island_generations" , [0 ] * len (saved_islands ))
@@ -440,6 +464,10 @@ def load(self, path: str) -> None:
440464 # Ensure island_generations list has correct length
441465 if len (self .island_generations ) != len (self .islands ):
442466 self .island_generations = [0 ] * len (self .islands )
467+
468+ # Ensure island_best_programs list has correct length
469+ if len (self .island_best_programs ) != len (self .islands ):
470+ self .island_best_programs = [None ] * len (self .islands )
443471
444472 logger .info (f"Loaded database with { len (self .programs )} programs from { path } " )
445473
@@ -748,6 +776,53 @@ def _update_best_program(self, program: Program) -> None:
748776 else :
749777 logger .info (f"New best program { program .id } replaces { old_id } " )
750778
779+ def _update_island_best_program (self , program : Program , island_idx : int ) -> None :
780+ """
781+ Update the best program tracking for a specific island
782+
783+ Args:
784+ program: Program to consider as the new best for the island
785+ island_idx: Island index
786+ """
787+ # Ensure island_idx is valid
788+ if island_idx >= len (self .island_best_programs ):
789+ logger .warning (f"Invalid island index { island_idx } , skipping island best update" )
790+ return
791+
792+ # If island doesn't have a best program yet, this becomes the best
793+ current_island_best_id = self .island_best_programs [island_idx ]
794+ if current_island_best_id is None :
795+ self .island_best_programs [island_idx ] = program .id
796+ logger .debug (f"Set initial best program for island { island_idx } to { program .id } " )
797+ return
798+
799+ # Check if current best still exists
800+ if current_island_best_id not in self .programs :
801+ logger .warning (
802+ f"Island { island_idx } best program { current_island_best_id } no longer exists, updating to { program .id } "
803+ )
804+ self .island_best_programs [island_idx ] = program .id
805+ return
806+
807+ current_island_best = self .programs [current_island_best_id ]
808+
809+ # Update if the new program is better
810+ if self ._is_better (program , current_island_best ):
811+ old_id = current_island_best_id
812+ self .island_best_programs [island_idx ] = program .id
813+
814+ # Log the change
815+ if "combined_score" in program .metrics and "combined_score" in current_island_best .metrics :
816+ old_score = current_island_best .metrics ["combined_score" ]
817+ new_score = program .metrics ["combined_score" ]
818+ score_diff = new_score - old_score
819+ logger .debug (
820+ f"Island { island_idx } : New best program { program .id } replaces { old_id } "
821+ f"(combined_score: { old_score :.4f} → { new_score :.4f} , +{ score_diff :.4f} )"
822+ )
823+ else :
824+ logger .debug (f"Island { island_idx } : New best program { program .id } replaces { old_id } " )
825+
751826 def _sample_parent (self ) -> Program :
752827 """
753828 Sample a parent program from the current island for the next evolution step
@@ -869,91 +944,124 @@ def _sample_random_parent(self) -> Program:
869944
870945 def _sample_inspirations (self , parent : Program , n : int = 5 ) -> List [Program ]:
871946 """
872- Sample inspiration programs for the next evolution step
947+ Sample inspiration programs for the next evolution step.
948+
949+ For proper island-based evolution, inspirations are sampled ONLY from the
950+ current island, maintaining genetic isolation between islands.
873951
874952 Args:
875953 parent: Parent program
876954 n: Number of inspirations to sample
877955
878956 Returns:
879- List of inspiration programs
957+ List of inspiration programs from the current island
880958 """
881959 inspirations = []
960+
961+ # Get the parent's island (should be current_island)
962+ parent_island = parent .metadata .get ("island" , self .current_island )
963+
964+ # Get all programs from the current island
965+ island_program_ids = list (self .islands [parent_island ])
966+ island_programs = [self .programs [pid ] for pid in island_program_ids if pid in self .programs ]
967+
968+ if not island_programs :
969+ logger .warning (f"Island { parent_island } has no programs for inspiration sampling" )
970+ return []
882971
883- # Always include the absolute best program if available and different from parent
972+ # Include the island's best program if available and different from parent
973+ island_best_id = self .island_best_programs [parent_island ]
884974 if (
885- self . best_program_id is not None
886- and self . best_program_id != parent .id
887- and self . best_program_id in self .programs
975+ island_best_id is not None
976+ and island_best_id != parent .id
977+ and island_best_id in self .programs
888978 ):
889- best_program = self .programs [self . best_program_id ]
890- inspirations .append (best_program )
891- logger .debug (f"Including best program { self . best_program_id } in inspirations" )
892- elif self . best_program_id is not None and self . best_program_id not in self .programs :
893- # Clean up stale best program reference
979+ island_best = self .programs [island_best_id ]
980+ inspirations .append (island_best )
981+ logger .debug (f"Including island { parent_island } best program { island_best_id } in inspirations" )
982+ elif island_best_id is not None and island_best_id not in self .programs :
983+ # Clean up stale island best reference
894984 logger .warning (
895- f"Best program { self . best_program_id } no longer exists, clearing reference"
985+ f"Island { parent_island } best program { island_best_id } no longer exists, clearing reference"
896986 )
897- self .best_program_id = None
987+ self .island_best_programs [ parent_island ] = None
898988
899- # Add top programs as inspirations
989+ # Add top programs from the island as inspirations
900990 top_n = max (1 , int (n * self .config .elite_selection_ratio ))
901- top_programs = self .get_top_programs (n = top_n )
902- for program in top_programs :
991+ top_island_programs = self .get_top_programs (n = top_n , island_idx = parent_island )
992+ for program in top_island_programs :
903993 if program .id not in [p .id for p in inspirations ] and program .id != parent .id :
904994 inspirations .append (program )
905995
906- # Add diverse programs using config.num_diverse_programs
907- if len (self .programs ) > n and len (inspirations ) < n :
908- # Calculate how many diverse programs to add (up to remaining slots)
996+ # Add diverse programs from within the island
997+ if len (island_programs ) > n and len (inspirations ) < n :
909998 remaining_slots = n - len (inspirations )
910999
911- # Sample from different feature cells for diversity
1000+ # Try to sample from different feature cells within the island
9121001 feature_coords = self ._calculate_feature_coords (parent )
913-
914- # Get programs from nearby feature cells
9151002 nearby_programs = []
916- for _ in range (remaining_slots ):
1003+
1004+ # Create a mapping of feature cells to island programs for efficient lookup
1005+ island_feature_map = {}
1006+ for prog_id in island_program_ids :
1007+ if prog_id in self .programs :
1008+ prog = self .programs [prog_id ]
1009+ prog_coords = self ._calculate_feature_coords (prog )
1010+ cell_key = self ._feature_coords_to_key (prog_coords )
1011+ island_feature_map [cell_key ] = prog_id
1012+
1013+ # Try to find programs from nearby feature cells within the island
1014+ for _ in range (remaining_slots * 3 ): # Try more times to find nearby programs
9171015 # Perturb coordinates
9181016 perturbed_coords = [
919- max (0 , min (self .feature_bins - 1 , c + random .randint (- 1 , 1 )))
1017+ max (0 , min (self .feature_bins - 1 , c + random .randint (- 2 , 2 )))
9201018 for c in feature_coords
9211019 ]
922-
923- # Try to get program from this cell
1020+
9241021 cell_key = self ._feature_coords_to_key (perturbed_coords )
925- if cell_key in self .feature_map :
926- program_id = self .feature_map [cell_key ]
927- # Check if program still exists before adding
1022+ if cell_key in island_feature_map :
1023+ program_id = island_feature_map [cell_key ]
9281024 if (
9291025 program_id != parent .id
9301026 and program_id not in [p .id for p in inspirations ]
1027+ and program_id not in [p .id for p in nearby_programs ]
9311028 and program_id in self .programs
9321029 ):
9331030 nearby_programs .append (self .programs [program_id ])
934- elif program_id not in self .programs :
935- # Clean up stale reference in feature_map
936- logger .debug (f"Removing stale program { program_id } from feature_map" )
937- del self .feature_map [cell_key ]
1031+ if len (nearby_programs ) >= remaining_slots :
1032+ break
9381033
939- # If we need more, add random programs
1034+ # If we still need more, add random programs from the island
9401035 if len (inspirations ) + len (nearby_programs ) < n :
9411036 remaining = n - len (inspirations ) - len (nearby_programs )
942- all_ids = set (self .programs .keys ())
1037+
1038+ # Get available programs from the island
9431039 excluded_ids = (
9441040 {parent .id }
9451041 .union (p .id for p in inspirations )
9461042 .union (p .id for p in nearby_programs )
9471043 )
948- available_ids = list (all_ids - excluded_ids )
949-
950- if available_ids :
951- random_ids = random .sample (available_ids , min (remaining , len (available_ids )))
1044+ available_island_ids = [
1045+ pid for pid in island_program_ids
1046+ if pid not in excluded_ids and pid in self .programs
1047+ ]
1048+
1049+ if available_island_ids :
1050+ random_ids = random .sample (
1051+ available_island_ids ,
1052+ min (remaining , len (available_island_ids ))
1053+ )
9521054 random_programs = [self .programs [pid ] for pid in random_ids ]
9531055 nearby_programs .extend (random_programs )
9541056
9551057 inspirations .extend (nearby_programs )
9561058
1059+ # Log island isolation info
1060+ logger .debug (
1061+ f"Sampled { len (inspirations )} inspirations from island { parent_island } "
1062+ f"(island has { len (island_programs )} programs total)"
1063+ )
1064+
9571065 return inspirations [:n ]
9581066
9591067 def _enforce_population_limit (self , exclude_program_id : Optional [str ] = None ) -> None :
@@ -1103,6 +1211,9 @@ def migrate_programs(self) -> None:
11031211 # Add to target island
11041212 self .islands [target_island ].add (migrant_copy .id )
11051213 self .programs [migrant_copy .id ] = migrant_copy
1214+
1215+ # Update island-specific best program if migrant is better
1216+ self ._update_island_best_program (migrant_copy , target_island )
11061217
11071218 logger .debug (
11081219 f"Migrated program { migrant .id } from island { i } to island { target_island } "
@@ -1214,10 +1325,13 @@ def log_island_status(self) -> None:
12141325 logger .info ("Island Status:" )
12151326 for stat in stats :
12161327 current_marker = " *" if stat ["is_current" ] else " "
1328+ island_idx = stat ['island' ]
1329+ island_best_id = self .island_best_programs [island_idx ] if island_idx < len (self .island_best_programs ) else None
1330+ best_indicator = f" (best: { island_best_id } )" if island_best_id else ""
12171331 logger .info (
12181332 f"{ current_marker } Island { stat ['island' ]} : { stat ['population_size' ]} programs, "
12191333 f"best={ stat ['best_score' ]:.4f} , avg={ stat ['average_score' ]:.4f} , "
1220- f"diversity={ stat ['diversity' ]:.2f} , gen={ stat ['generation' ]} "
1334+ f"diversity={ stat ['diversity' ]:.2f} , gen={ stat ['generation' ]} { best_indicator } "
12211335 )
12221336
12231337 # Artifact storage and retrieval methods
0 commit comments