UNPKG

genetic-search

Version:

Multiprocessing genetic algorithm implementation library

226 lines (190 loc) 6.09 kB
import type { PhenomeRow, PhenomeCacheInterface } from "./types"; import { arrayBinaryOperation, createFilledArray } from "./utils"; /** * A dummy phenome cache implementation that does nothing. * * This class is used when the {@link GeneticSearch} is created without a * phenome cache. * * @category Cache * @category Strategies */ export class DummyPhenomeCache implements PhenomeCacheInterface { getReady(_: number): PhenomeRow | undefined { return undefined; } get(_: number, defaultValue?: PhenomeRow): PhenomeRow | undefined { return defaultValue; } set(_: number, __: PhenomeRow): void { return; } clear(_: number[]): void { return; } export(): Record<number, never> { return {}; } import(_: Record<number, never>): void { return; } } /** * A simple phenome cache implementation. * * This cache stores the constant phenome value for each genome. * * @category Cache * @category Strategies */ export class SimplePhenomeCache implements PhenomeCacheInterface { protected readonly cache: Map<number, PhenomeRow> = new Map(); get(genomeId: number, defaultValue?: PhenomeRow): PhenomeRow | undefined { return this.cache.has(genomeId) ? this.cache.get(genomeId)! : defaultValue; } getReady(genomeId: number): PhenomeRow | undefined { return this.cache.has(genomeId) ? this.get(genomeId) : undefined; } set(genomeId: number, phenome: PhenomeRow): void { this.cache.set(genomeId, phenome); } clear(excludeGenomeIds: number[]): void { const excludeIdsSet = new Set(excludeGenomeIds); for (const id of this.cache.keys()) { if (!excludeIdsSet.has(id)) { this.cache.delete(id); } } } export(): Record<number, PhenomeRow> { return Object.fromEntries(this.cache); } import(data: Record<number, PhenomeRow>): void { this.cache.clear(); for (const [id, cacheItem] of Object.entries(data)) { this.cache.set(Number(id), cacheItem); } } } /** * A phenome cache implementation that stores the phenome for each genome as a * weighted average of all phenome that have been set for that genome. * * @category Cache * @category Strategies */ export class AveragePhenomeCache implements PhenomeCacheInterface { /** * A map of genome IDs to their respective phenome and the number of times they have been set. * * The key is the genome ID, and the value is an array with two elements. The first element is the * current phenome for the genome, and the second element is the number of times the phenome have * been set. */ protected readonly cache: Map<number, [PhenomeRow, number]> = new Map(); get(genomeId: number, defaultValue?: PhenomeRow): PhenomeRow | undefined { if (!this.cache.has(genomeId)) { return defaultValue; } const [row, count] = this.cache.get(genomeId)!; return row.map((x) => x / count); } getReady(): PhenomeRow | undefined { return undefined; } set(genomeId: number, phenome: PhenomeRow): void { if (!this.cache.has(genomeId)) { this.cache.set(genomeId, [phenome, 1]); return; } const [row, count] = this.cache.get(genomeId)!; this.cache.set(genomeId, [row.map((x, i) => x + phenome[i]), count + 1]); } clear(excludeGenomeIds: number[]): void { const excludeIdsSet = new Set(excludeGenomeIds); for (const id of this.cache.keys()) { if (!excludeIdsSet.has(id)) { this.cache.delete(id); } } } export(): Record<number, [PhenomeRow, number]> { return Object.fromEntries(this.cache); } import(data: Record<number, [PhenomeRow, number]>): void { this.cache.clear(); for (const [id, cacheItem] of Object.entries(data)) { this.cache.set(Number(id), cacheItem); } } } /** * A phenome cache implementation that stores the phenome for each genome as a * weighted average of all phenome that have been set for that genome. * * The closer the genome age is to 0, the closer the phenome are to the average phenome of the population, * which helps to combat outliers for new genomes. * * @category Cache * @category Strategies */ export class WeightedAgeAveragePhenomeCache extends AveragePhenomeCache { /** * The weight factor used for calculating the weighted average. */ private weight: number; /** * The current average phenome row, or undefined if not yet calculated. */ private averageRow: PhenomeRow | undefined = undefined; /** * Constructs a new WeightedAgeAveragePhenomeCache. * @param weight The weight factor used for calculating the weighted average. */ constructor(weight: number) { super(); this.weight = weight; } set(genomeId: number, phenome: PhenomeRow): void { super.set(genomeId, phenome); this.resetAverageRow(); } get(genomeId: number, defaultValue?: PhenomeRow): PhenomeRow | undefined { const row = super.get(genomeId, defaultValue); if (row === undefined) { return undefined; } if (!this.refreshAverageRow()) { return row; } const [, age] = this.cache.get(genomeId)!; const averageDiff = arrayBinaryOperation(row, this.averageRow!, (lhs, rhs) => lhs - rhs); const weightedAverageDiff = averageDiff.map((x) => x * this.weight/age); return arrayBinaryOperation(row, weightedAverageDiff, (lhs, rhs) => lhs - rhs); } private refreshAverageRow(): boolean { if (this.cache.size === 0) { this.resetAverageRow(); return false; } let weightedTotal = 0; const result = createFilledArray(this.getPhenomeCount(), 0); for (const phenome of this.cache.values()) { const [row, weight] = phenome; for (let i = 0; i < row.length; ++i) { result[i] += row[i]; } weightedTotal += weight; } this.averageRow = result.map((x) => x / weightedTotal); return true; } private resetAverageRow(): void { this.averageRow = undefined; } private getPhenomeCount(): number { return this.cache.values().next().value![0].length!; } }