UNPKG

genetic-search

Version:

Multiprocessing genetic algorithm implementation library

508 lines 22.6 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { createEvaluatedPopulation, extractEvaluatedPopulation, IdGenerator } from "./utils"; import { distinctBy, repeat } from "./itertools"; import { GenomeStatsManager, PopulationSummaryManager } from "./stats"; /** * A genetic search algorithm. * * @template TGenome The type of genome objects in the population. * * @remarks * This class implements the genetic search algorithm. The algorithm is * configured using the [[GeneticSearchConfig]] object. * * The algorithm uses the following components, which can be customized by * providing a custom implementation: * * - A [[PopulateStrategyInterface]] to generate the initial population. * - A [[MutationStrategyInterface]] to mutate the population. * - A [[CrossoverStrategyInterface]] to cross over the population. * - A [[PhenomeStrategyInterface]] to calculate the phenome of the population. * - A [[FitnessStrategyInterface]] to calculate the fitness of the population. * - A [[PhenomeCacheInterface]] to cache the phenome of the population. * * @category Genetic Algorithm */ export class GeneticSearch { /** * Constructs a new instance of the GeneticSearch class. * * @param config - The configuration for the genetic search. * @param strategy - The strategy configuration for genetic operations. * @param idGenerator - An optional ID generator for the genomes. */ constructor(config, strategy, idGenerator) { this._generation = 1; this._populationBuffer = []; this.idGenerator = idGenerator !== null && idGenerator !== void 0 ? idGenerator : new IdGenerator(); this.genomeStatsManager = new GenomeStatsManager(); this.populationSummaryManager = new PopulationSummaryManager(); this.strategy = strategy; this.config = config; this._population = strategy.populate.populate(config.populationSize, this.idGenerator); this._populationBuffer = this.population; } /** * The current generation number. * * @returns The current generation number. */ get generation() { return this._generation; } /** * Gets the best genome from the population. * * @returns The best genome from the population. */ get bestGenome() { return this._population[0]; } /** * The current population of genomes. * * @returns The current population of genomes. */ get population() { return this._population; } /** * Sets the current population of genomes. * * @param population The new population of genomes. */ set population(population) { this.setPopulation(population); } /** * Sets the current population of genomes. * * @param population The new population of genomes. * @param resetIdGenerator Whether to reset the ID generator. Defaults to true. */ setPopulation(population, resetIdGenerator = true) { if (resetIdGenerator) { this.idGenerator.reset(population); } this._populationBuffer = population; } /** * Calculates and returns the partitions of the population for the genetic operations. * * @returns A tuple containing: * - The number of genomes that will survive. * - The number of genomes that will be created by crossover. * - The number of genomes that will be created by mutation. */ get partitions() { // Calculate the number of genomes that will survive based on the survival rate. const countToSurvive = Math.round(this.config.populationSize * this.config.survivalRate); // Calculate the number of genomes that will die (not survive). const countToDie = this.config.populationSize - countToSurvive; // Calculate the number of new genomes that will be created by crossover. const countToCross = Math.round(countToDie * this.config.crossoverRate); // Calculate the number of new genomes that will be created by mutation. const countToMutate = countToDie - countToCross; return [countToSurvive, countToCross, countToMutate]; } /** * Retrieves the phenome cache used by the genetic search algorithm. * * @returns {PhenomeCacheInterface} The phenome cache instance. */ get cache() { return this.strategy.cache; } /** * Retrieves the population summary, optionally rounding the statistics to a specified precision. * * @param roundPrecision Optional. The number of decimal places to round the summary statistics to. * If not provided, no rounding is applied. * @returns The population summary, with statistics rounded to the specified precision if provided. */ getPopulationSummary(roundPrecision) { return roundPrecision === undefined ? this.populationSummaryManager.get() : this.populationSummaryManager.getRounded(roundPrecision); } /** * Runs the genetic search algorithm. * * @param config The configuration for the genetic search algorithm. * @returns A promise that resolves when the algorithm has finished running. */ fit(config) { return __awaiter(this, void 0, void 0, function* () { var _a; // Determine the number of generations to run, defaulting to Infinity if not specified. const generationsCount = (_a = config.generationsCount) !== null && _a !== void 0 ? _a : Infinity; // Run the genetic search algorithm for the specified number of generations. for (let i = 0; i < generationsCount; i++) { const generation = this.generation; // Refresh the population from the population buffer. this.refreshPopulation(); // Clear the cache of phenome. this.clearCache(); // Run the before step callback if specified. if (config.beforeStep) { config.beforeStep(generation); } // Run a step of the genetic search algorithm. const result = yield this.fitStep(config.scheduler); // Run the after step callback if specified. if (config.afterStep) { config.afterStep(generation, result); } // Check if the stop condition is met and stop the algorithm if it is. if (config.stopCondition && config.stopCondition(result)) { break; } } }); } /** * Runs a single step of the genetic search algorithm. * * @param scheduler Optional. The scheduler to use for the genetic search algorithm. * @returns A promise that resolves with the fitness of the best genome in the population. */ fitStep(scheduler) { return __awaiter(this, void 0, void 0, function* () { // Refresh population from buffer. this.refreshPopulation(); // Collect phenome phenome for the population. const phenomeMatrix = yield this.strategy.phenome.collect(this._population, this.strategy.cache); // Calculate fitness for the population. const fitnessColumn = this.strategy.fitness.score(phenomeMatrix); // Update genome statistics. this.genomeStatsManager.update(this.population, phenomeMatrix, fitnessColumn); // Sort the population by fitness. const sortedEvaluatedPopulation = this.strategy.sorting.sort(createEvaluatedPopulation(this._population, fitnessColumn, phenomeMatrix)); const [sortedPopulation, sortedFitnessColumn] = extractEvaluatedPopulation(sortedEvaluatedPopulation); // Update population summary. this.populationSummaryManager.update(sortedPopulation); // Step the scheduler if provided. if (scheduler !== undefined) { scheduler.step(sortedEvaluatedPopulation); } // Run crossover and mutation. this.refreshPopulationBuffer(sortedEvaluatedPopulation); // Increase generation counter. this._generation++; // Return the sorted fitness column. return sortedFitnessColumn; }); } /** * Clears the cache. * * @remarks * This method clears the cache, which is used to store the phenome of the genomes. * The cache is used to avoid re-calculating the phenome of the genomes if they remain unchanged. */ clearCache() { this.strategy.cache.clear(this.population.map((genome) => genome.id)); } /** * Refreshes the population. * * @remarks * This method is used to refresh the population, which is the array of genomes that are currently being evaluated. * The population is refreshed by swapping the current population with the population buffer. */ refreshPopulation() { this._population = this._populationBuffer; } /** * Crosses the given input population. * * @param input The population of genomes to cross. * @param count The number of new genomes to create. * @returns An array of new genomes created by crossing the input population. */ crossover(input, count) { const newPopulation = []; // Select parents for crossover. Then for each parents array, cross them and create a new genome. for (const parents of this.strategy.selection.selectForCrossover(input, count)) { const crossedGenome = this.strategy.crossover.cross(parents, this.idGenerator.nextId()); // Initialize the statistics for the new genome. this.genomeStatsManager.initItem(crossedGenome, 'crossover', parents); newPopulation.push(crossedGenome); } return newPopulation; } /** * Mutates the given input population. * * @param input The population of genomes to mutate. * @param count The number of new genomes to create. * @returns An array of new genomes created by mutating the input population. */ mutate(input, count) { const newPopulation = []; // Select parents for mutation. Then for each parent, mutate it and create a new genome. for (const genome of this.strategy.selection.selectForMutation(input, count)) { // Mutate the parent and create a new genome. const mutatedGenome = this.strategy.mutation.mutate(genome, this.idGenerator.nextId()); // Initialize the statistics for the new genome. this.genomeStatsManager.initItem(mutatedGenome, 'mutation', [genome]); newPopulation.push(mutatedGenome); } // Return the new population. return newPopulation; } /** * Refreshes the population buffer from the evaluated population. * * @param input The population of genomes to refresh the population buffer with. */ refreshPopulationBuffer(input) { const [countToSurvive, countToCross, countToMutate] = this.partitions; const sortedPopulation = input.map((x) => x.genome); // Select the top fittest genomes to survive. const survivedEvaluatedPopulation = input.slice(0, countToSurvive); const survivedPopulation = survivedEvaluatedPopulation.map((x) => x.genome); // Select parents for crossover. Then for each parents array, cross them and create a new genome. const crossedPopulation = this.crossover(input, countToCross); // Select parents for mutation. Then for each parent, mutate it and create a new genome. const mutatedPopulation = this.mutate(input, countToMutate); // Set the current population to the sorted population. this._population = sortedPopulation; // Set the next population to the combination of the survived, crossed, and mutated populations. this._populationBuffer = [...survivedPopulation, ...crossedPopulation, ...mutatedPopulation]; } } /** * A composed genetic search algorithm that combines multiple genetic search strategies. * * @template TGenome The type of genome objects in the population. * * @remarks * This class implements a composite genetic search algorithm that utilizes multiple * genetic search strategies, including eliminators and a final strategy. The algorithm * is configured using the [[ComposedGeneticSearchConfig]] object. * * The algorithm integrates the following components, which can be customized by * providing custom implementations: * * - A [[GeneticSearchStrategyConfig]] to define the strategy for the genetic operations. * - An [[IdGeneratorInterface]] to generate unique IDs for the genomes. * * @category Genetic Algorithm */ export class ComposedGeneticSearch { /** * Constructs a new instance of the ComposedGeneticSearch class. * * @param config - The configuration for the composed genetic search algorithm. * @param strategy - The strategy configuration for genetic operations. * @param idGenerator - An optional ID generator for the genomes. */ constructor(config, strategy, idGenerator) { this.config = config; this.strategy = strategy; this.idGenerator = idGenerator !== null && idGenerator !== void 0 ? idGenerator : new IdGenerator(); this.eliminators = [...repeat(() => new GeneticSearch(config.eliminators, strategy, this.idGenerator), config.final.populationSize)].map((factory) => factory()); this.final = new GeneticSearch(config.final, strategy, this.idGenerator); } /** * The current generation number. * * @returns The current generation number. */ get generation() { return this.final.generation; } /** * Gets the best genome from the population. * * @returns The best genome from the population. */ get bestGenome() { return this.final.bestGenome; } /** * The current population of genomes. * * @returns The current population of genomes. */ get population() { // Initialize an empty population result array. const result = []; // Add genomes from the final population, limited to the configured size. result.push(...this.final.population.slice(0, this.config.final.populationSize)); // Add genomes from each eliminator's population. for (const eliminators of this.eliminators) { result.push(...eliminators.population); } // Return the combined population result. return result; } set population(population) { this.setPopulation(population); } /** * Sets the current population of genomes. * * @param population The new population of genomes. * @param resetIdGenerator Whether to reset the ID generator. Defaults to true. */ setPopulation(population, resetIdGenerator = true) { // If the resetIdGenerator option is specified, reset the ID generator. if (resetIdGenerator) { this.idGenerator.reset(population); } // Set the population of the final search algorithm. this.final.setPopulation(population.slice(0, this.final.population.length), false); // Remove the genomes that were assigned to the final search algorithm. population = population.slice(this.final.population.length); // Assign the remaining genomes to the eliminators. for (const eliminator of this.eliminators) { // Set the population of the eliminator. eliminator.setPopulation(population.slice(0, eliminator.population.length), false); // Remove the genomes that were assigned to the eliminator. population = population.slice(eliminator.population.length); } } /** * Calculates and returns the partitions of the population for the genetic operations. * * @returns A tuple containing: * - The number of genomes that will survive. * - The number of genomes that will be created by crossover. * - The number of genomes that will be created by mutation. */ get partitions() { // Calculate the total number of genomes that will survive, be crossed, and be mutated. // This is the sum of the counts from each eliminator. const result = [0, 0, 0]; for (const eliminators of this.eliminators) { const [countToSurvive, countToCross, countToMutate] = eliminators.partitions; // Add the counts from the current eliminator to the result. result[0] += countToSurvive; result[1] += countToCross; result[2] += countToMutate; } return result; } /** * Retrieves the phenome cache used by the genetic search algorithm. * * @returns {PhenomeCacheInterface} The phenome cache instance. */ get cache() { return this.strategy.cache; } /** * Retrieves the population summary, optionally rounding the statistics to a specified precision. * * @param roundPrecision Optional. The number of decimal places to round the summary statistics to. * If not provided, no rounding is applied. * @returns The population summary, with statistics rounded to the specified precision if provided. */ getPopulationSummary(roundPrecision) { return this.final.getPopulationSummary(roundPrecision); } /** * Runs the genetic search algorithm. * * @param config The configuration for the genetic search algorithm. * @returns A promise that resolves when the algorithm has finished running. */ fit(config) { return __awaiter(this, void 0, void 0, function* () { var _a; // Determine the number of generations to run, defaulting to Infinity if not specified. const generationsCount = (_a = config.generationsCount) !== null && _a !== void 0 ? _a : Infinity; // Iterate through each generation until the specified count is reached. for (let i = 0; i < generationsCount; i++) { // Refresh the population for the current generation. this.refreshPopulation(); // Clear any cached phenome to ensure accurate calculations. this.clearCache(); // Execute the before-step callback if it is provided in the config. if (config.beforeStep) { config.beforeStep(i); } // Run a single step of the genetic search algorithm using the scheduler. const result = yield this.fitStep(config.scheduler); // Execute the after-step callback if it is provided in the config. if (config.afterStep) { config.afterStep(i, result); } // Check if the stop condition is met, and break the loop if so. if (config.stopCondition && config.stopCondition(result)) { break; } } }); } /** * Runs a single step of the genetic search algorithm. * * @param scheduler Optional. The scheduler to use for the genetic search algorithm. * @returns A promise that resolves with the fitness of the best genome in the population. */ fitStep(scheduler) { return __awaiter(this, void 0, void 0, function* () { // Run a single step of the genetic search algorithm for each eliminator. for (const eliminators of this.eliminators) { yield eliminators.fitStep(); } // Run crossing and mutation for the final population. this.final.refreshPopulation(); // Set the population for the final population by combining the best genomes from the eliminators // with the current population. this.final.setPopulation([...distinctBy([...this.final.population, ...this.bestGenomes], (x) => x.id)], false); // Run the final step of the genetic search algorithm. return yield this.final.fitStep(scheduler); }); } /** * Clears the cache. * * @remarks * This method clears the cache, which is used to store the phenome of the genomes. * The cache is used to avoid re-calculating the phenome of the genomes if they remain unchanged. */ clearCache() { this.strategy.cache.clear(this.population.map((genome) => genome.id)); } /** * Refreshes the population. * * @remarks * This method is used to refresh the population, which is the array of genomes that are currently being evaluated. * The population is refreshed by swapping the current population with the population buffer. */ refreshPopulation() { // Refresh the population for the final search algorithm. this.final.refreshPopulation(); // Refresh the population for each eliminator. for (const eliminator of this.eliminators) { eliminator.refreshPopulation(); } } /** * Gets the best genomes from the eliminators. * * @remarks * This method returns the best genomes from each eliminator. * The best genomes are the genomes that have the highest fitness score. * The best genomes are determined by calling {@link GeneticSearch.bestGenome} on each eliminator. * * @returns The best genomes from the eliminators. */ get bestGenomes() { return this.eliminators.map((eliminators) => eliminators.bestGenome); } } //# sourceMappingURL=genetic.js.map