// === 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.dir2,d.x,d.y);}) .on("click", onclick) .call(d3.drag().on("drag", dragged)) .append("path") .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 origin = [1, 0]; var dot = numeric.dot(origin, dir) var angle = Math.acos(dot) var nd = toDegrees(angle) - 90; 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.dir2 = d.next.dir; d.x = d.next.x; d.y = d.next.y; return getTransform(d.dir2, d.x, d.y, d)} ) .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 dda = getNextAngle2(pt1); if(isNaN(dda[0]) || isNaN(dda[1])) { console.log("zero vector", dda) dda = pt1.dir2; } var dx = pt1.x + TRAVEL_LENGTH[pt1.group] * dda[0]; var dy = pt1.y + TRAVEL_LENGTH[pt1.group] * dda[1]; pt1.next = {"dir": dda, "x":dx, "y":dy, "theta": pt1.next.theta, "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.dir2,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θ) > θτ) { 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)); pt.next.theta = acos if (Math.abs(acos) > θτ) { var theta = Math.sign(acos)*θτ; pt.next.theta = theta var rotMat = [[Math.cos(theta), -Math.sin(theta)], [Math.sin(theta), Math.cos(theta)]] newDir = numeric.dot(rotMat, pt.dir2) } return newDir; } 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, 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 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 angleToVector(a) { var x = Math.cos(a); var y = Math.sin(a); var v = [x, y]; return normalize(v); } 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: angleToVector(a), group: group, next: {"dir": dir, "ddir": getUnitDir(x,y), "theta": 0}, metric: getType(group), show: false }; }; }