genetic-search
Version:
Multiprocessing genetic algorithm implementation library
226 lines (190 loc) • 6.09 kB
text/typescript
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!;
}
}