View Javadoc
1   package net.bmahe.genetics4j.core;
2   
3   import java.util.ArrayList;
4   import java.util.Arrays;
5   import java.util.List;
6   import java.util.Objects;
7   import java.util.Optional;
8   import java.util.stream.IntStream;
9   
10  import org.apache.commons.collections4.CollectionUtils;
11  import org.apache.commons.lang3.Validate;
12  import org.apache.logging.log4j.LogManager;
13  import org.apache.logging.log4j.Logger;
14  
15  import net.bmahe.genetics4j.core.chromosomes.Chromosome;
16  import net.bmahe.genetics4j.core.chromosomes.factory.ChromosomeFactoryProvider;
17  import net.bmahe.genetics4j.core.combination.ChromosomeCombinator;
18  import net.bmahe.genetics4j.core.combination.GenotypeCombinator;
19  import net.bmahe.genetics4j.core.evaluation.FitnessEvaluator;
20  import net.bmahe.genetics4j.core.evolutionlisteners.EvolutionListener;
21  import net.bmahe.genetics4j.core.mutation.Mutator;
22  import net.bmahe.genetics4j.core.replacement.ReplacementStrategyImplementor;
23  import net.bmahe.genetics4j.core.selection.Selector;
24  import net.bmahe.genetics4j.core.spec.AbstractEAConfiguration;
25  import net.bmahe.genetics4j.core.spec.AbstractEAExecutionContext;
26  import net.bmahe.genetics4j.core.spec.EvolutionResult;
27  import net.bmahe.genetics4j.core.spec.ImmutableEvolutionResult;
28  import net.bmahe.genetics4j.core.spec.PostEvaluationProcessor;
29  import net.bmahe.genetics4j.core.termination.Termination;
30  import net.bmahe.genetics4j.core.util.GenotypeGenerator;
31  
32  /**
33   * Main orchestrator class for evolutionary algorithms, managing the complete evolution process.
34   * 
35   * <p>EASystem serves as the central coordinator that brings together all components of an evolutionary algorithm
36   * including genetic operators, selection strategies, evaluation functions, and termination criteria. It manages the
37   * evolutionary cycle and provides a unified interface for running optimizations.
38   * 
39   * <p>The system coordinates the following evolutionary process:
40   * <ol>
41   * <li><strong>Initialization</strong>: Generate initial population of random genotypes</li>
42   * <li><strong>Evaluation</strong>: Compute fitness values for all individuals</li>
43   * <li><strong>Selection</strong>: Choose parents for reproduction based on fitness</li>
44   * <li><strong>Reproduction</strong>: Create offspring through crossover and mutation</li>
45   * <li><strong>Replacement</strong>: Integrate offspring into next generation</li>
46   * <li><strong>Termination check</strong>: Determine if stopping criteria are met</li>
47   * <li><strong>Iteration</strong>: Repeat until termination conditions are satisfied</li>
48   * </ol>
49   * 
50   * <p>Key responsibilities include:
51   * <ul>
52   * <li><strong>Population management</strong>: Maintaining population size and diversity</li>
53   * <li><strong>Genetic operator coordination</strong>: Applying crossover, mutation, and selection</li>
54   * <li><strong>Fitness evaluation orchestration</strong>: Managing parallel and synchronous evaluation</li>
55   * <li><strong>Evolution monitoring</strong>: Providing hooks for logging and progress tracking</li>
56   * <li><strong>Resource management</strong>: Efficient memory usage and computation distribution</li>
57   * </ul>
58   * 
59   * <p>Configuration components:
60   * <ul>
61   * <li><strong>EAConfiguration</strong>: Defines genetic representation and operator policies</li>
62   * <li><strong>EAExecutionContext</strong>: Provides runtime services and factory implementations</li>
63   * <li><strong>FitnessEvaluator</strong>: Computes quality measures for candidate solutions</li>
64   * <li><strong>Termination criteria</strong>: Determines when to stop evolution</li>
65   * </ul>
66   * 
67   * <p>The system supports various evolutionary paradigms:
68   * <ul>
69   * <li><strong>Genetic Algorithms</strong>: Traditional binary and real-valued optimization</li>
70   * <li><strong>Genetic Programming</strong>: Evolution of tree-structured programs</li>
71   * <li><strong>Evolution Strategies</strong>: Real-valued optimization with adaptive parameters</li>
72   * <li><strong>Multi-objective optimization</strong>: Pareto-based optimization with multiple objectives</li>
73   * </ul>
74   * 
75   * <p>Example usage:
76   * 
77   * <pre>{@code
78   * // Configure the evolutionary algorithm
79   * EAConfiguration<Double> config = EAConfigurationBuilder.<Double>builder()
80   *     .chromosomeSpecs(DoubleChromosomeSpec.of(10, -5.0, 5.0))
81   *     .parentSelectionPolicy(Tournament.of(3))
82   *     .mutationPolicy(RandomMutation.of(0.1))
83   *     .build();
84   * 
85   * // Set up execution context
86   * EAExecutionContext<Double> context = EAExecutionContexts.forScalarFitness();
87   * 
88   * // Create fitness evaluator
89   * Fitness<Double> fitness = genotype -> {
90   *     // Implement problem-specific fitness function
91   *     return computeFitness(genotype);
92   * };
93   * 
94   * // Build and run the evolutionary system
95   * EASystem<Double> system = EASystemFactory.from(config, context, fitness);
96   * EvolutionResult<Double> result = system.evolve(
97   *     populationSize: 100,
98   *     termination: Terminations.generations(1000)
99   * );
100  * }</pre>
101  * 
102  * <p>Thread safety: EASystem instances are generally not thread-safe and should not be shared between multiple threads
103  * without external synchronization. However, the system supports parallel fitness evaluation internally when configured
104  * appropriately.
105  * 
106  * @param <T> the type of fitness values, must be comparable for selection and ranking
107  * @see EASystemFactory
108  * @see net.bmahe.genetics4j.core.spec.EAConfiguration
109  * @see net.bmahe.genetics4j.core.spec.EAExecutionContext
110  * @see FitnessEvaluator
111  * @see Termination
112  */
113 public class EASystem<T extends Comparable<T>> {
114 	public static final Logger logger = LogManager.getLogger(EASystem.class);
115 
116 	private final GenotypeGenerator<T> genotypeGenerator;
117 	private final FitnessEvaluator<T> fitnessEvaluator;
118 	private final AbstractEAConfiguration<T> eaConfiguration;
119 	private final AbstractEAExecutionContext<T> eaExecutionContext;
120 	private final int populationSize;
121 
122 	private final List<ChromosomeCombinator<T>> chromosomeCombinators;
123 	private final ChromosomeFactoryProvider chromosomeFactoryProvider;
124 
125 	private final ReplacementStrategyImplementor<T> replacementStrategyImplementor;
126 
127 	private final List<Mutator> mutators;
128 
129 	private final double offspringRatio;
130 
131 	private final Selector<T> parentSelector;
132 
133 	public EASystem(final AbstractEAConfiguration<T> _eaConfiguration,
134 			final long _populationSize,
135 			final List<ChromosomeCombinator<T>> _chromosomeCombinators,
136 			final double _offspringRatio,
137 			final Selector<T> _parentSelectionPolicyHandler,
138 			final List<Mutator> _mutators,
139 			final ReplacementStrategyImplementor<T> _replacementStrategyImplementor,
140 			final AbstractEAExecutionContext<T> _eaExecutionContext,
141 			final FitnessEvaluator<T> _fitnessEvaluator) {
142 		Objects.requireNonNull(_eaConfiguration);
143 		Validate.isTrue(_populationSize > 0);
144 		Objects.requireNonNull(_chromosomeCombinators);
145 		Validate.isTrue(_chromosomeCombinators.size() == _eaConfiguration.numChromosomes());
146 		Validate.inclusiveBetween(0.0, 1.0, _offspringRatio);
147 		Objects.requireNonNull(_parentSelectionPolicyHandler);
148 		Objects.requireNonNull(_replacementStrategyImplementor);
149 		Objects.requireNonNull(_eaExecutionContext);
150 		Objects.requireNonNull(_fitnessEvaluator);
151 
152 		this.eaConfiguration = _eaConfiguration;
153 		this.eaExecutionContext = _eaExecutionContext;
154 		this.populationSize = (int) _populationSize;
155 		this.chromosomeCombinators = _chromosomeCombinators;
156 		this.offspringRatio = _offspringRatio;
157 		this.mutators = _mutators;
158 		this.chromosomeFactoryProvider = _eaExecutionContext.chromosomeFactoryProvider();
159 		this.fitnessEvaluator = _fitnessEvaluator;
160 
161 		parentSelector = _parentSelectionPolicyHandler;
162 
163 		this.replacementStrategyImplementor = _replacementStrategyImplementor;
164 		this.genotypeGenerator = new GenotypeGenerator<>(chromosomeFactoryProvider, eaConfiguration);
165 	}
166 
167 	private List<T> evaluate(final long generation, final List<Genotype> population) {
168 		Validate.isTrue(generation >= 0);
169 		Objects.requireNonNull(population);
170 		Validate.isTrue(population.isEmpty() == false);
171 
172 		logger.debug("Evaluating population of size {}", population.size());
173 		final List<T> fitnesses = fitnessEvaluator.evaluate(generation, population);
174 
175 		logger.debug("Done evaluating population of size {}", population.size());
176 		return fitnesses;
177 	}
178 
179 	private List<Genotype> initializePopulation() {
180 		final int initialPopulationSize = eaExecutionContext.populationSize();
181 		logger.info("Generating initial population of {} individuals", initialPopulationSize);
182 
183 		final List<Genotype> genotypes = new ArrayList<>(initialPopulationSize);
184 
185 		final var seedPopulation = eaConfiguration.seedPopulation();
186 		if (CollectionUtils.isNotEmpty(seedPopulation)) {
187 			genotypes.addAll(seedPopulation);
188 		}
189 		if (genotypes.size() < initialPopulationSize) {
190 			final var missingInitialIndividualCount = initialPopulationSize - genotypes.size();
191 			logger.info(
192 					"{} seed individual(s) added and generating {} individuals to reach the target of {} initial population size",
193 					genotypes.size(),
194 					missingInitialIndividualCount,
195 					initialPopulationSize);
196 
197 			final var extraIndividuals = genotypeGenerator.generateGenotypes(missingInitialIndividualCount);
198 			genotypes.addAll(extraIndividuals);
199 		}
200 
201 		return genotypes;
202 	}
203 
204 	private List<Genotype> mutateGenotypes(final long generation, final List<Genotype> genotypes) {
205 		Validate.isTrue(generation >= 0);
206 		Objects.requireNonNull(genotypes);
207 
208 		return genotypes.stream().map(child -> {
209 			Genotype mutatedChild = child;
210 
211 			for (final Mutator mutator : mutators) {
212 				mutatedChild = mutator.mutate(generation, mutatedChild);
213 			}
214 
215 			return mutatedChild;
216 		}).toList();
217 	}
218 
219 	private List<Genotype> combineParents(final Population<T> parents, final GenotypeCombinator genotypeCombinator) {
220 		Objects.requireNonNull(parents);
221 		Objects.requireNonNull(genotypeCombinator);
222 
223 		return IntStream.range(0, parents.size() / 2).parallel().boxed().flatMap(baseIndex -> {
224 			final int firstParentIndex = baseIndex * 2;
225 			final Genotype firstParent = parents.getGenotype(firstParentIndex);
226 			final T firstParentFitness = parents.getFitness(firstParentIndex);
227 
228 			final int secondParentIndex = firstParentIndex + 1;
229 			final Genotype secondParent = parents.getGenotype(secondParentIndex);
230 			final T secondParentFitness = parents.getFitness(secondParentIndex);
231 
232 			final List<List<Chromosome>> chromosomes = new ArrayList<>();
233 			for (int chromosomeIndex = 0; chromosomeIndex < eaConfiguration.numChromosomes(); chromosomeIndex++) {
234 
235 				final Chromosome firstChromosome = firstParent.getChromosome(chromosomeIndex);
236 				final Chromosome secondChromosome = secondParent.getChromosome(chromosomeIndex);
237 
238 				final List<Chromosome> combinedChromosomes = chromosomeCombinators.get(chromosomeIndex)
239 						.combine(eaConfiguration, firstChromosome, firstParentFitness, secondChromosome, secondParentFitness);
240 
241 				chromosomes.add(combinedChromosomes);
242 				logger.trace("Combining {} with {} ---> {}", firstChromosome, secondChromosome, combinedChromosomes);
243 			}
244 
245 			final List<Genotype> offsprings = genotypeCombinator.combine(eaConfiguration, chromosomes);
246 			return offsprings.stream();
247 		}).toList();
248 	}
249 
250 	/**
251 	 * Create offsprings without mutation
252 	 *
253 	 * @param generation
254 	 * @param population
255 	 * @param offspringsNeeded
256 	 * @return
257 	 */
258 	private List<Genotype> createBasicOffsprings(final long generation, final Population<T> population,
259 			final int offspringsNeeded) {
260 		Objects.requireNonNull(population);
261 		Validate.isTrue(offspringsNeeded > 0);
262 
263 		final GenotypeCombinator genotypeCombinator = eaConfiguration.genotypeCombinator();
264 
265 		final int parentsNeeded = (int) (offspringsNeeded * 2);
266 		logger.info("Selecting {} parents as we expect to generate {} offsprings", parentsNeeded, offspringsNeeded);
267 		final Population<T> selectedParents = parentSelector.select(
268 				eaConfiguration,
269 				generation,
270 				parentsNeeded,
271 				population.getAllGenotypes(),
272 				population.getAllFitnesses());
273 		logger.trace("Selected parents: {}", selectedParents);
274 		Validate.isTrue(selectedParents.size() % 2 == 0);
275 
276 		logger.info("Combining {} parents into offsprings", selectedParents.size());
277 		return combineParents(selectedParents, genotypeCombinator);
278 	}
279 
280 	/**
281 	 * Gets the evolutionary algorithm configuration used by this system.
282 	 * 
283 	 * @return the EA configuration containing genetic operators, chromosome specs, and evolutionary parameters
284 	 */
285 	public AbstractEAConfiguration<T> getEAConfiguration() {
286 		return eaConfiguration;
287 	}
288 
289 	/**
290 	 * Gets the target population size for each generation.
291 	 * 
292 	 * @return the population size as configured for this evolutionary system
293 	 */
294 	public long getPopulationSize() {
295 		return populationSize;
296 	}
297 
298 	/**
299 	 * Creates offspring from the given population through selection, crossover, and mutation.
300 	 * 
301 	 * <p>This method orchestrates the reproductive process by:
302 	 * <ol>
303 	 * <li>Selecting parents from the population using the configured selection strategy</li>
304 	 * <li>Applying crossover operations to generate basic offspring</li>
305 	 * <li>Applying mutation operators to introduce genetic variation</li>
306 	 * </ol>
307 	 * 
308 	 * @param generation       the current generation
309 	 * @param population       the current population to select parents from
310 	 * @param offspringsNeeded the number of offspring to generate
311 	 * @return a list of mutated offspring genotypes
312 	 * @throws NullPointerException     if population is null
313 	 * @throws IllegalArgumentException if offspringsNeeded is not positive
314 	 */
315 	public List<Genotype> createOffsprings(final long generation, final Population<T> population,
316 			final int offspringsNeeded) {
317 		Objects.requireNonNull(population);
318 		Validate.isTrue(offspringsNeeded > 0);
319 
320 		final List<Genotype> offpsrings = createBasicOffsprings(generation, population, offspringsNeeded);
321 		logger.info("Generated {} offsprings", offpsrings.size());
322 
323 		logger.info("Mutating offsprigns");
324 		return mutateGenotypes(generation, offpsrings);
325 	}
326 
327 	/**
328 	 * Executes the complete evolutionary algorithm process until termination criteria are met.
329 	 * 
330 	 * <p>This is the main entry point for running evolution. The method manages the entire evolutionary cycle including:
331 	 * <ul>
332 	 * <li>Initial population generation and evaluation</li>
333 	 * <li>Iterative evolution through selection, reproduction, and replacement</li>
334 	 * <li>Progress monitoring via evolution listeners</li>
335 	 * <li>Termination condition checking</li>
336 	 * </ul>
337 	 * 
338 	 * <p>The evolution process continues until the configured termination criteria (e.g., maximum generations, target
339 	 * fitness, convergence) are satisfied.
340 	 * 
341 	 * @return an EvolutionResult containing the final population, fitness values, generation count, and configuration
342 	 *         details
343 	 * @see Termination
344 	 * @see EvolutionResult
345 	 */
346 	public EvolutionResult<T> evolve() {
347 		final Termination<T> termination = eaConfiguration.termination();
348 
349 		logger.info("Starting evolution");
350 
351 		final Optional<PostEvaluationProcessor<T>> postEvaluationProcessor = eaConfiguration.postEvaluationProcessor();
352 		final List<EvolutionListener<T>> evolutionListeners = eaExecutionContext.evolutionListeners();
353 
354 		fitnessEvaluator.preEvaluation();
355 		evolutionListeners.forEach(evolutionListener -> evolutionListener.preEvaluation(eaConfiguration));
356 
357 		long generation = 0;
358 		final List<Genotype> genotypes = initializePopulation();
359 
360 		logger.info("Evaluating initial population");
361 		final List<T> fitnessScore = evaluate(generation, genotypes);
362 		final Population<T> initialPopulation = Population.of(genotypes, fitnessScore);
363 
364 		Population<T> population = postEvaluationProcessor.map(pep -> pep.apply(0, initialPopulation))
365 				.orElseGet(() -> initialPopulation);
366 
367 		while (termination
368 				.isDone(eaConfiguration, generation, population.getAllGenotypes(), population.getAllFitnesses()) == false) {
369 			final long currentGeneration = generation;
370 			logger.info("Going through evolution of generation {}", currentGeneration);
371 
372 			for (final EvolutionListener<T> evolutionListener : evolutionListeners) {
373 				evolutionListener
374 						.onEvolution(currentGeneration, population.getAllGenotypes(), population.getAllFitnesses(), false);
375 			}
376 
377 			final int offspringsNeeded = (int) (populationSize * offspringRatio);
378 			final List<Genotype> offsprings = createOffsprings(currentGeneration, population, offspringsNeeded);
379 
380 			logger.info("Evaluating offsprings");
381 			final List<T> offspringScores = evaluate(currentGeneration, offsprings);
382 
383 			final Population<T> childrenPopulation = postEvaluationProcessor
384 					.map(pep -> pep.apply(currentGeneration, Population.of(offsprings, offspringScores)))
385 					.orElseGet(() -> Population.of(offsprings, offspringScores));
386 
387 			logger.info("Executing replacement strategy");
388 			final int nextGenerationPopulationSize = eaExecutionContext.populationSize();
389 			final Population<T> newPopulation = replacementStrategyImplementor.select(
390 					eaConfiguration,
391 					currentGeneration,
392 					nextGenerationPopulationSize,
393 					population.getAllGenotypes(),
394 					population.getAllFitnesses(),
395 					childrenPopulation.getAllGenotypes(),
396 					childrenPopulation.getAllFitnesses());
397 
398 			if (newPopulation.size() < nextGenerationPopulationSize) {
399 				logger.info("New population only has {} members. Generating more individuals", newPopulation.size());
400 				final List<Genotype> additionalIndividuals = genotypeGenerator
401 						.generateGenotypes(nextGenerationPopulationSize - newPopulation.size());
402 				logger.debug("Number of generated individuals: {}", additionalIndividuals.size());
403 
404 				if (additionalIndividuals.isEmpty() == false) {
405 
406 					final List<T> additionalFitness = evaluate(currentGeneration, additionalIndividuals);
407 					newPopulation.addAll(Population.of(additionalIndividuals, additionalFitness));
408 				}
409 			}
410 
411 			if (logger.isTraceEnabled()) {
412 				logger.trace("[Generation {}] New population: {}", currentGeneration, Arrays.asList(newPopulation));
413 			}
414 			population = newPopulation;
415 			generation++;
416 		}
417 
418 		logger.info("Evolution has terminated");
419 
420 		// isDone has returned true and we want to let the evolutionListeners run a last time
421 		for (final EvolutionListener<T> evolutionListener : evolutionListeners) {
422 			evolutionListener.onEvolution(generation, population.getAllGenotypes(), population.getAllFitnesses(), true);
423 			evolutionListener.postEvaluation(eaConfiguration);
424 		}
425 
426 		fitnessEvaluator.postEvaluation();
427 
428 		return ImmutableEvolutionResult
429 				.of(eaConfiguration, generation, population.getAllGenotypes(), population.getAllFitnesses());
430 	}
431 
432 	/**
433 	 * Evaluates a collection of genotypes once without running the full evolutionary process.
434 	 * 
435 	 * <p>This method provides a way to evaluate genotypes using the configured fitness evaluator without executing the
436 	 * complete evolutionary algorithm. It handles the pre/post evaluation lifecycle and notifies evolution listeners of
437 	 * the results.
438 	 * 
439 	 * <p>Useful for:
440 	 * <ul>
441 	 * <li>Testing fitness functions with specific genotypes</li>
442 	 * <li>Evaluating externally generated populations</li>
443 	 * <li>Batch evaluation scenarios</li>
444 	 * </ul>
445 	 * 
446 	 * @param generation the generation number for logging and listener notification
447 	 * @param genotypes  the list of genotypes to evaluate
448 	 * @return a list of fitness values corresponding to the input genotypes
449 	 * @throws IllegalArgumentException if generation is negative or genotypes is empty
450 	 * @throws NullPointerException     if genotypes is null
451 	 */
452 	public List<T> evaluateOnce(final long generation, final List<Genotype> genotypes) {
453 		Validate.isTrue(generation >= 0);
454 		Objects.requireNonNull(genotypes);
455 		Validate.isTrue(genotypes.isEmpty() == false);
456 
457 		final List<EvolutionListener<T>> evolutionListeners = eaExecutionContext.evolutionListeners();
458 
459 		fitnessEvaluator.preEvaluation();
460 		evolutionListeners.forEach(evolutionListener -> evolutionListener.preEvaluation(eaConfiguration));
461 		final var fitness = evaluate(generation, genotypes);
462 
463 		final var population = Population.of(genotypes, fitness);
464 		for (final EvolutionListener<T> evolutionListener : eaExecutionContext.evolutionListeners()) {
465 			evolutionListener.onEvolution(generation, population.getAllGenotypes(), population.getAllFitnesses(), true);
466 		}
467 
468 		evolutionListeners.forEach(evolutionListener -> evolutionListener.postEvaluation(eaConfiguration));
469 		fitnessEvaluator.postEvaluation();
470 
471 		return fitness;
472 	}
473 }