View Javadoc
1   package net.bmahe.genetics4j.neat.selection;
2   
3   import java.util.ArrayList;
4   import java.util.Comparator;
5   import java.util.List;
6   import java.util.Objects;
7   import java.util.random.RandomGenerator;
8   import java.util.stream.IntStream;
9   
10  import org.apache.commons.lang3.Validate;
11  import org.apache.logging.log4j.LogManager;
12  import org.apache.logging.log4j.Logger;
13  
14  import net.bmahe.genetics4j.core.Genotype;
15  import net.bmahe.genetics4j.core.Individual;
16  import net.bmahe.genetics4j.core.Population;
17  import net.bmahe.genetics4j.core.selection.Selector;
18  import net.bmahe.genetics4j.core.spec.AbstractEAConfiguration;
19  import net.bmahe.genetics4j.core.util.IndividualUtils;
20  import net.bmahe.genetics4j.neat.NeatUtils;
21  import net.bmahe.genetics4j.neat.Species;
22  import net.bmahe.genetics4j.neat.SpeciesIdGenerator;
23  import net.bmahe.genetics4j.neat.spec.selection.NeatSelection;
24  
25  /**
26   * Concrete implementation of species-based selection for NEAT (NeuroEvolution of Augmenting Topologies) algorithm.
27   * 
28   * <p>NeatSelectorImpl implements the core species-based selection mechanism that is fundamental to NEAT's ability to
29   * maintain population diversity and protect structural innovations. It organizes the population into species based on
30   * genetic compatibility, applies fitness sharing within species, manages species lifecycle, and allocates reproduction
31   * opportunities across species.
32   * 
33   * <p>NEAT selection process:
34   * <ol>
35   * <li><strong>Species assignment</strong>: Organize population into species using compatibility predicate</li>
36   * <li><strong>Species trimming</strong>: Remove lower-performing individuals within each species</li>
37   * <li><strong>Species filtering</strong>: Eliminate species below minimum viable size</li>
38   * <li><strong>Reproduction allocation</strong>: Determine number of offspring for each species</li>
39   * <li><strong>Parent selection</strong>: Select parents within species using configured selector</li>
40   * <li><strong>Species maintenance</strong>: Update species representatives for next generation</li>
41   * </ol>
42   * 
43   * <p>Key features:
44   * <ul>
45   * <li><strong>Genetic compatibility</strong>: Groups individuals with similar network topologies</li>
46   * <li><strong>Fitness sharing</strong>: Reduces competition between similar individuals</li>
47   * <li><strong>Innovation protection</strong>: Allows new topologies time to optimize</li>
48   * <li><strong>Population diversity</strong>: Maintains multiple species exploring different solutions</li>
49   * </ul>
50   * 
51   * <p>Species management:
52   * <ul>
53   * <li><strong>Species formation</strong>: Creates new species when compatibility threshold exceeded</li>
54   * <li><strong>Species growth</strong>: Assigns compatible individuals to existing species</li>
55   * <li><strong>Species extinction</strong>: Eliminates species that fall below minimum size</li>
56   * <li><strong>Species continuity</strong>: Maintains species representatives across generations</li>
57   * </ul>
58   * 
59   * <p>Common usage patterns:
60   * 
61   * <pre>{@code
62   * // Create NEAT selector implementation
63   * RandomGenerator randomGen = RandomGenerator.getDefault();
64   * SpeciesIdGenerator speciesIdGen = new SpeciesIdGenerator();
65   * 
66   * // Configure species predicate for compatibility
67   * BiPredicate<Individual<Double>, Individual<Double>> speciesPredicate = (i1,
68   * 		i2) -> NeatUtils.compatibilityDistance(i1.genotype(), i2.genotype(), 0, 2, 2, 1.0f) < 3.0; // Compatibility
69   * 																																	// threshold
70   * 
71   * // Configure NEAT selection policy
72   * NeatSelection<Double> neatSelection = NeatSelection.<Double>builder()
73   * 		.perSpeciesKeepRatio(0.8f) // Keep top 80% of each species
74   * 		.minSpeciesSize(5) // Minimum 5 individuals per species
75   * 		.speciesPredicate(speciesPredicate)
76   * 		.speciesSelection(Tournament.of(3)) // Tournament within species
77   * 		.build();
78   * 
79   * // Create within-species selector
80   * Selector<Double> speciesSelector = new TournamentSelector<>(randomGen, 3);
81   * 
82   * // Create NEAT selector
83   * NeatSelectorImpl<Double> selector = new NeatSelectorImpl<>(randomGen, neatSelection, speciesIdGen, speciesSelector);
84   * 
85   * // Use in population selection
86   * Population<Double> selectedPopulation = selector.select(eaConfiguration, population, 100 // Select 100 individuals
87   * );
88   * }</pre>
89   * 
90   * <p>Species lifecycle management:
91   * <ul>
92   * <li><strong>Initialization</strong>: Empty species list for first generation</li>
93   * <li><strong>Assignment</strong>: Individuals assigned to species based on compatibility</li>
94   * <li><strong>Trimming</strong>: Poor performers removed while preserving species diversity</li>
95   * <li><strong>Reproduction</strong>: Offspring allocated proportionally to species average fitness</li>
96   * <li><strong>Evolution</strong>: Species composition changes as population evolves</li>
97   * </ul>
98   * 
99   * <p>Fitness sharing mechanism:
100  * <ul>
101  * <li><strong>Within-species competition</strong>: Individuals primarily compete within their species</li>
102  * <li><strong>Species-based allocation</strong>: Reproduction opportunities distributed across species</li>
103  * <li><strong>Diversity protection</strong>: Prevents single topology from dominating population</li>
104  * <li><strong>Innovation preservation</strong>: New structural innovations get time to optimize</li>
105  * </ul>
106  * 
107  * <p>Performance considerations:
108  * <ul>
109  * <li><strong>Compatibility caching</strong>: Species assignment optimized for repeated use</li>
110  * <li><strong>Efficient trimming</strong>: In-place species membership updates</li>
111  * <li><strong>Memory management</strong>: Species structures reused across generations</li>
112  * <li><strong>Parallel processing</strong>: Species-based organization enables concurrent operations</li>
113  * </ul>
114  * 
115  * @param <T> the fitness value type (typically Double)
116  * @see NeatSelection
117  * @see Species
118  * @see SpeciesIdGenerator
119  * @see Selector
120  */
121 public class NeatSelectorImpl<T extends Number & Comparable<T>> implements Selector<T> {
122 	public static final Logger logger = LogManager.getLogger(NeatSelectorImpl.class);
123 
124 	private final RandomGenerator randomGenerator;
125 	private final NeatSelection<T> neatSelection;
126 	private final SpeciesIdGenerator speciesIdGenerator;
127 	private final Selector<T> speciesSelector;
128 
129 	private List<Species<T>> previousSpecies;
130 
131 	/**
132 	 * Constructs a new NEAT selector implementation with the specified components.
133 	 * 
134 	 * <p>The selector uses the random generator for stochastic decisions, the NEAT selection policy for species
135 	 * management parameters, the species ID generator for creating new species, and the species selector for
136 	 * within-species parent selection.
137 	 * 
138 	 * @param _randomGenerator    random number generator for stochastic selection operations
139 	 * @param _neatSelection      NEAT selection policy defining species management parameters
140 	 * @param _speciesIdGenerator generator for unique species identifiers
141 	 * @param _speciesSelector    selector for choosing parents within each species
142 	 * @throws IllegalArgumentException if any parameter is null
143 	 */
144 	public NeatSelectorImpl(final RandomGenerator _randomGenerator,
145 			final NeatSelection<T> _neatSelection,
146 			final SpeciesIdGenerator _speciesIdGenerator,
147 			final Selector<T> _speciesSelector) {
148 		Objects.requireNonNull(_randomGenerator);
149 		Objects.requireNonNull(_neatSelection);
150 		Objects.requireNonNull(_speciesIdGenerator);
151 		Objects.requireNonNull(_speciesSelector);
152 
153 		this.randomGenerator = _randomGenerator;
154 		this.neatSelection = _neatSelection;
155 		this.speciesIdGenerator = _speciesIdGenerator;
156 		this.speciesSelector = _speciesSelector;
157 
158 		this.previousSpecies = new ArrayList<>();
159 	}
160 
161 	/**
162 	 * Trims a species by removing lower-performing individuals while preserving minimum viable size.
163 	 * 
164 	 * <p>This method applies fitness-based selection within a species, keeping the best performers while ensuring the
165 	 * species maintains sufficient size for genetic diversity. The number of individuals kept is the maximum of the
166 	 * minimum species size and the keep ratio applied to the current species size.
167 	 * 
168 	 * @param species             the species to trim
169 	 * @param comparator          comparator for ranking individuals by fitness
170 	 * @param minSpeciesSize      minimum number of individuals to keep
171 	 * @param perSpeciesKeepRatio proportion of current species to preserve
172 	 * @return a new species containing only the selected individuals
173 	 * @throws IllegalArgumentException if species is null
174 	 */
175 	protected Species<T> trimSpecies(final Species<T> species, final Comparator<Individual<T>> comparator,
176 			final int minSpeciesSize, final float perSpeciesKeepRatio) {
177 		Objects.requireNonNull(species);
178 
179 		final List<Individual<T>> members = species.getMembers();
180 		final float speciesSize = members.size();
181 		final int numIndividualtoKeep = (int) Math.max(minSpeciesSize, speciesSize * perSpeciesKeepRatio);
182 
183 		if (logger.isDebugEnabled()) {
184 			logger.debug(
185 					"Species id: {}, size: {}, perSepciesKeepRatio: {}, we want to keep {} members - best fitness: {}",
186 					species.getId(),
187 					speciesSize,
188 					perSpeciesKeepRatio,
189 					numIndividualtoKeep,
190 					members.stream().max(comparator).map(Individual::fitness));
191 		}
192 
193 		final Species<T> trimmedSpecies = new Species<>(species.getId(), List.of());
194 		if (numIndividualtoKeep > 0) {
195 			final var selectedIndividuals = members.stream()
196 					.sorted(comparator.reversed())
197 					.limit(numIndividualtoKeep)
198 					.toList();
199 
200 			trimmedSpecies.addAllMembers(selectedIndividuals);
201 		}
202 		return trimmedSpecies;
203 
204 	}
205 
206 	/**
207 	 * Eliminates the lowest-performing individuals from all species while maintaining species diversity.
208 	 * 
209 	 * <p>This method applies the species trimming process to all species in the population, removing poor performers
210 	 * within each species while preserving the overall species structure. Species that become empty after trimming are
211 	 * filtered out.
212 	 * 
213 	 * @param eaConfiguration evolutionary algorithm configuration containing fitness comparator
214 	 * @param allSpecies      list of all species in the population
215 	 * @return list of species after trimming, with empty species removed
216 	 * @throws IllegalArgumentException if any parameter is null
217 	 */
218 	protected List<Species<T>> eliminateLowestPerformers(final AbstractEAConfiguration<T> eaConfiguration,
219 			final List<Species<T>> allSpecies) {
220 		Objects.requireNonNull(eaConfiguration);
221 		Objects.requireNonNull(allSpecies);
222 
223 		final Comparator<Individual<T>> comparator = IndividualUtils.fitnessBasedComparator(eaConfiguration);
224 
225 		final float perSpeciesKeepRatio = neatSelection.perSpeciesKeepRatio();
226 		logger.trace("Keeping only the best {} number of individuals per species", perSpeciesKeepRatio);
227 
228 		final int minSpeciesSize = neatSelection.minSpeciesSize();
229 
230 		return allSpecies.stream()
231 				.map(species -> trimSpecies(species, comparator, minSpeciesSize, perSpeciesKeepRatio))
232 				.filter(species -> species.getNumMembers() > 0)
233 				.toList();
234 	}
235 
236 	@Override
237 	public Population<T> select(final AbstractEAConfiguration<T> eaConfiguration, final long generation,
238 			final int numIndividuals, final List<Genotype> genotypes, final List<T> fitnessScore) {
239 		Objects.requireNonNull(eaConfiguration);
240 		Objects.requireNonNull(genotypes);
241 		Objects.requireNonNull(fitnessScore);
242 		Validate.isTrue(generation >= 0);
243 		Validate.isTrue(numIndividuals > 0);
244 		Validate.isTrue(genotypes.size() == fitnessScore.size());
245 
246 		final Population<T> population = Population.of(genotypes, fitnessScore);
247 
248 		final List<Species<T>> allSpecies = NeatUtils.speciate(
249 				randomGenerator,
250 				speciesIdGenerator,
251 				previousSpecies,
252 				population,
253 				neatSelection.speciesPredicate());
254 
255 		logger.debug("Number of species found: {}", allSpecies.size());
256 		logger.trace("Species: {}", allSpecies);
257 
258 		/**
259 		 * We want to remove the bottom performers of each species
260 		 */
261 		final var allTrimmedSpecies = eliminateLowestPerformers(eaConfiguration, allSpecies);
262 		logger.debug("After trimming, we have {} species", allTrimmedSpecies.size());
263 
264 		previousSpecies = allTrimmedSpecies;
265 		if (allTrimmedSpecies.isEmpty()) {
266 			return Population.empty();
267 		}
268 
269 		/**
270 		 * Now we want to select the next generation on a per species basis and proportionally to the sum of the fitnesses
271 		 * of each members
272 		 */
273 
274 		final double[] sumFitnesses = new double[allTrimmedSpecies.size()];
275 		double totalSum = 0;
276 		for (int i = 0; i < allTrimmedSpecies.size(); i++) {
277 			final var species = allTrimmedSpecies.get(i);
278 			sumFitnesses[i] = species.getMembers()
279 					.stream()
280 					.mapToDouble(individual -> individual.fitness().doubleValue())
281 					.sum() / (float) species.getNumMembers();
282 			totalSum += sumFitnesses[i];
283 		}
284 
285 		final List<Integer> decreasingFitnessIndex = IntStream.range(0, sumFitnesses.length)
286 				.boxed()
287 				.sorted(Comparator.comparing(i -> sumFitnesses[(int) i]).reversed())
288 				.toList();
289 
290 		final Population<T> selected = new Population<>();
291 		final List<Integer> selectedSpeciesIds = new ArrayList<>();
292 
293 		final List<Population<T>> trimmedPopulations = allTrimmedSpecies.stream()
294 				.map(species -> Population.of(species.getMembers()))
295 				.toList();
296 
297 		final boolean strictSpeciation = neatSelection.interspeciesMatingRate() == 0.0f;
298 
299 		int i = 0;
300 		while (selected.size() < numIndividuals && i < sumFitnesses.length) {
301 			int speciesIndex = decreasingFitnessIndex.get(i);
302 
303 			int numIndividualSpecies = (int) (numIndividuals * sumFitnesses[speciesIndex] / totalSum);
304 			if (numIndividualSpecies > numIndividuals - selected.size()) {
305 				numIndividualSpecies = numIndividuals - selected.size();
306 			}
307 
308 			if (numIndividualSpecies > 0) {
309 				if (strictSpeciation && numIndividualSpecies > 1) {
310 					numIndividualSpecies = numIndividualSpecies - (numIndividualSpecies % 2);
311 				}
312 
313 				if (numIndividualSpecies > 0) {
314 					final Population<T> speciesPopulation = trimmedPopulations.get(speciesIndex);
315 					final int speciesId = allTrimmedSpecies.get(speciesIndex).getId();
316 
317 					logger.debug(
318 							"sub selecting {} for index {} - species id: {}",
319 							numIndividualSpecies,
320 							speciesIndex,
321 							speciesId);
322 
323 					final var selectedFromSpecies = speciesSelector.select(
324 							eaConfiguration,
325 							generation,
326 							numIndividualSpecies,
327 							speciesPopulation.getAllGenotypes(),
328 							speciesPopulation.getAllFitnesses());
329 
330 					selected.addAll(selectedFromSpecies);
331 					for (int j = 0; j < selectedFromSpecies.size(); j++) {
332 						selectedSpeciesIds.add(speciesId);
333 					}
334 				}
335 			}
336 
337 			i++;
338 		}
339 
340 		if (selected.size() < numIndividuals) {
341 			logger.debug(
342 					"There are less selected individual [{}] than desired [{}]. Will include additional invididuals",
343 					selected.size(),
344 					numIndividuals);
345 			final Population<T> speciesPopulation = trimmedPopulations.get(decreasingFitnessIndex.getFirst());
346 			final int speciesId = allTrimmedSpecies.get(decreasingFitnessIndex.getFirst()).getId();
347 
348 			int remainder = numIndividuals - selected.size();
349 			if (strictSpeciation && remainder > 1) {
350 				remainder = remainder - (remainder % 2);
351 			}
352 
353 			if (remainder > 0) {
354 				final var additionalSelected = speciesSelector.select(
355 						eaConfiguration,
356 						generation,
357 						remainder,
358 						speciesPopulation.getAllGenotypes(),
359 						speciesPopulation.getAllFitnesses());
360 
361 				selected.addAll(additionalSelected);
362 				for (int j = 0; j < additionalSelected.size(); j++) {
363 					selectedSpeciesIds.add(speciesId);
364 				}
365 			}
366 		}
367 
368 		if (strictSpeciation == false && selected.size() >= 2) {
369 			applyInterspeciesMating(selected, selectedSpeciesIds, neatSelection.interspeciesMatingRate());
370 		}
371 
372 		return selected;
373 	}
374 
375 	/**
376 	 * Randomly introduces cross-species pairs by swapping the second individual of some pairs with an individual from a
377 	 * different species.
378 	 *
379 	 * <p>{@code EASystem} pairs consecutive indices (0+1, 2+3, ...). The selected population is ordered by species, so
380 	 * by default pairs stay within species. For each pair, with probability {@code rate}, the second individual is
381 	 * swapped
382 	 * with a randomly chosen individual from a different species elsewhere in the population.
383 	 *
384 	 * @param selected           the selected population, modified in place
385 	 * @param selectedSpeciesIds species id parallel to selected, modified in place
386 	 * @param rate               probability of interspecies mating per pair
387 	 */
388 	private void applyInterspeciesMating(final Population<T> selected, final List<Integer> selectedSpeciesIds,
389 			final float rate) {
390 		final List<Genotype> genotypes = selected.getAllGenotypes();
391 		final List<T> fitnesses = selected.getAllFitnesses();
392 
393 		for (int pairStart = 0; pairStart + 1 < selected.size(); pairStart += 2) {
394 			if (randomGenerator.nextFloat() >= rate) {
395 				continue;
396 			}
397 
398 			final int firstSpeciesId = selectedSpeciesIds.get(pairStart);
399 			int swapCandidate = randomGenerator.nextInt(selected.size());
400 			int attempts = 0;
401 			while (selectedSpeciesIds.get(swapCandidate) == firstSpeciesId && attempts < selected.size()) {
402 				swapCandidate = randomGenerator.nextInt(selected.size());
403 				attempts++;
404 			}
405 
406 			if (selectedSpeciesIds.get(swapCandidate) == firstSpeciesId) {
407 				continue;
408 			}
409 
410 			final int secondIndex = pairStart + 1;
411 			swap(genotypes, fitnesses, selectedSpeciesIds, secondIndex, swapCandidate);
412 		}
413 	}
414 
415 	private void swap(final List<Genotype> genotypes, final List<T> fitnesses, final List<Integer> speciesIds,
416 			final int i, final int j) {
417 		final Genotype tempGenotype = genotypes.get(i);
418 		genotypes.set(i, genotypes.get(j));
419 		genotypes.set(j, tempGenotype);
420 
421 		final T tempFitness = fitnesses.get(i);
422 		fitnesses.set(i, fitnesses.get(j));
423 		fitnesses.set(j, tempFitness);
424 
425 		final int tempSpeciesId = speciesIds.get(i);
426 		speciesIds.set(i, speciesIds.get(j));
427 		speciesIds.set(j, tempSpeciesId);
428 	}
429 }