|
// === VARIABLES === |
|
// Agent Parameters |
|
var R_r = [d3.select("#zr-1").attr("value"), d3.select("#zr-2").attr("value")]; |
|
var R_o = [d3.select("#zo-1").attr("value"), d3.select("#zo-2").attr("value")]; |
|
var R_a = [d3.select("#za-1").attr("value"), d3.select("#za-2").attr("value")]; |
|
var K = [d3.select("#k-1").attr("value"), d3.select("#k-2").attr("value")]; |
|
var TRAVEL_LENGTH = [d3.select("#speed-1").attr("value"), |
|
d3.select("#speed-2").attr("value")]; |
|
var TURN_RATE = [d3.select("#theta-1").attr("value"), |
|
d3.select("#theta-2").attr("value")]; |
|
var METRICS = [getMetric(d3.select("#metric-1").property("checked")), |
|
getMetric(d3.select("#metric-2").property("checked"))]; |
|
|
|
// Start Conditions |
|
var NUM_AGENTS = d3.select("#agents").attr("value"); |
|
var INIT_SPARSITY = d3.select("#sparsity").attr("value"); |
|
var GROUP_DISTRIBUTION = 1; |
|
|
|
// Helper vars |
|
var POINT_SIZE_1 = 20; |
|
var POINT_SIZE_2 = 30; |
|
var DT = 100; |
|
var timer; |
|
|
|
|
|
// === INIT === |
|
var svg = d3.select("svg"), |
|
width = +svg.attr("width"), |
|
height = +svg.attr("height"), |
|
transform = d3.zoomIdentity; |
|
|
|
var points = d3.range(NUM_AGENTS).map(phyllotaxis(INIT_SPARSITY)); |
|
|
|
var g = svg.append("g"); |
|
|
|
|
|
|
|
// === D3 FUNCTIONS === |
|
var t_p = d3.transition("points") |
|
.duration(DT) |
|
.ease(d3.easeLinear); |
|
var t_r = d3.transition("radii") |
|
.duration(DT) |
|
.ease(d3.easeLinear); |
|
var t = d3.transition() |
|
.duration(DT) |
|
.ease(d3.easeLinear); |
|
var zoom = d3.zoom() |
|
.scaleExtent([1 / 4, 8]) |
|
.on("zoom", zoomed); |
|
function zoomed() { |
|
transform = d3.event.transform; |
|
g.attr("transform", d3.event.transform); |
|
} |
|
|
|
|
|
// === CONTROLS === |
|
var play = function() { |
|
timer = d3.interval(() => { |
|
next(); |
|
}, DT); |
|
} |
|
var pause = function() { |
|
timer.stop(); |
|
} |
|
// parameters |
|
function updateR_r(value,i) { |
|
R_r[i] = value; |
|
d3.selectAll(".R_r-"+i).transition(t) |
|
.attr("r", (d) => { return show_radius(d, R_r[i]); }); |
|
} |
|
function updateR_o(value,i) { |
|
R_o[i] = value; |
|
d3.selectAll(".R_o-"+i).transition(t) |
|
.attr("r", (d) => { return show_radius(d, R_o[i]); }); |
|
} |
|
function updateR_a(value,i) { |
|
R_a[i] = value; |
|
d3.selectAll(".R_a-"+i).transition(t) |
|
.attr("r", (d) => { return show_radius(d, R_a[i]); }); |
|
} |
|
function updateK(value,i) { |
|
K[i] = value; |
|
console.log(K); |
|
} |
|
function updateSpeed(value,i) { |
|
TRAVEL_LENGTH[i] = value; |
|
console.log(TRAVEL_LENGTH); |
|
} |
|
function updateTurnRate(value,i) { |
|
TURN_RATE[i] = value; |
|
console.log(TURN_RATE); |
|
} |
|
function updateNumAgents(value) { |
|
NUM_AGENTS = value; |
|
d3.select("#reset").classed("btn-warning", true); |
|
} |
|
function updateSparsity(value) { |
|
INIT_SPARSITY = value; |
|
d3.select("#reset").classed("btn-warning", true); |
|
} |
|
function updateDistribution(value) { |
|
var d2 = 100-value; |
|
GROUP_DISTRIBUTION = d2/100; |
|
console.log(GROUP_DISTRIBUTION) |
|
d3.select("#d-g1").text(d2 + "%"); |
|
d3.select("#d-g2").text(value + "%"); |
|
d3.select("#reset").classed("btn-warning", true); |
|
} |
|
function changeMetric(value,i) { |
|
var j = i+1; |
|
console.log("metric", i, value); |
|
METRICS[i] = getMetric(value); |
|
if (value) { //distance |
|
d3.select("#ak-box_" + j + "k").attr("hidden", "hidden"); |
|
d3.select("#ak-box_" + j + "a").attr("hidden", null); |
|
} |
|
else { //knn |
|
d3.select("#ak-box_" + j + "a").attr("hidden", "hidden"); |
|
d3.select("#ak-box_" + j + "k").attr("hidden", null); |
|
} |
|
d3.select("#reset").classed("btn-warning", true); |
|
} |
|
//reset |
|
function reset() { |
|
d3.select("#reset").classed("btn-warning", null); |
|
svg.selectAll(".point").remove(); |
|
svg.selectAll(".r").remove(); |
|
points = d3.range(NUM_AGENTS).map(phyllotaxis(INIT_SPARSITY)); |
|
start(); |
|
svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity); |
|
} |
|
//eigenvalues |
|
function printEigenvalues() { |
|
var L = getLaplacian(); |
|
var eigs = getEigenvalues(L); |
|
console.info("Eigenvalues", eigs); |
|
var fiedler = getFiedler(eigs); |
|
var components = getNumComponents(eigs); |
|
d3.select("#eigs").text(fiedler); |
|
d3.select("#num_coms").text(components); |
|
} |
|
|
|
|
|
|
|
// === START === |
|
function start() { |
|
// SETUP POINTS |
|
g.selectAll(".point").data(points).enter() |
|
.append("g") |
|
.attr("id", (d) => "pt-"+d.idx) |
|
.attr("class", "point") |
|
.attr("transform", (d) => {return getTransform(d.dir,d.x,d.y);}) |
|
.on("click", onclick) |
|
.call(d3.drag().on("drag", dragged)) |
|
.append("path") |
|
// .attr("d", (d3.symbol().size(POINT_SIZE).type(d3.symbolTriangle))) |
|
.attr("d", (d) => {return getPointSymbolType(d)()} ) |
|
|
|
|
|
// overlay points on top |
|
g.selectAll(".point").raise(); |
|
|
|
svg.call(zoom); |
|
// color points |
|
updateZones(); |
|
g.selectAll(".point").attr("class", (d) => {return "point " + d.next.style;}); |
|
} |
|
// invoke immediately |
|
start(); |
|
|
|
|
|
|
|
// === HELPER FUNCTIONS === |
|
function show_radius(d, r) { |
|
if (d.show) return r; |
|
else return 0; |
|
} |
|
function randomGroup(i) { |
|
var x = Math.random(); |
|
var group = x > GROUP_DISTRIBUTION ? 1 : 0; |
|
return group; |
|
} |
|
function getType(group) { |
|
return METRICS[group]; |
|
} |
|
function getMetric(checked) { |
|
if (checked) return "dist"; |
|
else return "knn"; |
|
} |
|
function getTransform(dir, x, y) { |
|
var nd = 90 + dir; |
|
var result = "" |
|
result += "rotate(" + nd + "," + x +"," + y +") " |
|
result += "translate(" + x + "," + y + ") " |
|
return result; |
|
} |
|
function getPointSymbolType(d) { |
|
if (d.group === 0) return d3.symbol().size(POINT_SIZE_1).type(d3.symbolTriangle); |
|
else return d3.symbol().size(POINT_SIZE_2).type(d3.symbolCircle); |
|
} |
|
|
|
|
|
// === UPDATE FUNCTIONS === |
|
function next() { |
|
// get next positions |
|
updateZones(); |
|
// move points |
|
g.selectAll(".point").interrupt().transition(t) |
|
.attr("transform",(d) => {d.dir = d.next.dir; d.x = d.next.x; d.y = d.next.y; return getTransform(d.dir, d.x, d.y)} ) |
|
.attr("class", (d) => {return "point " + d.next.style;}); |
|
// move radii |
|
g.selectAll(".r").interrupt().transition(t) |
|
.attr("cx",(d) => { return d.x;}) |
|
.attr("cy",(d) => { return d.y;}); |
|
} |
|
|
|
function updateZones() { |
|
g.selectAll(".point").each((pt1) => { |
|
var r_pts = pointsInRadius(pt1, R_r[pt1.group]), |
|
o_pts = betweenRadii(pt1, R_r[pt1.group], R_o[pt1.group]), |
|
a_pts; |
|
if (pt1.metric === "dist") { |
|
a_pts = betweenRadii(pt1, R_o[pt1.group], R_a[pt1.group]) |
|
} |
|
else { |
|
a_pts = knn(pt1, R_o[pt1.group], K[pt1.group]); |
|
} |
|
var neighbors = []; |
|
Array.prototype.push.apply(neighbors, r_pts); |
|
Array.prototype.push.apply(neighbors, o_pts); |
|
Array.prototype.push.apply(neighbors, a_pts); |
|
var ptclass = ""; |
|
// nr > 0 |
|
if (r_pts.length > 0) { |
|
pt1.next.ddir = r_angle(pt1, r_pts); |
|
ptclass = 'repulsed'; |
|
} |
|
// nr == 0 |
|
else { |
|
if (o_pts.length > 0 || a_pts.length > 0) { |
|
if (a_pts.length > 0 && o_pts.length === 0) { |
|
ptclass = "attracted"; |
|
pt1.next.ddir = a_angle(pt1, a_pts); |
|
} |
|
else if (o_pts.length > 0 && a_pts.length === 0) { |
|
ptclass = "oriented" |
|
pt1.next.ddir = o_angle(pt1, o_pts); |
|
} |
|
else if (o_pts.length > 0 && a_pts.length > 0) { |
|
ptclass = "ao" |
|
var dir_a = a_angle(pt1, a_pts); |
|
var dir_o = o_angle(pt1, o_pts); |
|
var nextdir = numeric.add(dir_a, dir_o); |
|
numeric.diveq(nextdir, 2); |
|
pt1.next.ddir = nextdir; |
|
} |
|
} |
|
// n == 0 |
|
else { |
|
pt1.next.ddir = pt1.dir2; |
|
console.log("no friends", pt1.idx); |
|
} |
|
} |
|
var da = getNextAngle(pt1); |
|
var dda = getNextAngle2(pt1); |
|
var dx = pt1.x + TRAVEL_LENGTH[pt1.group] * Math.cos(toRadians(da)); |
|
var dy = pt1.y + TRAVEL_LENGTH[pt1.group] * Math.sin(toRadians(da)); |
|
pt1.next = {"dir": da, "x":dx, "y":dy, "style": ptclass}; |
|
pt1.neighbors = neighbors; |
|
}); |
|
} |
|
|
|
|
|
// === EVENT HANDLERS === |
|
function onclick(d) { |
|
d.show = !d.show; |
|
console.info(d); |
|
|
|
if (d.show) { |
|
// Attraction |
|
if (d.metric === "dist") { |
|
g.append("circle").datum(d).lower() |
|
.attr("class", (d) => {return "r R_a r--" + d.idx + " R_a-" + d.group;}) |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }) |
|
.attr("r", 0) |
|
.attr("opacity", 0.10) |
|
.transition(t) |
|
.attr("r", (d) => { return show_radius(d, R_a[d.group]); }); |
|
} |
|
|
|
// Orientation |
|
g.append("circle").datum(d).lower() |
|
.attr("class", (d) => {return "r R_o r--" + d.idx + " R_o-" + d.group;}) |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }) |
|
.attr("r", 0) |
|
.attr("opacity", 0.15) |
|
.transition(t) |
|
.attr("r", (d) => { return show_radius(d, R_o[d.group]); }); |
|
// Repulsion |
|
g.append("circle").datum(d).lower() |
|
.attr("class", (d) => {return "r R_r r--" + d.idx + " R_r-" + d.group;}) |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }) |
|
.attr("r", 0) |
|
.attr("opacity", 0.35) |
|
.transition(t) |
|
.attr("r", (d) => { return show_radius(d, R_r[d.group]); }); |
|
} |
|
else { |
|
d3.select(".R_r.r--"+d.idx).transition(t).attr("r", 0).remove(); |
|
d3.select(".R_o.r--"+d.idx).transition(t).attr("r", 0).remove(); |
|
d3.select(".R_a.r--"+d.idx).transition(t).attr("r", 0).remove(); |
|
} |
|
} |
|
function dragged(d) { |
|
dx = d3.event.x; |
|
dy = d3.event.y; |
|
// update point |
|
var pt = d3.select("#pt-"+d.idx) |
|
.attr("transform", (d) => {return getTransform(d.dir,dx,dy);}) |
|
// update collision state |
|
updateZones(); |
|
g.selectAll(".point").attr("class", (d) => {return "point " + d.next.style;}); |
|
// update radius |
|
d3.select(".R_r.r--" + d.idx) |
|
.attr("cx", d.x = dx).attr("cy", d.y = dy); |
|
d3.select(".R_o.r--" + d.idx) |
|
.attr("cx", d.x = dx).attr("cy", d.y = dy); |
|
d3.select(".R_a.r--" + d.idx) |
|
.attr("cx", d.x = dx).attr("cy", d.y = dy); |
|
} |
|
|
|
|
|
// === MATH FUNCTIONS === |
|
function dist(pt1, pt2) { |
|
return Math.pow((pt1.x-pt2.x),2) + Math.pow((pt1.y-pt2.y),2); |
|
} |
|
|
|
function getNextAngle(pt) { |
|
var θτ = TURN_RATE[pt.group]; |
|
var dθ = (pt.dir - pt.next.ddir); |
|
var newDir = pt.next.ddir; |
|
|
|
if (Math.abs(dθ) > θτ) { |
|
// console.log("dθ", dθ, pt.dir, pt.next.ddir); |
|
newDir = pt.dir + Math.sign(dθ)*θτ; |
|
} |
|
return newDir % 360; |
|
} |
|
|
|
function getNextAngle2(pt) { |
|
var θτ = TURN_RATE[pt.group]; |
|
var dθ = numeric.sub(pt.dir2, pt.next.ddir) |
|
var newDir = pt.next.ddir; |
|
var dot = numeric.dot(pt.dir2, pt.next.ddir); |
|
var acos = toDegrees(Math.acos(dot)); |
|
console.log("dθ", pt.idx, acos, dθ, dot, pt.dir2, pt.next.ddir); |
|
if (Math.abs(acos) > θτ) { |
|
|
|
newDir = pt.dir + Math.sign(dθ)*θτ; |
|
} |
|
return newDir % 360; |
|
} |
|
|
|
function getLaplacian() { |
|
var L = []; |
|
g.selectAll(".point").each((p1,i,nodes) => { |
|
L[p1.idx] = Array(nodes.length).fill(0); |
|
for (var p2 of p1.neighbors) { |
|
L[p1.idx][p2.idx] = -1; |
|
} |
|
L[p1.idx][p1.idx] = -1 * L[p1.idx].reduce( ( acc, cur ) => acc + cur, 0 ); |
|
}); |
|
console.info("Graph Laplacian:", L.join('\n')); |
|
return L; |
|
} |
|
|
|
function getEigenvalues(L) { |
|
var eigs = numeric.eig(L); |
|
return eigs['lambda'].x.map((x) => +x.toFixed(3)).sort(); |
|
} |
|
|
|
function getFiedler(eigs) { |
|
return eigs.find((v) => {return v > 0;}) |
|
} |
|
|
|
function getNumComponents(eigs) { |
|
return eigs.filter((v) => {return v === 0;}).length; |
|
} |
|
|
|
// function r_angle(d, pts) { |
|
// var sum = 0; |
|
// for (var p of pts) { |
|
// sum += Math.atan2(p.y-d.y, p.x-d.x); |
|
// } |
|
// var ave = sum / pts.length; |
|
// return toDegrees(ave) + 180; |
|
// } |
|
|
|
function r_angle(d, pts) { |
|
var sum = [0, 0]; |
|
for (var p of pts) { |
|
r = [p.x-d.x, p.y-d.y]; |
|
r = normalize(r); |
|
numeric.addeq(sum,r); |
|
} |
|
var ave = normalize(sum); |
|
ave = numeric.neg(ave); |
|
return ave; |
|
} |
|
|
|
function a_angle(d, pts) { |
|
var sum = [0, 0]; |
|
for (var p of pts) { |
|
r = [p.x-d.x, p.y-d.y]; |
|
r = normalize(r); |
|
numeric.addeq(sum,r); |
|
} |
|
var ave = normalize(sum); |
|
return ave; |
|
} |
|
|
|
function o_angle(d, pts) { |
|
var sum = [0, 0]; |
|
for (var p of pts) { |
|
numeric.addeq(sum, p.dir2); |
|
} |
|
var ave = normalize(sum); |
|
return ave; |
|
} |
|
|
|
|
|
// function a_angle(d, pts) { |
|
// var sum = 0; |
|
// for (var p of pts) { |
|
// sum += Math.atan2(p.y-d.y, p.x-d.x); |
|
// } |
|
// var ave = sum / pts.length; |
|
// var result = toDegrees(ave); |
|
// // console.log("a", result); |
|
// return result; |
|
// } |
|
|
|
// function o_angle(d, pts) { |
|
// var sum = 0; |
|
// for (var p of pts) { |
|
// sum += p.dir; |
|
// } |
|
// var ave = sum / pts.length; |
|
// return ave; |
|
// } |
|
|
|
function knn(pt1,r, k) { |
|
var neighbors = []; |
|
var compare = (a,b) => {return a.dist - b.dist;} |
|
g.selectAll(".point").each((d) => { |
|
if (pt1.idx != d.idx && !withinRadius(pt1, d, r)) { |
|
var item = {"dist": dist(pt1,d), "point": d} |
|
neighbors.push(item); |
|
} |
|
}); |
|
neighbors.sort(compare); |
|
var result = neighbors.map((n) => n.point); |
|
return result.slice(0,k); |
|
} |
|
|
|
|
|
function pointsInRadius(pt1, r) { |
|
var result = []; |
|
d3.selectAll(".point").each((d) => { |
|
if (pt1.idx != d.idx && withinRadius(pt1, d, r)) { |
|
result.push(d); |
|
} |
|
}); |
|
return result; |
|
} |
|
|
|
function betweenRadii(pt1, r_inner, r_outer) { |
|
var result = []; |
|
d3.selectAll(".point").each((d) => { |
|
if (pt1.idx != d.idx && withinRadius(pt1, d, r_outer) && !withinRadius(pt1, d, r_inner)) { |
|
result.push(d); |
|
} |
|
}); |
|
return result; |
|
} |
|
|
|
function withinRadius(pt1, pt2, r) { |
|
var dx = pt1.x-pt2.x; |
|
var dy = pt1.y-pt2.y; |
|
|
|
return dx*dx + dy*dy <= r*r; |
|
} |
|
|
|
function toDegrees (angle) { |
|
return (angle * (180 / Math.PI)) % 360; |
|
} |
|
|
|
function toRadians (angle) { |
|
return angle * (Math.PI / 180); |
|
} |
|
|
|
function getRandomNum(min, max) { |
|
return Math.random() * (max - min) + min; |
|
} |
|
|
|
function normalize(v) { |
|
var mag = numeric.norm2(v); |
|
return numeric.div(v,mag); |
|
} |
|
|
|
function getUnitDir(x,y) { |
|
var v = [x,y]; |
|
var mag = numeric.norm2(v); |
|
return numeric.div(v,mag); |
|
} |
|
|
|
function phyllotaxis(radius) { |
|
var theta = Math.PI * (3 - Math.sqrt(5)); |
|
return function(i) { |
|
var r = radius * Math.sqrt(i), a = theta * i; |
|
var group = randomGroup(i); |
|
var dir = toDegrees(a)+getRandomNum(0,360); |
|
var x = width / 2 + r * Math.cos(a); |
|
var y = height / 2 + r * Math.sin(a); |
|
return { |
|
idx: i, |
|
x: x, |
|
y: y, |
|
dir: dir, |
|
dir2: getUnitDir(x,y), |
|
group: group, |
|
next: {"dir": dir, "ddir": getUnitDir(x,y)}, |
|
metric: getType(group), |
|
show: false //only for the center one |
|
}; |
|
}; |
|
} |