123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- import {render} from "./render.js";
- function rand_range(a, b) {
- return Math.random() * (b - a) + a;
- }
- function rand_normalish() {
- const r = Math.random() + Math.random() + Math.random() + Math.random();
- return (r / 4.0) * 2.0 - 1;
- }
- function lerp(x, a, b) {
- return x * (b - a) + a;
- }
- function clamp(x, a, b) {
- return Math.min(Math.max(x, a), b);
- }
- function sat(x) {
- return Math.min(Math.max(x, 0.0), 1.0);
- }
- class Population {
- constructor(params) {
- this._params = params;
- this._population = [...Array(this._params.population_size)].map(
- _ => ({fitness: 1, genotype: this._CreateRandomgenotype()}));
- this._lastGeneration = null;
- this._generations = 0;
- this._callback = null;
- }
- _CreateRandomGene() {
- return this._params.gene.ranges.map(r => rand_range(r[0], r[1]));
- }
- _CreateRandomgenotype() {
- return [...Array(this._params.genotype.size)].map(
- _ => this._CreateRandomGene());
- }
- Fittest() {
- return this._lastGeneration.parents[0];
- }
- async Run(srcData) {
- await render.setup(srcData);
- while (true) {
- await this._Step(srcData);
- }
- }
- async _Step(tgtImgData) {
- await this._StepPopulation(tgtImgData);
- const parents = this._population.sort((a, b) => (b.fitness - a.fitness));
- this._lastGeneration = {parents: parents};
- this._generations += 1;
- // Draw the main canvas on the worker while breeding next population.
- const cbPromise = this._callback(this, this._lastGeneration.parents[0]);
- this._population = this._BreedNewPopulation(parents);
- if (this._params.genotype.growth_per_increase > 0 ||
- this._params.genotype.size < this._params.genotype.max_size) {
- const increase = (
- (this._generations + 1) % this._params.genotype.generations_per_increase) == 0;
- if (increase) {
- const geneIncrease = this._params.genotype.growth_per_increase;
- this._params.genotype.size += geneIncrease;
- for (let i = 0; i < geneIncrease; i++) {
- for (let p of this._population) {
- p.genotype.push([...this._CreateRandomGene()]);
- }
- }
- }
- }
- await cbPromise;
- }
- async _StepPopulation(tgtImgData) {
- // Wait for them all to be done
- const promises = render.calculateFitnesses(
- this._params.gene.type, this._population.map(p => p.genotype));
- const responses = await Promise.all(promises);
- for (const r of responses) {
- for (const f of r.result) {
- this._population[f.index].fitness = f.fitness;
- }
- }
- }
- _BreedNewPopulation(parents) {
- function _RouletteSelection(sortedParents, totalFitness) {
- const roll = Math.random() * totalFitness;
- let sum = 0;
- for (let p of sortedParents) {
- sum += p.fitness;
- if (roll < sum) {
- return p;
- }
- }
- return sortedParents[sortedParents.length - 1];
- }
- function _RandomParent(sortedParents, otherParent, totalFitness) {
- const p = _RouletteSelection(sortedParents, totalFitness);
- return p;
- }
- function _CopyGenotype(g) {
- return ({
- fitness: g.fitness,
- genotype: [...g.genotype].map(gene => [...gene])
- });
- }
- const newPopulation = [];
- const totalFitness = parents.reduce((t, p) => t + p.fitness, 0);
- const numChildren = Math.ceil(parents.length * 0.8);
- const top = [...parents.slice(0, Math.ceil(parents.length * 0.25))];
- for (let j = 0; j < numChildren; j++) {
- const i = j % top.length;
- const p1 = top[i];
- const p2 = _RandomParent(parents, p1, totalFitness);
- const g = [];
- for (let r = 0; r < p1.genotype.length; r++ ) {
- const roll = Math.random();
- g.push(roll < 0.5 ? p1.genotype[r] : p2.genotype[r]);
- }
- newPopulation.push(_CopyGenotype({fitness: 1, genotype: g}));
- }
- // Let's say keep top X% go through, but with mutations
- const top5 = [...parents.slice(0, Math.ceil(parents.length * 0.05))];
- newPopulation.push(...top5.map(x => _CopyGenotype(x)));
- // Mutations!
- for (let p of newPopulation) {
- const genotypeLength = p.genotype.length;
- const mutationOdds = this._params.mutation.odds;
- const mutationMagnitude = this._params.mutation.magnitude;
- const mutationDecay = this._params.mutation.decay;
- function _Mutate(x, i) {
- const roll = Math.random();
- if (roll < mutationOdds) {
- const xi = genotypeLength - i;
- const mutationMod = Math.E ** (-1 * xi * mutationDecay);
- if (mutationMod <= 0.0001) {
- return x;
- }
- const magnitude = mutationMagnitude * mutationMod * rand_normalish();
- return sat(x + magnitude);
- }
- return x;
- }
- p.genotype = p.genotype.map(
- (g, i) => g.map(
- (x, xi) => _Mutate(x, i)));
- }
- // Immortality granted to the winners from the last life. May the odds be
- // forever in your favour.
- newPopulation.push(...top5.map(x => _CopyGenotype(x)));
- // Create a bunch of random crap to fill out the rest.
- while (newPopulation.length < parents.length) {
- newPopulation.push(
- {fitness: 1, genotype: this._CreateRandomgenotype()});
- }
- return newPopulation;
- }
- }
- class GeneticAlgorithmDemo {
- constructor() {
- this._Init();
- }
- _Init(scene) {
- this._statsText1 = document.getElementById('statsText');
- this._statsText2 = document.getElementById('numbersText');
- this._sourceImg = document.getElementById('sourceImg');
- this._sourceImg.src = 'assets/square.jpg';
- this._sourceImg.onload = () => {
- const ctx = this._sourceCanvas.getContext('2d');
- this._sourceCanvas.width = 128;
- this._sourceCanvas.height = this._sourceCanvas.width * (
- this._sourceImg.height / this._sourceImg.width);
- ctx.drawImage(
- this._sourceImg,
- 0, 0, this._sourceImg.width, this._sourceImg.height,
- 0, 0, this._sourceCanvas.width, this._sourceCanvas.height);
- this._sourceLODData = ctx.getImageData(
- 0, 0, this._sourceCanvas.width, this._sourceCanvas.height);
- this._sourceCanvas.width = 800;
- this._sourceCanvas.height = this._sourceCanvas.width * (
- this._sourceImg.height / this._sourceImg.width);
- this._targetCanvas.width = this._sourceCanvas.width;
- this._targetCanvas.height = this._sourceCanvas.height;
- ctx.drawImage(
- this._sourceImg,
- 0, 0, this._sourceImg.width, this._sourceImg.height,
- 0, 0, this._sourceCanvas.width, this._sourceCanvas.height);
- this._InitPopulation();
- };
- this._sourceCanvas = document.getElementById('source');
- this._targetCanvas = document.getElementById('target');
- }
- _InitPopulation() {
- const GENE_ELLIPSE = {
- type: 'ellipse',
- ranges: [
- [0, 1],
- [0, 1],
- [0, 1],
- [0.01, 0.1],
- [0, 1],
- [0, 1],
- [0.05, 0.5],
- [0, 1],
- ]
- };
- const GENE_LINE = {
- type: 'line',
- ranges: [
- [0, 1],
- [0, 1],
- [0, 1],
- [0.05, 0.2],
- [0, 1],
- [0, 1],
- [0, 1],
- [0, 1],
- [0, 1],
- ]
- };
- const params = {
- population_size: 512,
- genotype: {
- size: 64,
- max_size: 1000,
- generations_per_increase: 50,
- growth_per_increase: 1
- },
- gene: GENE_LINE,
- mutation: {
- magnitude: 0.25,
- odds: 0.1,
- decay: 0,
- }
- };
- this._population = new Population(params);
- this._population._callback = async (population, fittest) => {
- const p1 = render.draw(
- population._params.gene.type, fittest.genotype,
- this._targetCanvas.width, this._targetCanvas.height);
- const hd = await p1;
- const ctx = this._targetCanvas.getContext('2d');
- ctx.putImageData(hd.result.imageData, 0, 0);
- this._statsText2.innerText =
- this._population._generations + '\n' +
- this._population.Fittest().fitness.toFixed(3) + '\n' +
- this._population._population.length + '\n' +
- this._population._params.genotype.size;
- };
- this._population.Run(this._sourceLODData);
- this._statsText1.innerText =
- 'Generation:\n' +
- 'Fitness:\n' +
- 'Population:\n' +
- 'Genes:';
- }
- }
- const _DEMO = new GeneticAlgorithmDemo();
|