Skip to content

Instantly share code, notes, and snippets.

@dustinlarimer
Last active October 20, 2017 05:40
Show Gist options
  • Save dustinlarimer/6d58d11e127b9c9c5f21 to your computer and use it in GitHub Desktop.
Save dustinlarimer/6d58d11e127b9c9c5f21 to your computer and use it in GitHub Desktop.
Ant Colony Optimization in D3.js

Crude implementation of ACO in D3js, based on the awesome work of Joel Wenzel et al.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body { margin: 0 }
</style>
<body>
<div id="map"></div>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
!function(context){
var points = 20
, waves = 40
, nodes = []
, stage = {
height: context.innerHeight
, width: context.innerWidth
};
var wave = function(nodes){
var self = extend(this, { nodes: nodes });
var NUM_ANTS = 20
, RHO = 0.1 // Decay rate of the pheromone trails
, BETA = 3.0 // Importance of the durations
, ALPHA = 0.1 // Importance of the previous trails
, Q = 1.0; // see wikipedia - updating pheromones
var result = initPheromones();
var _pher = result.pheromones;
var _nextPher = result.pherUpdates;
self.route = [];
self.distance;
self.routes;
self.distances;
self.runOnce = function(){
var result = sendWave(NUM_ANTS,_pher,_nextPher);
self.routes = result.paths;
self.distances = result.distances;
}
function initPheromones(){
//init pheremons
var pheromones = new Array(self.nodes.length);
var pherUpdates = new Array(self.nodes.length);
/*
Get the average distance between points - http://www.ugosweb.com/Download/JavaACSFramework.pdf
Note that for each point, the distance to itself is 0. This means that
the total number of valid distances is numPoints*(numPoints-1) since the distance
to itself is not valid
*/
var dsum = 0;
for (var i = 0; i < self.nodes.length; i++) {
dsum += self.nodes[i].avg;
}
//each point has n-1 edges
var totalEdges = (20*(20-1));
var avgDistance = dsum / totalEdges;
var initVal = Q / avgDistance;
for (var i = 0; i < 20; ++i) {
pheromones[i] = new Array(20);
pherUpdates[i] = new Array(20);
for (var j = 0; j < 20; ++j) {
pheromones[i][j] = initVal;
pherUpdates[i][j] = 0.0;
}
}
return { pheromones:pheromones, pherUpdates:pherUpdates };
}
function sendWave(numAnts, pheromones, pherUpdates){
var startPoint = null; //parseInt(Math.random()*self.nodes.length);
var paths = new Array(numAnts);
var distances = new Array(numAnts);
var changed = false;
for (var ant = 0; ant < numAnts; ++ant)
{
var result = findAntPath(pheromones, pherUpdates, startPoint);
paths[ant] = result.path;
distances[ant] = result.distance;
if(self.route.length ==0 || result.distance<self.distance){
self.distance = result.distance;
self.route = result.path;
changed = true;
}
}
//update the smell globally
for (var i = 0; i < self.nodes.length; ++i)
{
for (var j = 0; j < self.nodes.length; ++j)
{
pheromones[i][j] =
//decay old pheromone
pheromones[i][j] * (1.0 - RHO)
//add new pheromone
+ pherUpdates[i][j];
pherUpdates[i][j] = 0.0;
}
}
return {paths:paths, distances:distances, changed:changed};
}
function findAntPath(pheromones, pherUpdates, startPoint){
var currPath = new Array(self.nodes.length);
var probability = new Array(self.nodes.length);
var visited = new Array(self.nodes.length);
if(startPoint == null){
//start at a random location
startPoint = parseInt(Math.random()*self.nodes.length);
}
var curr = startPoint;
currPath[0] = curr;
//get path for the ant
//any has to visit each point so run this numPoints times
//minus one since we visit the first node already
for (var step = 1; step < self.nodes.length; step++){
visited[curr] = 1;
//probability for next visit
var probSum = 0.0;
for (var next = 0; next < self.nodes.length; next++){
if (visited[next] != 1){
//probability[next] = distanceAB^-beta * pheromoneAB^alpha
//see reference formula
probability[next] = Math.pow(pheromones[curr][next], ALPHA) * Math.pow(self.nodes[curr].distances[next], 0.0-BETA);
probSum += probability[next];
}
else {
probability[next] = 0;
}
}
//One method is to convert the probability array to actual probabilities by
//dividing by probSum. Then create a random value between 0 and 1 and add up probabilities
//for each path to the next point until one finally surpasses the random value.
//This point would be chosen for the next path. Google does the same thing but
//Just takes a percentage of probSum rather than convert everything to probabilities
//between 0 and 1
var nextCity = -1;
var nextThresh = Math.random()*probSum;
for(var i=0;i<self.nodes.length;i++){
nextThresh -= probability[i];
if (nextThresh<=0) {
nextCity = i;
break;
}
}
currPath[step] = nextCity;
curr = nextCity;
}
/* do k2 optimization
var opt2 = new Tsp.Sequential2OptRunner({startRoute:currPath, distances:_distances, isKnownStart:false});
while(opt2.runOnce()){}
currPath = opt2.route;*/
var currDist = function(route, from, to){
var cost = 0;
for(var i=from+1;i<=to;i++){
cost += self.nodes[route[i-1]].distances[route[i]];
}
return cost;
}(currPath, 0, currPath.length-1);
//store pheromons so that they can be added to the previous
//values after all the ants have finished
for (var i = 0; i < self.nodes.length-1; i++)
{
pherUpdates[currPath[i]][currPath[i+1]] += Q/currDist;
}
return {distance:currDist, path:currPath};
}
return self;
};
var node = function(config){
extend(this, config);
return this;
};
node.prototype = {
calibrate: function(){
this.sum = 0;
this.avg = 0;
for (var i = 0; i < nodes.length; i++){
this.distances[i] = distance(nodes[this.id], nodes[i]);
this.sum += this.distances[i];
}
this.avg = this.sum / this.distances.length;
return this;
}
};
function distance(a, b){
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
}
function extend(target){
for (var i = 1; i < arguments.length; i++) {
for (var prop in arguments[i]){
target[prop] = arguments[i][prop];
}
}
return target;
}
// Generate random points
for (var i = 0; i < points; i++){
nodes.push(new node({
id: i
, x: Math.floor(Math.random() * (stage.width - 50)) + 25
, y: Math.floor(Math.random() * (stage.height - 50)) + 25
, distances: new Array(points)
, sum: 0
, avg: 0
}));
}
// Compose the landscape
var svg = d3.select("#map").append("svg");
var draggable = d3.behavior.drag()
.on("dragstart", function(d){
for (var i = 0; i < edges.length; i++){
if (edges[i]['source'] == d.id || edges[i]['target'] == d.id) {
paths.attr("stroke-opacity", 0);
//paths[0][i].remove();
}
}
})
.on("drag", function(d, i){
d.x = d3.event.x;
d.y = d3.event.y;
d3.select(this)
.attr("transform", function(d,i){
return "translate(" + d.x + ", " + d.y + ")";
})
})
.on("dragend", function(){
run();
});
var dots = svg.append("svg:g")
.attr("class", "nodes")
.selectAll("g.point")
.data(nodes);
dots
.enter()
.append("svg:g")
.attr("class", "point")
.attr("transform", function(d,i){ return "translate(" + d.x + ", " + d.y + ")"; })
.call(draggable)
.append("svg:circle")
.attr("r", 10)
.attr("stroke", "#fff")
.attr("stroke-width", 3);
var paths = svg.selectAll("line.path");
var edges = [];
function run(){
// Calculate distance between all points
for (var i = 0; i < nodes.length; i++){
nodes[i].calibrate();
waves = 40;
}
var runner = new wave(nodes);
var aco_timer = setInterval(function(){
var result;
waves--;
result = runner.runOnce();
if (!waves || waves < 0){
waves = 40;
clearInterval(aco_timer);
}
edges = [];
for (var i = 0; i < runner.route.length-1; i++){
edges.push({
source: nodes[runner.route[i]]['id']
, target: nodes[runner.route[i+1]]['id']
, x1: nodes[runner.route[i]]['x']
, y1: nodes[runner.route[i]]['y']
, x2: nodes[runner.route[i+1]]['x']
, y2: nodes[runner.route[i+1]]['y']
});
}
paths = paths.data(edges);
paths
.enter()
.insert("line", "g.nodes")
.attr("class", "path")
paths
.attr("stroke", "#333")
.attr("stroke-width", 10)
.attr("stroke-opacity", .1)
.attr("x1", function(d,i){ return d.x1 })
.attr("y1", function(d,i){ return d.y1 })
.attr("x2", function(d,i){ return d.x2 })
.attr("y2", function(d,i){ return d.y2 });
paths
.exit()
.transition()
.remove();
}, 50);
};
run();
}(this);
</script>
</body>
@robertleeplummerjr
Copy link

I fixed the svg cut off that was happening in some modern browsers here: https://gist.github.com/robertleeplummerjr/ba27beef22bfe8ad3d4f/revisions#diff-eacf331f0ffc35d4b482f1d15a887d3bR5
I would have pull requested, but github doesn't currently offer that with gists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment