@@ -91,6 +91,69 @@ def compute_spawn(adjusted_fitness, previous_sizes, pop_size, min_species_size):
9191
9292 return spawn_amounts
9393
94+ def _adjust_spawn_exact (self , spawn_amounts , pop_size , min_species_size ):
95+ """Adjust per-species spawn counts so that their sum matches pop_size exactly.
96+
97+ This adjustment preserves the per-species minimum (min_species_size) and biases
98+ changes as follows:
99+ - When adding individuals (total < pop_size), smaller species are incremented first.
100+ - When removing individuals (total > pop_size), larger species are decremented first.
101+ """
102+ total_spawn = sum (spawn_amounts )
103+ if total_spawn == pop_size :
104+ return spawn_amounts
105+
106+ num_species = len (spawn_amounts )
107+ min_total = num_species * min_species_size
108+ if min_total > pop_size :
109+ raise RuntimeError (
110+ "Configuration conflict: population size {0} is less than "
111+ "num_species * min_species_size {1} ({2} * {3}). Cannot satisfy per-species minima." .format (
112+ pop_size , min_total , num_species , min_species_size
113+ )
114+ )
115+
116+ diff = pop_size - total_spawn
117+ indexed = list (enumerate (spawn_amounts ))
118+
119+ if diff > 0 :
120+ # Too few genomes overall: give extras to smaller species first.
121+ indexed .sort (key = lambda x : x [1 ]) # ascending by current spawn size
122+ i = 0
123+ while diff > 0 and indexed :
124+ idx , val = indexed [i ]
125+ val += 1
126+ spawn_amounts [idx ] = val
127+ indexed [i ] = (idx , val )
128+ diff -= 1
129+ i = (i + 1 ) % len (indexed )
130+ else :
131+ # Too many genomes overall: remove from larger species first.
132+ remaining = - diff
133+ indexed .sort (key = lambda x : x [1 ], reverse = True ) # descending by current spawn size
134+ i = 0
135+ # We know a solution should exist whenever min_total <= pop_size, but
136+ # guard against pathological rounding behaviour with a safety break.
137+ while remaining > 0 and indexed :
138+ idx , val = indexed [i ]
139+ if val > min_species_size :
140+ val -= 1
141+ spawn_amounts [idx ] = val
142+ indexed [i ] = (idx , val )
143+ remaining -= 1
144+ i = (i + 1 ) % len (indexed )
145+ if i == 0 and remaining > 0 :
146+ # Could not adjust further without violating the per-species minimum.
147+ break
148+
149+ if sum (spawn_amounts ) != pop_size :
150+ raise RuntimeError (
151+ "Internal error adjusting spawn counts: could not match pop_size={0} "
152+ "with min_species_size={1}" .format (pop_size , min_species_size )
153+ )
154+
155+ return spawn_amounts
156+
94157 def reproduce (self , config , species , pop_size , generation ):
95158 """
96159 Handles creation of genomes, either from scratch or by sexual or
@@ -156,6 +219,9 @@ def reproduce(self, config, species, pop_size, generation):
156219 min_species_size = max (min_species_size , self .reproduction_config .elitism )
157220 spawn_amounts = self .compute_spawn (adjusted_fitnesses , previous_sizes ,
158221 pop_size , min_species_size )
222+ # Adjust spawn counts so that the total exactly matches the requested
223+ # population size while respecting the per-species minimum.
224+ spawn_amounts = self ._adjust_spawn_exact (spawn_amounts , pop_size , min_species_size )
159225
160226 new_population = {}
161227 species .species = {}
0 commit comments