From 784ae9989b58c55d91ed613f275824e0e97552fc Mon Sep 17 00:00:00 2001 From: Richard Klafter Date: Sun, 29 Jan 2017 13:30:46 -0800 Subject: [PATCH 1/2] Added support for async fitness function using Promises --- examples/string-solver-async.html | 168 ++++++++++++++++++++++++++++++ js/genetic-0.1.14.js | 140 +++++++++++++------------ js/genetic-0.1.14.min.js | 2 +- lib/genetic.js | 140 +++++++++++++------------ 4 files changed, 317 insertions(+), 133 deletions(-) create mode 100644 examples/string-solver-async.html diff --git a/examples/string-solver-async.html b/examples/string-solver-async.html new file mode 100644 index 0000000..dcacf7c --- /dev/null +++ b/examples/string-solver-async.html @@ -0,0 +1,168 @@ + + + + + Genetic.js String Solver + + + + + +

Genetic String Solver

+ + + + + + + + + + + + + + + +
GenerationFitnessSolution
+ + + + + + \ No newline at end of file diff --git a/js/genetic-0.1.14.js b/js/genetic-0.1.14.js index 9be9bb9..b2759ec 100644 --- a/js/genetic-0.1.14.js +++ b/js/genetic-0.1.14.js @@ -5,9 +5,9 @@ window.Genetic = require('./genetic'); },{"./genetic":2}],2:[function(require,module,exports){ var Genetic = Genetic || (function(){ - + 'use strict'; - + // facilitates communcation between web workers var Serialization = { "stringify": function (obj) { @@ -25,19 +25,19 @@ var Genetic = Genetic || (function(){ }); } }; - + var Clone = function(obj) { if (obj == null || typeof obj != "object") return obj; - + return JSON.parse(JSON.stringify(obj)); }; - + var Optimize = { "Maximize": function (a, b) { return a >= b; } , "Minimize": function (a, b) { return a < b; } }; - + var Select1 = { "Tournament2": function(pop) { var n = pop.length; @@ -64,7 +64,7 @@ var Genetic = Genetic || (function(){ return pop[(this.internalGenState["seq"]++)%pop.length].entity; } }; - + var Select2 = { "Tournament2": function(pop) { return [Select1.Tournament2.call(this, pop), Select1.Tournament2.call(this, pop)]; @@ -80,9 +80,9 @@ var Genetic = Genetic || (function(){ return [Select1.Fittest.call(this, pop), Select1.Random.call(this, pop)]; } }; - + function Genetic() { - + // population this.fitness = null; this.seed = null; @@ -93,7 +93,7 @@ var Genetic = Genetic || (function(){ this.optimize = null; this.generation = null; this.notification = null; - + this.configuration = { "size": 250 , "crossover": 0.9 @@ -104,48 +104,48 @@ var Genetic = Genetic || (function(){ , "webWorkers": true , "skip": 0 }; - + this.userData = {}; this.internalGenState = {}; - + this.entities = []; - + this.usingWebWorker = false; - - this.start = function() { - - var i; + + this.doIteration = function(i){ var self = this; - function mutateOrNot(entity) { // applies mutation based on mutation probability return Math.random() <= self.configuration.mutation && self.mutate ? self.mutate(Clone(entity)) : entity; } - - // seed the population - for (i=0;i= this.configuration.iterations) return; + i++; + + // reset for each generation + this.internalGenState = {}; + + // score entities with support for returning promises + var promises = this.entities + .map(function(entity){ + return Promise.resolve(self.fitness(entity)) + .then(function(fitness){ + return { "fitness": fitness, "entity": entity }; + }) + }) + //wait for all fitness functions to return & then sort + return Promise.all(promises).then(function(pop){ + return pop.sort(function (a, b) { + return self.optimize(a.fitness, b.fitness) ? -1 : 1; + }) + }).then((function(pop){ // generation notification var mean = pop.reduce(function (a, b) { return a + b.fitness; }, 0)/pop.length; var stdev = Math.sqrt(pop .map(function (a) { return (a.fitness - mean) * (a.fitness - mean); }) .reduce(function (a, b) { return a+b; }, 0)/pop.length); - + var stats = { "maximum": pop[0].fitness , "minimum": pop[pop.length-1].fitness @@ -153,43 +153,51 @@ var Genetic = Genetic || (function(){ , "stdev": stdev }; - var r = this.generation ? this.generation(pop.slice(0, this.configuration["maxResults"]), i, stats) : true; + //check for termination + var r = this.generation ? this.generation(pop, i, stats) : true; var isFinished = (typeof r != "undefined" && !r) || (i == this.configuration.iterations-1); - + + //call notification if ( this.notification && (isFinished || this.configuration["skip"] == 0 || i%this.configuration["skip"] == 0) ) { this.sendNotification(pop.slice(0, this.configuration["maxResults"]), i, stats, isFinished); } - - if (isFinished) - break; - + + if (isFinished) return Promise.resolve(); + // crossover and mutate var newPop = []; - if (this.configuration.fittestAlwaysSurvives) // lets the best solution fall through newPop.push(pop[0].entity); - - while (newPop.length < self.configuration.size) { + while (newPop.length < this.configuration.size) { if ( this.crossover // if there is a crossover function && Math.random() <= this.configuration.crossover // base crossover on specified probability - && newPop.length+1 < self.configuration.size // keeps us from going 1 over the max population size + && newPop.length+1 < this.configuration.size // keeps us from going 1 over the max population size ) { var parents = this.select2(pop); var children = this.crossover(Clone(parents[0]), Clone(parents[1])).map(mutateOrNot); newPop.push(children[0], children[1]); } else { - newPop.push(mutateOrNot(self.select1(pop))); + newPop.push(mutateOrNot(this.select1(pop))); } } - this.entities = newPop; + }).bind(this)).then(function(){ + return self.doIteration(i); + }) + } + + this.start = function() { + // seed the population + for (var i=0;i=b},Minimize:function(a,b){return a=b},Minimize:function(a,b){return a=this.configuration.iterations)return;i++;this.internalGenState={};var promises=this.entities.map(function(entity){return Promise.resolve(self.fitness(entity)).then(function(fitness){return{fitness:fitness,entity:entity}})});return Promise.all(promises).then(function(pop){return pop.sort(function(a,b){return self.optimize(a.fitness,b.fitness)?-1:1})}).then(function(pop){var mean=pop.reduce(function(a,b){return a+b.fitness},0)/pop.length;var stdev=Math.sqrt(pop.map(function(a){return(a.fitness-mean)*(a.fitness-mean)}).reduce(function(a,b){return a+b},0)/pop.length);var stats={maximum:pop[0].fitness,minimum:pop[pop.length-1].fitness,mean:mean,stdev:stdev};var r=this.generation?this.generation(pop,i,stats):true;var isFinished=typeof r!="undefined"&&!r||i==this.configuration.iterations-1;if(this.notification&&(isFinished||this.configuration["skip"]==0||i%this.configuration["skip"]==0)){this.sendNotification(pop.slice(0,this.configuration["maxResults"]),i,stats,isFinished)}if(isFinished)return Promise.resolve();var newPop=[];if(this.configuration.fittestAlwaysSurvives)newPop.push(pop[0].entity);while(newPop.length= b; } , "Minimize": function (a, b) { return a < b; } }; - + var Select1 = { "Tournament2": function(pop) { var n = pop.length; @@ -59,7 +59,7 @@ var Genetic = Genetic || (function(){ return pop[(this.internalGenState["seq"]++)%pop.length].entity; } }; - + var Select2 = { "Tournament2": function(pop) { return [Select1.Tournament2.call(this, pop), Select1.Tournament2.call(this, pop)]; @@ -75,9 +75,9 @@ var Genetic = Genetic || (function(){ return [Select1.Fittest.call(this, pop), Select1.Random.call(this, pop)]; } }; - + function Genetic() { - + // population this.fitness = null; this.seed = null; @@ -88,7 +88,7 @@ var Genetic = Genetic || (function(){ this.optimize = null; this.generation = null; this.notification = null; - + this.configuration = { "size": 250 , "crossover": 0.9 @@ -99,48 +99,48 @@ var Genetic = Genetic || (function(){ , "webWorkers": true , "skip": 0 }; - + this.userData = {}; this.internalGenState = {}; - + this.entities = []; - + this.usingWebWorker = false; - - this.start = function() { - - var i; + + this.doIteration = function(i){ var self = this; - function mutateOrNot(entity) { // applies mutation based on mutation probability return Math.random() <= self.configuration.mutation && self.mutate ? self.mutate(Clone(entity)) : entity; } - - // seed the population - for (i=0;i= this.configuration.iterations) return; + i++; + + // reset for each generation + this.internalGenState = {}; + + // score entities with support for returning promises + var promises = this.entities + .map(function(entity){ + return Promise.resolve(self.fitness(entity)) + .then(function(fitness){ + return { "fitness": fitness, "entity": entity }; + }) + }) + //wait for all fitness functions to return & then sort + return Promise.all(promises).then(function(pop){ + return pop.sort(function (a, b) { + return self.optimize(a.fitness, b.fitness) ? -1 : 1; + }) + }).then((function(pop){ // generation notification var mean = pop.reduce(function (a, b) { return a + b.fitness; }, 0)/pop.length; var stdev = Math.sqrt(pop .map(function (a) { return (a.fitness - mean) * (a.fitness - mean); }) .reduce(function (a, b) { return a+b; }, 0)/pop.length); - + var stats = { "maximum": pop[0].fitness , "minimum": pop[pop.length-1].fitness @@ -148,43 +148,51 @@ var Genetic = Genetic || (function(){ , "stdev": stdev }; + //check for termination var r = this.generation ? this.generation(pop, i, stats) : true; var isFinished = (typeof r != "undefined" && !r) || (i == this.configuration.iterations-1); - + + //call notification if ( this.notification && (isFinished || this.configuration["skip"] == 0 || i%this.configuration["skip"] == 0) ) { - this.sendNotification(pop.slice(0, this.maxResults), i, stats, isFinished); + this.sendNotification(pop.slice(0, this.configuration["maxResults"]), i, stats, isFinished); } - - if (isFinished) - break; - + + if (isFinished) return Promise.resolve(); + // crossover and mutate var newPop = []; - if (this.configuration.fittestAlwaysSurvives) // lets the best solution fall through newPop.push(pop[0].entity); - - while (newPop.length < self.configuration.size) { + while (newPop.length < this.configuration.size) { if ( this.crossover // if there is a crossover function && Math.random() <= this.configuration.crossover // base crossover on specified probability - && newPop.length+1 < self.configuration.size // keeps us from going 1 over the max population size + && newPop.length+1 < this.configuration.size // keeps us from going 1 over the max population size ) { var parents = this.select2(pop); var children = this.crossover(Clone(parents[0]), Clone(parents[1])).map(mutateOrNot); newPop.push(children[0], children[1]); } else { - newPop.push(mutateOrNot(self.select1(pop))); + newPop.push(mutateOrNot(this.select1(pop))); } } - this.entities = newPop; + }).bind(this)).then(function(){ + return self.doIteration(i); + }) + } + + this.start = function() { + // seed the population + for (var i=0;i Date: Sun, 5 Feb 2017 15:14:10 -0800 Subject: [PATCH 2/2] dont simulate webworkers --- lib/genetic.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/genetic.js b/lib/genetic.js index b5b36b7..faa1e3a 100644 --- a/lib/genetic.js +++ b/lib/genetic.js @@ -205,8 +205,7 @@ var Genetic = Genetic || (function(){ if (this.usingWebWorker) { postMessage(response); } else { - // self declared outside of scope - self.notification(response.pop.map(Serialization.parse), response.generation, response.stats, response.isFinished); + this.notification(response.pop.map(Serialization.parse), response.generation, response.stats, response.isFinished); } }; @@ -263,12 +262,7 @@ var Genetic = Genetic || (function(){ }; worker.postMessage(""); } else { - // simulate webworker - (function(){ - var onmessage; - eval(blobScript); - onmessage(null); - })(); + self.start(); } }