ensilico
Version:
Synthetic data generation, simulated vehicles
679 lines (586 loc) • 20.7 kB
JavaScript
var Scalar = {};
// A bit larger than sqrt(MIN_NORMAL)
Scalar.tiny = function() {
return 1e-154;
};
Scalar.toRadians = function(degrees) {
return degrees * Math.PI / 180;
};
// Returns the ratio or the specified upper limit
Scalar.rationalMin = function(numerator, denominator, maxRatio) {
var fn = maxRatio * numerator;
var fd = maxRatio * denominator;
return fn / Math.max(numerator, fd);
};
Scalar.lerp = function(f1, f2, t) {
return (1 - t) * f1 + t * f2;
};
// Returns next state
Scalar.lag = function(state, target, responsiveness, stepsize) {
var k = responsiveness * stepsize;
return (k * target + state) / (k + 1);
};
// Returns rate for integration
Scalar.lagRate = function(state, target, responsiveness, stepsize) {
return responsiveness * (target - state) / (responsiveness * stepsize + 1);
};
Scalar.integrate = function(state, rate, stepsize) {
return stepsize * rate + state;
};
function Pair() {
this.x = 0;
this.y = 0;
}
Pair.ZERO = new Pair();
Pair.prototype.norm = function() {
return Math.sqrt(this.dot(this));
};
Pair.prototype.dot = function(p) {
return this.x * p.x + this.y * p.y;
};
Pair.prototype.cross = function(p) {
return this.x * p.y - this.y * p.x;
}
Pair.prototype.load = function(p) {
this.x = p.x;
this.y = p.y;
return this;
};
Pair.prototype.loadDelta = function(p1, p2) {
this.x = p1.x - p2.x;
this.y = p1.y - p2.y;
return this;
};
Pair.prototype.loadPolar = function(r, theta) {
this.x = r * Math.cos(theta);
this.y = r * Math.sin(theta);
return this;
};
// Load this with scalar product
Pair.prototype.loadProduct = function(f, p) {
this.x = f * p.x;
this.y = f * p.y;
return this;
};
// Load this with the result of (0,0,z) X p
Pair.prototype.loadCrossProduct = function(z, p) {
this.x = -z * p.y;
this.y = z * p.x;
return this;
};
Pair.prototype.loadMidpoint = function(p1, p2) {
this.x = 0.5 * (p1.x + p2.x);
this.y = 0.5 * (p1.y + p2.y);
return this;
};
Pair.prototype.loadLerp = function(p1, p2, t) {
this.x = (1 - t) * p1.x + t * p2.x;
this.y = (1 - t) * p1.y + t * p2.y;
return this;
};
Pair.prototype.negate = function() {
this.x = -this.x;
this.y = -this.y;
return this;
};
Pair.prototype.multiplyBy = function(f) {
this.x *= f;
this.y *= f;
return this;
};
// Caller is responsible for avoiding division by zero
Pair.prototype.divideBy = function(f) {
var reciprocal = 1 / f;
this.x *= reciprocal;
this.y *= reciprocal;
return this;
};
Pair.prototype.rotateBy = function(angle) {
var c = Math.cos(angle);
var s = Math.sin(angle);
var x = this.x * c - this.y * s;
var y = this.x * s + this.y * c;
this.x = x;
this.y = y;
return this;
};
Pair.prototype.add = function(p) {
this.x += p.x;
this.y += p.y;
return this;
};
// Add scalar product
Pair.prototype.addProduct = function(f, p) {
this.x += f * p.x;
this.y += f * p.y;
return this;
};
Pair.prototype.subtract = function(p) {
this.x -= p.x;
this.y -= p.y;
return this;
};
// Subtract scalar product
Pair.prototype.subtractProduct = function(f, p) {
this.x -= f * p.x;
this.y -= f * p.y;
return this;
};
// Subtract the projection of this onto p
// leaving only the perpendicular component of this
Pair.prototype.subtractProjection = function(p) {
var f = this.dot(p) / (p.x * p.x + p.y * p.y + Scalar.tiny());
return this.subtractProduct(f, p);
};
Pair.prototype.normalize = function() {
return this.divideBy(this.norm() + Scalar.tiny());
};
Pair.prototype.integrate = function(rate, stepsize) {
this.x += stepsize * rate.x;
this.y += stepsize * rate.y;
return this;
};
function Particle() {
// Default values
this.mass = 0.005;
this.drag = 0.001;
this.buoyancy = 0.0005; // buoyancy to weight ratio in air
this.position = new Pair();
this.velocity = new Pair();
// Use closure to avoid object allocation in inner loop
var reusage = {
force: new Pair()
};
this.reusage = function() {
return reusage;
};
}
Particle.prototype.update = function(stepsize, geosphere, externalForce) {
var massRate = this.mass / (stepsize + Scalar.tiny());
var densityRatio = geosphere.density(this.position);
var drag = this.drag * densityRatio;
var force = this.reusage().force;
force
.load(externalForce)
.addProduct(this.mass * (1 - densityRatio * this.buoyancy), geosphere.gravity)
.addProduct(massRate, this.velocity);
var denominator = massRate + drag * this.velocity.norm();
this.velocity.load(force).divideBy(denominator);
this.position.integrate(this.velocity, stepsize);
};
// Pivots about the origin
function Flexor() {
// Default values
this.overallLength = 3;
this.pivotOffset = 0.2;
this.mass = 0.1;
this.spring = 10;
this.damping = 0.1;
this.drag = 0.1;
this.tipPosition = new Pair();
this.alignTo(0);
this.tipVelocity = new Pair();
// Use closure to avoid object allocation in inner loop
var reusage = {
force: new Pair(),
velocity: new Pair()
};
this.reusage = function() {
return reusage;
};
}
Flexor.prototype.radius = function() {
return this.overallLength - this.pivotOffset;
};
Flexor.prototype.alignTo = function(angle) {
this.tipPosition.loadPolar(this.radius(), angle);
return this;
};
Flexor.prototype.update = function(stepsize, geosphere, externalForce, targetPosition, targetVelocity) {
var massRate = this.mass / (stepsize + Scalar.tiny());
var reusage = this.reusage();
var force = reusage.force;
var velocity = reusage.velocity;
force
.load(externalForce)
.addProduct(this.mass, geosphere.gravity)
.addProduct(massRate, this.tipVelocity)
.addProduct(this.spring, targetPosition)
.addProduct(this.damping, targetVelocity);
force.subtractProjection(this.tipPosition);
var denominator =
massRate +
stepsize * this.spring +
this.damping +
this.drag * this.tipVelocity.norm();
velocity.load(force).divideBy(denominator);
var norm = this.tipPosition.norm();
var corrector = (this.radius() - norm) / (stepsize * norm + Scalar.tiny());
velocity.addProduct(corrector, this.tipPosition);
this.tipVelocity.load(velocity);
this.tipPosition.integrate(velocity, stepsize);
};
function Filament(headPosition, tailPosition) {
// Default values
this.spacing = 0.2;
this.mass = 0.0005;
this.spring = 10;
this.damping = 0.1;
this.drag = 0.0001;
this.buoyancy = 0.0005; // buoyancy to weight ratio in air
var n = Filament.numSegments();
this.position = [];
this.velocity = [];
for (var i = 0; i <= n; i++) {
var p = new Pair();
p.loadLerp(headPosition, tailPosition, i / n);
this.position.push(p);
this.velocity.push(new Pair());
}
this.upAxis = [];
this.downAxis = [new Pair()]; // unused first element
for (var i = 0; i < n; i++) {
this.upAxis.push(new Pair());
this.downAxis.push(new Pair());
}
this.updateAxes();
// Use closure to avoid object allocation in inner loop
var reusage = {
deltaPosition: new Pair(),
deltaVelocity: new Pair(),
accumulator: new Pair(),
leadForce: new Pair(),
lagForce: new Pair()
};
this.reusage = function() {
return reusage;
};
}
Filament.numSegments = function() {
return 20;
};
Filament.prototype.updateAxes = function() {
var n = Filament.numSegments();
for (var i = 0; i < n; i++) {
this.upAxis[i]
.loadDelta(this.position[i+1], this.position[i])
.normalize();
this.downAxis[i+1]
.load(this.upAxis[i])
.negate();
}
};
Filament.prototype.scalarForce = function(i, sense, axis) {
var reusage = this.reusage();
var deltaPosition = reusage.deltaPosition;
var deltaVelocity = reusage.deltaVelocity;
var accumulator = reusage.accumulator;
deltaPosition.loadDelta(this.position[i+sense], this.position[i]);
deltaVelocity.loadDelta(this.velocity[i+sense], this.velocity[i]);
accumulator
.loadProduct(this.spring, deltaPosition)
.addProduct(this.damping, deltaVelocity);
return accumulator.dot(axis[i]) - this.spring * this.spacing;
};
Filament.prototype.storeHeadForce = function(geosphere, headForce) {
var densityRatio = geosphere.density(this.position);
headForce
.loadProduct(this.mass * (1 - densityRatio * this.buoyancy), geosphere.gravity)
.addProduct(this.scalarForce(0, 1, this.upAxis), this.upAxis[0]);
};
Filament.prototype.storeTailForce = function(geosphere, tailForce) {
var densityRatio = geosphere.density(this.position);
var n = Filament.numSegments();
tailForce
.loadProduct(this.mass * (1 - densityRatio * this.buoyancy), geosphere.gravity)
.addProduct(this.scalarForce(n, -1, this.downAxis), this.downAxis[n]);
};
Filament.prototype.update = function(stepsize, geosphere, headPosition, headVelocity, tailPosition, tailVelocity) {
var n = Filament.numSegments();
this.position[0].load(headPosition);
this.velocity[0].load(headVelocity);
this.position[n].load(tailPosition);
this.velocity[n].load(tailVelocity);
this.updateAxes();
var halfstep = 0.5 * stepsize;
var massRate = this.mass / (halfstep + Scalar.tiny());
var lumped = this.spring * halfstep + this.damping;
this.updateCore(halfstep, geosphere, massRate, lumped, 0, n, 1, this.upAxis);
this.updateCore(halfstep, geosphere, massRate, lumped, n, 0, -1, this.downAxis);
};
Filament.prototype.updateCore = function(stepsize, geosphere, massRate, lumped, start, end, sense, axis) {
var reusage = this.reusage();
var leadForce = reusage.leadForce;
var lagForce = reusage.lagForce;
var accumulator = reusage.accumulator;
for (var i = start + sense; i != end; i += sense) {
var densityRatio = geosphere.density(this.position[i]);
leadForce
.loadDelta(this.position[i+sense], this.position[i])
.multiplyBy(this.spring)
.addProduct(this.damping, this.velocity[i+sense]);
lagForce
.loadProduct(massRate, this.velocity[i])
.addProduct(this.mass * (1 - densityRatio * this.buoyancy), geosphere.gravity)
.subtractProduct(this.scalarForce(i-sense, sense, axis), axis[i-sense])
.subtractProduct(this.spring * this.spacing, axis[i]);
var fluid = this.drag * densityRatio * this.velocity[i].norm();
accumulator
.loadProduct(massRate + fluid, leadForce)
.subtractProduct(lumped - fluid, lagForce)
.divideBy(massRate + lumped);
this.velocity[i]
.loadProduct(accumulator.dot(axis[i]), axis[i])
.add(lagForce)
.divideBy(massRate + fluid);
this.position[i].integrate(this.velocity[i], stepsize);
}
};
Filament.prototype.visualize = function(context, elapsedTime) {
var accumulator = this.reusage().accumulator;
context.moveTo(this.position[0].x, this.position[0].y);
var len = Filament.numSegments() + 1;
for (var i = 0; i < len; i++) {
accumulator.loadMidpoint(this.position[i], this.position[Math.min(i+1, len-1)]);
context.quadraticCurveTo(
this.position[i].x,
this.position[i].y,
accumulator.x,
accumulator.y);
}
};
var Platform = {};
// Copies values from src for corresponding properties in dst
// Returns dst
// Optional log handler
Platform.softCopy = function(dst, src, log) {
for (var key in src) {
var srcOwn = src.hasOwnProperty(key);
var dstOwn = dst.hasOwnProperty(key);
// The key must be in both objects
if (srcOwn && dstOwn) {
var srcValue = src[key];
var dstValue = dst[key];
var srcType = typeof srcValue;
var dstType = typeof dstValue;
// Both values must have the same type
if (srcType == dstType) {
if (srcType != "object") {
dst[key] = srcValue;
} else if (srcValue && dstValue) {
Platform.softCopy(dstValue, srcValue, log);
} else if (log) {
if (!srcValue) {
log("src error at " + key);
}
if (!dstValue) {
log("dst error at " + key);
}
}
} else if (log) {
log("type mismatch at " + key);
}
} else if (log) {
if (!srcOwn) {
log("skip src key " + key);
}
if (!dstOwn) {
log("no dst key " + key);
}
}
}
return dst;
};
Platform.getJson = function(url, success) {
if (!url) {
success({});
} else {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
var json = JSON.parse(xhr.responseText);
success(json);
}
};
xhr.send();
}
};
Platform.load = function(dst, url, log, onLoad, getJsonArg) {
var getJson = getJsonArg || Platform.getJson;
getJson(url, function(json) {
Platform.softCopy(dst, json, log);
onLoad(dst);
});
}
var FrameTimer = (function() {
var listeners = [];
var mainWindow = null;
var previousTimestamp = 0;
function nextFrame() {
mainWindow.requestAnimationFrame(onFrame);
}
function onFrame(timestamp) {
var elapsedTime = (timestamp - previousTimestamp) / 1000;
previousTimestamp = timestamp;
var len = listeners.length;
for (var i = 0; i < len; i++) {
listeners[i](elapsedTime);
}
nextFrame();
}
return {
addListener: function(windowArg, listener) {
listeners.push(listener);
if (!mainWindow) {
mainWindow = windowArg;
previousTimestamp = windowArg.performance.now();
nextFrame();
}
}
};
})();
var Executive = (function() {
var agents = {};
function Agent(simulation, canvas, log) {
this.simulation = simulation;
this.context = canvas.getContext("2d");
this.simulationLeadTime = 0;
this.onBreakFrame = null;
this.clutch = new Clutch();
// Default values
var preferences = {
visualScale: 100,
stepsize: 0.01,
maxStepsPerFrame: 100
};
// Allow simulation to override
Platform.softCopy(preferences, simulation.preferences, log);
this.visualScale = preferences.visualScale;
this.controls = {
stepsize: preferences.stepsize,
pointer: {
contact: 0,
velocity: new Pair()
}
};
this.maxStepsPerFrame = preferences.maxStepsPerFrame;
this.registerListeners(canvas);
}
Agent.prototype.registerListeners = function(canvas) {
var clutch = this.clutch;
canvas.addEventListener("mousedown", function(event) {
clutch.onEngage(event.clientX, event.clientY);
}, false);
canvas.addEventListener("mousemove", function(event) {
clutch.onMove(event.clientX, event.clientY);
}, false);
canvas.addEventListener("mouseup", function() {
clutch.onDisengage();
}, false);
canvas.addEventListener("touchstart", function(event) {
var touch = event.touches[0];
clutch.onEngage(touch.clientX, touch.clientY);
}, false);
canvas.addEventListener("touchmove", function(event) {
var touch = event.touches[0];
clutch.onMove(touch.clientX, touch.clientY);
}, false);
canvas.addEventListener("touchend", function() {
clutch.onDisengage();
}, false);
};
Agent.prototype.onFrame = function(elapsedTime) {
var stepsize = this.controls.stepsize;
// Adjust for simulation lead from previous frame
var simulationTime = elapsedTime - this.simulationLeadTime;
// Number of steps needed to meet or exceed the adjusted time
// Impose an upper limit because the browser might pause the animation indefinitely
var numSteps = Math.ceil(Scalar.rationalMin(simulationTime, stepsize, this.maxStepsPerFrame));
// Calculate adjustment for the next frame
var period = numSteps * stepsize;
this.simulationLeadTime = Math.max(0, period - simulationTime);
// Calculate pointer velocity
var contact = this.clutch.engaged ? 1 : 0;
this.controls.pointer.contact = contact;
this.controls.pointer.velocity
.loadDelta(this.clutch.currentPosition, this.clutch.basePosition)
.multiplyBy(contact / (period + Scalar.tiny()));
this.clutch.rebase();
// Update the simulation
this.updateSimulation(numSteps);
// Simple visualization
this.visualizeSimulation(elapsedTime);
};
Agent.prototype.updateSimulation = function(numSteps) {
if (this.onBreakFrame) {
this.onBreakFrame(this.simulation);
this.onBreakFrame = null;
}
for (var i = 0; i < numSteps; i++) {
this.simulation.update(this.controls);
}
};
Agent.prototype.visualizeSimulation = function(elapsedTime) {
var context = this.context;
var w = context.canvas.width;
var h = context.canvas.height;
context.clearRect(0, 0, w, h);
context.save();
context.translate(w / 2, h / 2);
context.scale(this.visualScale, -this.visualScale);
context.beginPath();
this.simulation.visualize(context, elapsedTime);
context.restore();
context.stroke();
};
return {
// Simulation to canvas is assumed to be 1:1
// Starting an id already started is neither supported nor defined
// Optional windowArg is for dependency injection
start: function(id, simulation, url, windowArg) {
var mainWindow = windowArg || window;
var log = function(msg) {
mainWindow.console.log(msg);
};
var canvas = mainWindow.document.getElementById(id);
var agent = new Agent(simulation, canvas, log);
agents[id] = agent;
Platform.load(simulation, url, log, function() {
FrameTimer.addListener(mainWindow, function(elapsedTime) {
agent.onFrame(elapsedTime);
});
});
},
breakFrame: function(id, onBreakFrame, debugArg) {
var debug = debugArg || function() {
debugger;
};
agents[id].onBreakFrame = function(sim) {
onBreakFrame(sim);
debug();
};
}
};
})();
function Clutch() {
this.engaged = false;
this.currentPosition = new Pair();
this.basePosition = new Pair();
}
Clutch.prototype.onEngage = function(x, y) {
this.engaged = true;
this.currentPosition.x = x;
this.currentPosition.y = y;
this.rebase();
};
Clutch.prototype.onMove = function(x, y) {
this.currentPosition.x = x;
this.currentPosition.y = y;
};
Clutch.prototype.onDisengage = function() {
this.engaged = false;
};
Clutch.prototype.rebase = function() {
this.basePosition.load(this.currentPosition);
};