<html>
|
|
<head>
|
|
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
|
|
<script
|
|
src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
|
|
integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E="
|
|
crossorigin="anonymous">
|
|
</script>
|
|
|
|
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
|
|
|
<script>
|
|
class Gene
|
|
{
|
|
/**
|
|
* Constructs a new Gene to store in a chromosome.
|
|
* @param min minimum value that this gene can store
|
|
* @param max value this gene can possibly be
|
|
* @param value normalized value
|
|
*/
|
|
constructor(min, max, value)
|
|
{
|
|
this.min = min;
|
|
this.max = max;
|
|
this.value = value;
|
|
}
|
|
|
|
/**
|
|
* De-normalizes the value of the gene
|
|
* @returns {*}
|
|
*/
|
|
getRealValue()
|
|
{
|
|
return (this.max - this.min) * this.value + this.min;
|
|
}
|
|
|
|
getValue()
|
|
{
|
|
return this.value;
|
|
}
|
|
|
|
setValue(val)
|
|
{
|
|
this.value = val;
|
|
}
|
|
|
|
makeClone()
|
|
{
|
|
return new Gene(this.min, this.max, this.value);
|
|
}
|
|
|
|
makeRandomGene()
|
|
{
|
|
return new Gene(this.min, this.max, Math.random());
|
|
}
|
|
}
|
|
|
|
|
|
class Chromosome
|
|
{
|
|
/**
|
|
* Constructs a chromosome by making a copy of
|
|
* a list of genes.
|
|
* @param geneArray
|
|
*/
|
|
constructor(geneArray)
|
|
{
|
|
this.genes = [];
|
|
for(let i = 0; i < geneArray.length; i++)
|
|
{
|
|
this.genes.push(geneArray[i].makeClone());
|
|
}
|
|
}
|
|
|
|
getGenes()
|
|
{
|
|
return this.genes;
|
|
}
|
|
|
|
/**
|
|
* Mutates a random gene.
|
|
*/
|
|
mutate()
|
|
{
|
|
this.genes[Math.round(Math.random() * (this.genes.length-1))].setValue(Math.random());
|
|
}
|
|
|
|
/**
|
|
* Creates a totally new chromosome with same
|
|
* genetic structure as this chromosome but different
|
|
* values.
|
|
* @returns {Chromosome}
|
|
*/
|
|
createRandomChromosome()
|
|
{
|
|
let geneAr = [];
|
|
for(let i = 0; i < this.genes.length; i++)
|
|
{
|
|
geneAr.push(this.genes[i].makeRandomGene());
|
|
}
|
|
return new Chromosome(geneAr);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Mates two chromosomes using the blending method
|
|
* and returns a list of 2 offspring.
|
|
* @param father
|
|
* @param mother
|
|
* @returns {Chromosome[]}
|
|
*/
|
|
const breed = function(father, mother)
|
|
{
|
|
let son = new Chromosome(father.getGenes());
|
|
let daughter = new Chromosome(mother.getGenes());
|
|
|
|
for(let i = 0;i < son.getGenes().length; i++)
|
|
{
|
|
let blendCoef = Math.random();
|
|
blendGene(son.getGenes()[i], daughter.getGenes()[i], blendCoef);
|
|
}
|
|
return [son, daughter];
|
|
};
|
|
|
|
|
|
/**
|
|
* Blends two genes together based on a random blend
|
|
* coefficient.
|
|
**/
|
|
const blendGene = function(gene1, gene2, blendCoef)
|
|
{
|
|
let value1 = (blendCoef * gene1.getValue()) +
|
|
(gene2.getValue() * (1- blendCoef));
|
|
let value2 = ((1-blendCoef) * gene1.getValue()) +
|
|
(gene2.getValue() * blendCoef);
|
|
|
|
gene1.setValue(value1);
|
|
gene2.setValue(value2);
|
|
};
|
|
|
|
/**
|
|
* Helper function to sort an array
|
|
*
|
|
* @param prop name of JSON property to sort by
|
|
* @returns {function(*, *): number}
|
|
*/
|
|
function predicateBy(prop)
|
|
{
|
|
return function(a,b)
|
|
{
|
|
var result;
|
|
if(a[prop] > b[prop])
|
|
{
|
|
result = 1;
|
|
}
|
|
else if(a[prop] < b[prop])
|
|
{
|
|
result = -1;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function which computes the fitness of everyone in the
|
|
* population and returns the most fit survivors. Method
|
|
* known as elitism.
|
|
*
|
|
* @param population
|
|
* @param keepNumber
|
|
* @param fitnessFunction
|
|
* @returns {{average: number,
|
|
* survivors: Array, bestFit: Chromosome }}
|
|
*/
|
|
const naturalSelection = function(population, keepNumber, fitnessFunction)
|
|
{
|
|
let fitnessArray = [];
|
|
let total = 0;
|
|
for(let i = 0; i < population.length; i++)
|
|
{
|
|
const fitness = fitnessFunction(population[i]);
|
|
console.log(fitness);
|
|
fitnessArray.push({fit:fitness, chrom: population[i]});
|
|
total+= fitness;
|
|
}
|
|
|
|
fitnessArray.sort(predicateBy("fit"));
|
|
|
|
let survivors = [];
|
|
let bestFitness = fitnessArray[0].fit;
|
|
let bestChromosome = fitnessArray[0].chrom;
|
|
for(let i = 0; i < keepNumber; i++)
|
|
{
|
|
survivors.push(fitnessArray[i].chrom);
|
|
}
|
|
return {average: total/population.length, survivors: survivors, bestFit: bestFitness, bestChrom: bestChromosome};
|
|
};
|
|
|
|
|
|
/**
|
|
* Randomly everyone in the population
|
|
*
|
|
* @param population
|
|
* @param desiredPopulationSize
|
|
*/
|
|
const matePopulation = function(population, desiredPopulationSize)
|
|
{
|
|
const originalLength = population.length;
|
|
while(population.length < desiredPopulationSize)
|
|
{
|
|
let index1 = Math.round(Math.random() * (originalLength-1));
|
|
let index2 = Math.round(Math.random() * (originalLength-1));
|
|
if(index1 !== index2)
|
|
{
|
|
const babies = breed(population[index1], population[index2]);
|
|
population.push(babies[0]);
|
|
population.push(babies[1]);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Randomly mutates the population
|
|
**/
|
|
const mutatePopulation = function(population, mutatePercentage)
|
|
{
|
|
if(population.length >= 2)
|
|
{
|
|
let mutations = mutatePercentage *
|
|
population.length *
|
|
population[0].getGenes().length;
|
|
for(let i = 0; i < mutations; i++)
|
|
{
|
|
population[i].mutate();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
console.log("Error, population too small to mutate");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Introduces x random chromosomes to the population.
|
|
* @param population
|
|
* @param immigrationSize
|
|
*/
|
|
const newBlood = function(population, immigrationSize)
|
|
{
|
|
for(let i = 0; i < immigrationSize; i++)
|
|
{
|
|
let geneticChromosome = population[0];
|
|
population.push(geneticChromosome.createRandomChromosome());
|
|
}
|
|
};
|
|
|
|
|
|
let costx = Math.random() * 10;
|
|
let costy = Math.random() * 10;
|
|
|
|
/** Defines the cost as the "distance" to a 2-d point.
|
|
* @param chromosome
|
|
* @returns {number}
|
|
*/
|
|
const basicCostFunction = function(chromosome)
|
|
{
|
|
return Math.abs(chromosome.getGenes()[0].getRealValue() - costx) +
|
|
Math.abs(chromosome.getGenes()[1].getRealValue() - costy);
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a totally random population based on a desired size
|
|
* and a prototypical chromosome.
|
|
*
|
|
* @param geneticChromosome
|
|
* @param populationSize
|
|
* @returns {Array}
|
|
*/
|
|
const createRandomPopulation = function(geneticChromosome, populationSize)
|
|
{
|
|
let population = [];
|
|
for(let i = 0; i < populationSize; i++)
|
|
{
|
|
population.push(geneticChromosome.createRandomChromosome());
|
|
}
|
|
return population;
|
|
};
|
|
|
|
|
|
/**
|
|
* Runs the genetic algorithm by going through the processes of
|
|
* natural selection, mutation, mating, and immigrations. This
|
|
* process will continue until an adequately performing chromosome
|
|
* is found or a generation threshold is passed.
|
|
*
|
|
* @param geneticChromosome Prototypical chromosome: used so algo knows
|
|
* what the dna of the population looks like.
|
|
* @param costFunction Function which defines how bad a Chromosome is
|
|
* @param populationSize Desired population size for population
|
|
* @param maxGenerations Cut off level for number of generations to run
|
|
* @param desiredCost Sufficient cost to terminate program at
|
|
* @param mutationRate Number between [0,1] representing proportion of genes
|
|
* to mutate each generation
|
|
* @param keepNumber Number of Organisms which survive each generation
|
|
* @param newBloodNumber Number of random immigrants to introduce into
|
|
* the population each generation.
|
|
* @returns {*}
|
|
*/
|
|
const runGeneticOptimization = function(geneticChromosome, costFunction,
|
|
populationSize, maxGenerations,
|
|
desiredCost, mutationRate, keepNumber,
|
|
newBloodNumber)
|
|
{
|
|
let population = createRandomPopulation(geneticChromosome, populationSize);
|
|
let generation = 0;
|
|
let bestCost = Number.MAX_VALUE;
|
|
let bestChromosome = geneticChromosome;
|
|
do
|
|
{
|
|
matePopulation(population, populationSize);
|
|
newBlood(population, newBloodNumber);
|
|
mutatePopulation(population, mutationRate);
|
|
let generationResult = naturalSelection(population, keepNumber, costFunction);
|
|
|
|
if(bestCost > generationResult.bestFit)
|
|
{
|
|
bestChromosome = generationResult.bestChrom;
|
|
bestCost = generationResult.bestFit;
|
|
}
|
|
population = generationResult.survivors;
|
|
|
|
generation++;
|
|
console.log("Generation " + generation + " Best Cost: " + bestCost);
|
|
}while(generation < maxGenerations && bestCost > desiredCost);
|
|
return bestChromosome;
|
|
};
|
|
|
|
|
|
/**
|
|
* Ugly globals used to keep track of population state for the graph.
|
|
*/
|
|
let genericChromosomeG, costFunctionG,
|
|
populationSizeG, maxGenerationsG,
|
|
desiredCostG, mutationRateG, keepNumberG,
|
|
newBloodNumberG, populationG, generationG,
|
|
bestCostG = Number.MAX_VALUE, bestChromosomeG = genericChromosomeG;
|
|
const runGeneticOptimizationForGraph = function()
|
|
{
|
|
let generationResult = naturalSelection(populationG, keepNumberG, costFunctionG);
|
|
|
|
stats.push([generationG, generationResult.bestFit, generationResult.average]);
|
|
|
|
if(bestCostG > generationResult.bestFit)
|
|
{
|
|
bestChromosomeG = generationResult.bestChrom;
|
|
bestCostG = generationResult.bestFit;
|
|
}
|
|
populationG = generationResult.survivors;
|
|
|
|
generationG++;
|
|
|
|
console.log("Generation " + generationG + " Best Cost: " + bestCostG);
|
|
|
|
console.log(generationResult);
|
|
|
|
matePopulation(populationG, populationSizeG);
|
|
newBlood(populationG, newBloodNumberG);
|
|
mutatePopulation(populationG, mutationRateG);
|
|
|
|
createGraph();
|
|
};
|
|
|
|
|
|
let stats = [];
|
|
const createGraph = function()
|
|
{
|
|
var dataPoints = [];
|
|
|
|
console.log(dataPoints);
|
|
|
|
var data = new google.visualization.DataTable();
|
|
data.addColumn('number', 'Gene 1');
|
|
data.addColumn('number', 'Gene 2');
|
|
|
|
for(let i = 0; i < populationG.length; i++)
|
|
{
|
|
data.addRow([populationG[i].getGenes()[0].getRealValue(),
|
|
populationG[i].getGenes()[1].getRealValue()]);
|
|
}
|
|
|
|
var options = {
|
|
title: 'Genetic Evolution On Two Genes Generation: ' + generationG,
|
|
hAxis: {title: 'Gene 1', minValue: 0, maxValue: 10},
|
|
vAxis: {title: 'Gene 2', minValue: 0, maxValue: 10},
|
|
};
|
|
|
|
var chart = new google.visualization.ScatterChart(document.getElementById('chart_div'));
|
|
|
|
chart.draw(data, options);
|
|
|
|
//line chart stuff
|
|
var line_data = new google.visualization.DataTable();
|
|
line_data.addColumn('number', 'Generation');
|
|
line_data.addColumn('number', 'Best');
|
|
line_data.addColumn('number', 'Average');
|
|
|
|
line_data.addRows(stats);
|
|
|
|
console.log(stats);
|
|
|
|
var lineChartOptions = {
|
|
hAxis: {
|
|
title: 'Generation'
|
|
},
|
|
vAxis: {
|
|
title: 'Cost'
|
|
},
|
|
colors: ['#AB0D06', '#007329']
|
|
};
|
|
|
|
var chart = new google.visualization.LineChart(document.getElementById('line_chart'));
|
|
chart.draw(line_data, lineChartOptions);
|
|
};
|
|
|
|
|
|
let gene1 = new Gene(1,10,10);
|
|
let gene2 = new Gene(1,10,0.4);
|
|
let geneList = [gene1, gene2];
|
|
|
|
let exampleOrganism = new Chromosome(geneList);
|
|
|
|
|
|
genericChromosomeG = exampleOrganism;
|
|
costFunctionG = basicCostFunction;
|
|
populationSizeG = 100;
|
|
maxGenerationsG = 30;
|
|
desiredCostG = 0.00001;
|
|
mutationRateG = 0.3;
|
|
keepNumberG = 30;
|
|
newBloodNumberG = 10;
|
|
generationG = 0;
|
|
|
|
|
|
function verifyForm()
|
|
{
|
|
if(Number($("#populationSize").val()) <= 1)
|
|
{
|
|
alert("Population size must be greater than one.");
|
|
return false;
|
|
}
|
|
|
|
if(Number($("#mutationRate").val()) > 1 ||
|
|
Number($("#mutationRate").val()) < 0)
|
|
{
|
|
alert("Mutation rate must be between zero and one.");
|
|
return false;
|
|
}
|
|
|
|
if(Number($("#survivalSize").val()) < 0)
|
|
{
|
|
alert("Survival size can't be less than one.");
|
|
return false;
|
|
}
|
|
|
|
if(Number($("#newBlood").val()) < 0)
|
|
{
|
|
alert("New organisms can't be a negative number.");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function resetPopulation()
|
|
{
|
|
if(verifyForm())
|
|
{
|
|
stats = [];
|
|
autoRunning = false;
|
|
$("#runAutoOptimizer").val("Auto Run");
|
|
|
|
populationSizeG = $("#populationSize").val();
|
|
mutationRateG = $("#mutationRate").val();
|
|
keepNumberG = $("#survivalSize").val();
|
|
newBloodNumberG = $("#newBlood").val();
|
|
generationG = 0;
|
|
|
|
populationG = createRandomPopulation(genericChromosomeG, populationSizeG);
|
|
createGraph();
|
|
}
|
|
}
|
|
|
|
populationG = createRandomPopulation(genericChromosomeG, populationSizeG);
|
|
|
|
|
|
window.onload = function (){
|
|
google.charts.load('current', {packages: ['corechart', 'line']});
|
|
google.charts.load('current', {'packages':['corechart']}).then(function()
|
|
{
|
|
createGraph();
|
|
})
|
|
};
|
|
|
|
|
|
let autoRunning = false;
|
|
function runAutoOptimizer()
|
|
{
|
|
if(autoRunning === true)
|
|
{
|
|
runGeneticOptimizationForGraph();
|
|
setTimeout(runAutoOptimizer, 1000);
|
|
}
|
|
}
|
|
|
|
function startStopAutoRun()
|
|
{
|
|
autoRunning = !autoRunning;
|
|
|
|
if(autoRunning)
|
|
{
|
|
$("#runAutoOptimizer").val("Stop Auto Run");
|
|
}
|
|
else
|
|
{
|
|
$("#runAutoOptimizer").val("Resume Auto Run");
|
|
}
|
|
|
|
runAutoOptimizer();
|
|
}
|
|
|
|
//runGeneticOptimization(exampleOrganism, basicCostFunction, 100, 50, 0.01, 0.3, 20, 10);
|
|
|
|
</script>
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
<div id="chart_div" style="width: 900px; height: 500px;"></div>
|
|
|
|
<div id="line_chart"></div>
|
|
|
|
<input class='btn btn-primary' id="runOptimizer" onclick='runGeneticOptimizationForGraph()' type="button" value="Next Generation">
|
|
<input class='btn btn-primary' id="runAutoOptimizer" onclick='startStopAutoRun()' type="button" value="Auto Run">
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2>Population Variables</h2>
|
|
</div>
|
|
<form class="card-body">
|
|
<div class="row p-2">
|
|
<div class="col">
|
|
<label for="populationSize">Population Size</label>
|
|
<input type="text" class="form-control" value="100" id="populationSize" placeholder="Population Size" required>
|
|
</div>
|
|
<div class="col">
|
|
<label for="populationSize">Survival Size</label>
|
|
<input type="text" class="form-control" value="20" id="survivalSize" placeholder="Survival Size" required>
|
|
</div>
|
|
</div>
|
|
<div class="row p-2">
|
|
<div class="col">
|
|
<label for="populationSize">Mutation Rate</label>
|
|
<input type="text" class="form-control" value="0.03" id="mutationRate" placeholder="Mutation Rate" required>
|
|
</div>
|
|
<div class="col">
|
|
<label for="populationSize">New Organisms Per Generation</label>
|
|
<input type="text" class="form-control" value="5" id="newBlood" placeholder="New Organisms" required>
|
|
</div>
|
|
</div>
|
|
<br>
|
|
<input class='btn btn-primary' id="reset" onclick='resetPopulation()' type="button" value="Reset Population">
|
|
</form>
|
|
</div>
|
|
</body>
|
|
|
|
|
|
</html>
|