diff --git a/examples/typing-monkeys.ts b/examples/typing-monkeys.ts new file mode 100644 index 0000000..50cde80 --- /dev/null +++ b/examples/typing-monkeys.ts @@ -0,0 +1,78 @@ +import { evolve, stats } from "../src/lib" +import { Organism } from "../src/types"; + +const target = "hello world"; + +const { population, populationFitness } = evolve({ + mutationRate: 0.009, + populationSize: 200, + genomeLength: target.length, + gene: () => randomChar(), + fitness: (o) => { + let score = 0; + + for (let i = 0; i < o.genome.length; i++) { + if (o.genome[i] === target[i]) { + score++; + } + } + + return score / o.genome.length; + }, + matingPool: (population, populationFitness, maxFitness) => { + const matingPool: typeof population[number][] = []; + + for (let i = 0; i < population.length; i++) { + const fitness = map(populationFitness[i], 0, maxFitness, 0, 1); + const n = Math.floor(fitness * 100); + + for (let j = 0; j < n; j++) { + const o = population[i]; + matingPool.push(o); + } + } + + return matingPool; + }, + crossover: (a: Organism, b: Organism) => { + const midpoint = a.genome.length / 2; + + const genome = [...a.genome.slice(0, midpoint), ...b.genome.slice(midpoint)]; + + const child: Organism = { + genome, + }; + + return child; + }, + mutate: (child, mutationRate, gene) => { + for (let i = 0; i < child.genome.length; i++) { + if (Math.random() < mutationRate) { + child.genome[i] = gene(); + } + } + }, + shouldFinish: ({ generation }) => { + return generation >= 400; + }, +}); + +const s = stats(population, populationFitness) +console.log(s) +console.log(s.fittest.genome.join("")) + +function randomChar() { + const chars = 'abcdefghijklmnopqrstuvwxyz '; + const randomIndex = Math.floor(Math.random() * chars.length); + return chars[randomIndex]; +} + +function map(value, currentMin, currentMax, targetMin, targetMax) { + // Calculate the proportion of the value relative to the current range + let proportion = (value - currentMin) / (currentMax - currentMin); + + // Scale the proportion to fit within the target range + let mappedValue = proportion * (targetMax - targetMin) + targetMin; + + return mappedValue; +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..b7bff38 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +import "./examples/typing-monkeys" diff --git a/src/lib.ts b/src/lib.ts new file mode 100644 index 0000000..516fedf --- /dev/null +++ b/src/lib.ts @@ -0,0 +1,139 @@ +import { CalculatePopulationFitnessOptions, CreateMatingPoolOptions, CreatePopulationOptions, EvolutionStats, EvolveOptions, GeneFn, Genome, MateOptions, Organism, Population, PopulationFitness } from "./types"; + +export function createPopulation( + n: number, + options: CreatePopulationOptions, +) { + const pop: Population = []; + + for (let i = 0; i < n; i++) { + pop.push(options.organism(i)); + } + + return pop; +} + +export function createGenome( + length: number, + gene: GeneFn, +): Genome { + return Array.from({ length }).map(() => gene()); +} + +export function calculatePopulationFitness( + population: Population, + options: CalculatePopulationFitnessOptions, +) { + const populationFitness: number[] = []; + + for (let i = 0; i < population.length; i++) { + const o = population[i]; + populationFitness[i] = options.fitness(o); + } + + return populationFitness; +} + +export function createMatingPool( + population: Population, + populationFitness: PopulationFitness, + options: CreateMatingPoolOptions, +) { + let maxFitness = 0; + + for (let i = 0; i < population.length; i++) { + const fitness = populationFitness[i]; + + if (fitness > maxFitness) { + maxFitness = fitness; + } + } + + return options.matingPool(population, populationFitness, maxFitness); +} + +export function mate( + population: Population, + matingPool: Organism[], + options: MateOptions, +) { + const newPop: Population = []; + + for (let i = 0; i < population.length; i++) { + const a = Math.floor(Math.random() * matingPool.length); + const b = Math.floor(Math.random() * matingPool.length); + + const partnerA = matingPool[a]; + const partnerB = matingPool[b]; + let child = options.crossover(partnerA, partnerB); + options.mutate(child, options.mutationRate, options.gene); + + newPop.push(child); + } + + return newPop; +} + +export function evolve(options: EvolveOptions) { + const params = { + populationSize: 100, + mutationRate: 0.05, + ...options, + } satisfies EvolveOptions; + + let generation = 0; + + let population = createPopulation(params.populationSize, { + organism: () => ({ + genome: createGenome(options.genomeLength, options.gene), + }), + }); + let populationFitness = calculatePopulationFitness(population, { + fitness: params.fitness, + }); + + options.onGeneratePopulation?.({ population, populationFitness, context: { generation } }); + + while (!options.shouldFinish({ generation })) { + populationFitness = calculatePopulationFitness(population, { + fitness: params.fitness, + }); + + const matingPool = createMatingPool(population, populationFitness, { + matingPool: params.matingPool, + }); + + const newGeneration = mate(population, matingPool, { + crossover: params.crossover, + mutate: params.mutate, + mutationRate: params.mutationRate, + gene: params.gene, + }) + + population = newGeneration; + + generation++; + + options.onGeneratePopulation?.({ population, populationFitness, context: { generation } }); + } + + return { population, populationFitness }; +} + +export function stats( + population: Population, + populationFitness: PopulationFitness, +): EvolutionStats { + const fittestIndex = populationFitness.indexOf(Math.max(...populationFitness)); + const fittest = population[fittestIndex]; + + const totalFitness = populationFitness.reduce((acc, val) => acc + val, 0); + const avgFitness = totalFitness / populationFitness.length; + + return { + fittest, + fittestIndex, + avgFitness, + totalFitness, + }; +} diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..ce426ab --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,66 @@ +export type Gene = G; +export type Genome = Gene[]; +export type Organism = { + genome: Genome +}; + +export type Population = Organism[]; + +export type OrganismFn = (i: number) => Organism; +export type GeneFn = () => Gene; + +export type CrossoverFn = (a: Organism, b: Organism) => Organism; +export type MutationFn = (child: Organism, mutationRate: number, gene: GeneFn) => void; +export type FitnessFn = (o: Organism) => number; +export type MatingPoolFn = (population: Population, populationFitness: PopulationFitness, maxFitness: number) => Population; + +export type PopulationFitness = number[]; + +export type EvolutionStats = { + fittest: Organism + fittestIndex: number + avgFitness: number + totalFitness: number +}; + +export type GenerationEvent = { + context: ProblemContext + population: Population + populationFitness: PopulationFitness +}; + +export type ProblemContext = { generation: number }; + +export type CreatePopulationOptions = { + organism: OrganismFn +}; + +export type CreateMatingPoolOptions = { + matingPool: MatingPoolFn +}; + +export type CalculatePopulationFitnessOptions = { + fitness: FitnessFn +}; + +export type MateOptions = { + mutationRate: number + crossover: CrossoverFn + mutate: MutationFn + gene: GeneFn +}; + +type EvolveOptions = { + populationSize?: number + genomeLength: number + gene: GeneFn + matingPool: MatingPoolFn + crossover: CrossoverFn + mutationRate?: number + mutate: MutationFn + fitness: FitnessFn + + shouldFinish: (c: ProblemContext) => boolean + + onGeneratePopulation?: (e: GenerationEvent) => void +};