@@ -130,6 +130,9 @@ def add(
130130
131131 self .programs [program .id ] = program
132132
133+ # Enforce population size limit
134+ self ._enforce_population_limit ()
135+
133136 # Calculate feature coordinates for MAP-Elites
134137 feature_coords = self ._calculate_feature_coords (program )
135138
@@ -552,25 +555,23 @@ def _sample_parent(self) -> Program:
552555 Returns:
553556 Parent program from current island
554557 """
555- # Decide between exploitation and exploration
556- if random .random () < self .config .exploitation_ratio and self .archive :
557- # Even for exploitation, prefer programs from current island
558- archive_programs_in_island = [
559- pid
560- for pid in self .archive
561- if pid in self .programs
562- and self .programs [pid ].metadata .get ("island" ) == self .current_island
563- ]
564-
565- if archive_programs_in_island :
566- parent_id = random .choice (archive_programs_in_island )
567- return self .programs [parent_id ]
568- else :
569- # Fall back to any archive program if current island has none
570- parent_id = random .choice (list (self .archive ))
571- return self .programs [parent_id ]
558+ # Use exploration_ratio and exploitation_ratio to decide sampling strategy
559+ rand_val = random .random ()
560+
561+ if rand_val < self .config .exploration_ratio :
562+ # EXPLORATION: Sample from current island (diverse sampling)
563+ return self ._sample_exploration_parent ()
564+ elif rand_val < self .config .exploration_ratio + self .config .exploitation_ratio :
565+ # EXPLOITATION: Sample from archive (elite programs)
566+ return self ._sample_exploitation_parent ()
567+ else :
568+ # RANDOM: Sample from any program (remaining probability)
569+ return self ._sample_random_parent ()
572570
573- # Exploration: Sample from current island only
571+ def _sample_exploration_parent (self ) -> Program :
572+ """
573+ Sample a parent for exploration (from current island)
574+ """
574575 current_island_programs = self .islands [self .current_island ]
575576
576577 if not current_island_programs :
@@ -589,6 +590,41 @@ def _sample_parent(self) -> Program:
589590 # Sample from current island
590591 parent_id = random .choice (list (current_island_programs ))
591592 return self .programs [parent_id ]
593+
594+ def _sample_exploitation_parent (self ) -> Program :
595+ """
596+ Sample a parent for exploitation (from archive/elite programs)
597+ """
598+ if not self .archive :
599+ # Fallback to exploration if no archive
600+ return self ._sample_exploration_parent ()
601+
602+ # Prefer programs from current island in archive
603+ archive_programs_in_island = [
604+ pid
605+ for pid in self .archive
606+ if pid in self .programs
607+ and self .programs [pid ].metadata .get ("island" ) == self .current_island
608+ ]
609+
610+ if archive_programs_in_island :
611+ parent_id = random .choice (archive_programs_in_island )
612+ return self .programs [parent_id ]
613+ else :
614+ # Fall back to any archive program if current island has none
615+ parent_id = random .choice (list (self .archive ))
616+ return self .programs [parent_id ]
617+
618+ def _sample_random_parent (self ) -> Program :
619+ """
620+ Sample a completely random parent from all programs
621+ """
622+ if not self .programs :
623+ raise ValueError ("No programs available for sampling" )
624+
625+ # Sample randomly from all programs
626+ program_id = random .choice (list (self .programs .keys ()))
627+ return self .programs [program_id ]
592628
593629 def _sample_inspirations (self , parent : Program , n : int = 5 ) -> List [Program ]:
594630 """
@@ -616,14 +652,17 @@ def _sample_inspirations(self, parent: Program, n: int = 5) -> List[Program]:
616652 if program .id not in [p .id for p in inspirations ] and program .id != parent .id :
617653 inspirations .append (program )
618654
619- # Add diverse programs
655+ # Add diverse programs using config.num_diverse_programs
620656 if len (self .programs ) > n and len (inspirations ) < n :
621- # Sample from different feature cells
657+ # Calculate how many diverse programs to add (up to remaining slots)
658+ remaining_slots = n - len (inspirations )
659+
660+ # Sample from different feature cells for diversity
622661 feature_coords = self ._calculate_feature_coords (parent )
623662
624663 # Get programs from nearby feature cells
625664 nearby_programs = []
626- for _ in range (n - len ( inspirations ) ):
665+ for _ in range (remaining_slots ):
627666 # Perturb coordinates
628667 perturbed_coords = [
629668 max (0 , min (self .feature_bins - 1 , c + random .randint (- 1 , 1 )))
@@ -657,6 +696,70 @@ def _sample_inspirations(self, parent: Program, n: int = 5) -> List[Program]:
657696
658697 return inspirations [:n ]
659698
699+ def _enforce_population_limit (self ) -> None :
700+ """
701+ Enforce the population size limit by removing worst programs if needed
702+ """
703+ if len (self .programs ) <= self .config .population_size :
704+ return
705+
706+ # Calculate how many programs to remove
707+ num_to_remove = len (self .programs ) - self .config .population_size
708+
709+ logger .info (f"Population size ({ len (self .programs )} ) exceeds limit ({ self .config .population_size } ), removing { num_to_remove } programs" )
710+
711+ # Get programs sorted by fitness (worst first)
712+ all_programs = list (self .programs .values ())
713+
714+ # Sort by average metric (worst first)
715+ sorted_programs = sorted (
716+ all_programs ,
717+ key = lambda p : sum (p .metrics .values ()) / max (1 , len (p .metrics )) if p .metrics else 0.0
718+ )
719+
720+ # Remove worst programs, but never remove the best program
721+ programs_to_remove = []
722+ for program in sorted_programs :
723+ if len (programs_to_remove ) >= num_to_remove :
724+ break
725+ # Don't remove the best program
726+ if program .id != self .best_program_id :
727+ programs_to_remove .append (program )
728+
729+ # If we still need to remove more and only have the best program protected,
730+ # remove from the remaining programs anyway (but keep the absolute best)
731+ if len (programs_to_remove ) < num_to_remove :
732+ remaining_programs = [p for p in sorted_programs if p not in programs_to_remove and p .id != self .best_program_id ]
733+ additional_removals = remaining_programs [:num_to_remove - len (programs_to_remove )]
734+ programs_to_remove .extend (additional_removals )
735+
736+ # Remove the selected programs
737+ for program in programs_to_remove :
738+ program_id = program .id
739+
740+ # Remove from main programs dict
741+ if program_id in self .programs :
742+ del self .programs [program_id ]
743+
744+ # Remove from feature map
745+ keys_to_remove = []
746+ for key , pid in self .feature_map .items ():
747+ if pid == program_id :
748+ keys_to_remove .append (key )
749+ for key in keys_to_remove :
750+ del self .feature_map [key ]
751+
752+ # Remove from islands
753+ for island in self .islands :
754+ island .discard (program_id )
755+
756+ # Remove from archive
757+ self .archive .discard (program_id )
758+
759+ logger .debug (f"Removed program { program_id } due to population limit" )
760+
761+ logger .info (f"Population size after cleanup: { len (self .programs )} " )
762+
660763 # Island management methods
661764 def set_current_island (self , island_idx : int ) -> None :
662765 """Set which island is currently being evolved"""
0 commit comments