Browse Source

Network: Cleanup and refactoring PhysicsEngine (#3507)

* Initial refactoring

* Small refactoring BarnesHutSolver

* Disabled debug routine, empty lines removed

* Put back space in comment

* Typo
jittering-top
wimrijnders 6 years ago
committed by Yotam Berkowitz
parent
commit
ecdb59edd6
2 changed files with 218 additions and 167 deletions
  1. +167
    -130
      lib/network/modules/PhysicsEngine.js
  2. +51
    -37
      lib/network/modules/components/physics/BarnesHutSolver.js

+ 167
- 130
lib/network/modules/PhysicsEngine.js View File

@ -6,8 +6,8 @@ var HierarchicalSpringSolver = require('./components/physics/HierarchicalSpringS
var CentralGravitySolver = require('./components/physics/CentralGravitySolver').default;
var ForceAtlas2BasedRepulsionSolver = require('./components/physics/FA2BasedRepulsionSolver').default;
var ForceAtlas2BasedCentralGravitySolver = require('./components/physics/FA2BasedCentralGravitySolver').default;
var util = require('../../util');
var EndPoints = require('./components/edges/util/EndPoints').default; // for debugging with _drawForces()
/**
@ -121,9 +121,8 @@ class PhysicsEngine {
this.stopSimulation(false);
this.body.emitter.off();
});
// this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed.
this.body.emitter.on("_dataChanged", () => {
// update shortcut lists
// Nodes and/or edges have been added or removed, update shortcut lists.
this.updatePhysicsData();
});
@ -308,85 +307,95 @@ class PhysicsEngine {
}
}
/**
* A single simulation step (or 'tick') in the physics simulation
* Calculate the forces for one physics iteration and move the nodes.
* @private
*/
physicsStep() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
this.moveNodes();
}
/**
* Make dynamic adjustments to the timestep, based on current state.
*
* Helper function for physicsTick().
* @private
*/
physicsTick() {
// this is here to ensure that there is no start event when the network is already stable.
if (this.startedStabilization === false) {
this.body.emitter.emit('startStabilizing');
this.startedStabilization = true;
}
if (this.stabilized === false) {
// adaptivity means the timestep adapts to the situation, only applicable for stabilization
if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) {
// this is the factor for increasing the timestep on success.
let factor = 1.2;
// we assume the adaptive interval is
if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations.
// first the big step and revert. Revert saves the reference state.
this.timestep = 2 * this.timestep;
this.calculateForces();
this.moveNodes();
this.revert();
// now the normal step. Since this is the last step, it is the more stable one and we will take this.
this.timestep = 0.5 * this.timestep;
// since it's half the step, we do it twice.
this.calculateForces();
this.moveNodes();
this.calculateForces();
this.moveNodes();
// we compare the two steps. if it is acceptable we double the step.
if (this._evaluateStepQuality() === true) {
this.timestep = factor * this.timestep;
}
else {
// if not, we decrease the step to a minimum of the options timestep.
// if the decreased timestep is smaller than the options step, we do not reset the counter
// we assume that the options timestep is stable enough.
if (this.timestep/factor < this.options.timestep) {
this.timestep = this.options.timestep;
}
else {
// if the timestep was larger than 2 times the option one we check the adaptivity again to ensure
// that large instabilities do not form.
this.adaptiveCounter = -1; // check again next iteration
this.timestep = Math.max(this.options.timestep, this.timestep/factor);
}
}
}
else {
// normal step, keeping timestep constant
this.calculateForces();
this.moveNodes();
}
adjustTimeStep() {
const factor = 1.2; // Factor for increasing the timestep on success.
// increment the counter
this.adaptiveCounter += 1;
// we compare the two steps. if it is acceptable we double the step.
if (this._evaluateStepQuality() === true) {
this.timestep = factor * this.timestep;
}
else {
// if not, we decrease the step to a minimum of the options timestep.
// if the decreased timestep is smaller than the options step, we do not reset the counter
// we assume that the options timestep is stable enough.
if (this.timestep/factor < this.options.timestep) {
this.timestep = this.options.timestep;
}
else {
// case for the static timestep, we reset it to the one in options and take a normal step.
this.timestep = this.options.timestep;
this.calculateForces();
this.moveNodes();
// if the timestep was larger than 2 times the option one we check the adaptivity again to ensure
// that large instabilities do not form.
this.adaptiveCounter = -1; // check again next iteration
this.timestep = Math.max(this.options.timestep, this.timestep/factor);
}
}
}
/**
* A single simulation step (or 'tick') in the physics simulation
*
* @private
*/
physicsTick() {
this._startStabilizing(); // this ensures that there is no start event when the network is already stable.
if (this.stabilized === true) return;
// adaptivity means the timestep adapts to the situation, only applicable for stabilization
if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) {
// timestep remains stable for "interval" iterations.
let doAdaptive = (this.adaptiveCounter % this.adaptiveInterval === 0);
// determine if the network has stabilzied
if (this.stabilized === true) {
this.revert();
if (doAdaptive) {
// first the big step and revert.
this.timestep = 2 * this.timestep;
this.physicsStep();
this.revert(); // saves the reference state
// now the normal step. Since this is the last step, it is the more stable one and we will take this.
this.timestep = 0.5 * this.timestep;
// since it's half the step, we do it twice.
this.physicsStep();
this.physicsStep();
this.adjustTimeStep();
}
else {
this.physicsStep(); // normal step, keeping timestep constant
}
this.stabilizationIterations++;
this.adaptiveCounter += 1;
}
else {
// case for the static timestep, we reset it to the one in options and take a normal step.
this.timestep = this.options.timestep;
this.physicsStep();
}
if (this.stabilized === true) this.revert();
this.stabilizationIterations++;
}
/**
* Nodes and edges can have the physics toggles on or off. A collection of indices is created here so we can skip the check all the time.
*
@ -497,7 +506,6 @@ class PhysicsEngine {
*/
moveNodes() {
var nodeIndices = this.physicsBody.physicsNodeIndices;
var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9;
var maxNodeVelocity = 0;
var averageNodeVelocity = 0;
@ -506,9 +514,9 @@ class PhysicsEngine {
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
let nodeVelocity = this._performStep(nodeId, maxVelocity);
let nodeVelocity = this._performStep(nodeId);
// stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity);
maxNodeVelocity = Math.max(maxNodeVelocity, nodeVelocity);
averageNodeVelocity += nodeVelocity;
}
@ -518,66 +526,72 @@ class PhysicsEngine {
}
/**
* Calculate new velocity for a coordinate direction
*
* @param {number} v velocity for current coordinate
* @param {number} f regular force for current coordinate
* @param {number} m mass of current node
* @returns {number} new velocity for current coordinate
* @private
*/
calculateComponentVelocity(v,f, m) {
let df = this.modelOptions.damping * v; // damping force
let a = (f - df) / m; // acceleration
v += a * this.timestep;
// Put a limit on the velocities if it is really high
let maxV = this.options.maxVelocity || 1e9;
if (Math.abs(v) > maxV) {
v = ((v > 0) ? maxV: -maxV);
}
return v;
}
/**
* Perform the actual step
*
* @param {Node.id} nodeId
* @param {number} maxVelocity
* @returns {number}
* @returns {number} the new velocity of given node
* @private
*/
_performStep(nodeId, maxVelocity) {
_performStep(nodeId) {
let node = this.body.nodes[nodeId];
let timestep = this.timestep;
let forces = this.physicsBody.forces;
let velocities = this.physicsBody.velocities;
let force = this.physicsBody.forces[nodeId];
let velocity = this.physicsBody.velocities[nodeId];
// store the state so we can revert
this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y};
this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocity.x, vy:velocity.y};
if (node.options.fixed.x === false) {
let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force
let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
velocities[nodeId].x += ax * timestep; // velocity
velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x;
node.x += velocities[nodeId].x * timestep; // position
velocity.x = this.calculateComponentVelocity(velocity.x, force.x, node.options.mass);
node.x += velocity.x * this.timestep;
}
else {
forces[nodeId].x = 0;
velocities[nodeId].x = 0;
force.x = 0;
velocity.x = 0;
}
if (node.options.fixed.y === false) {
let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force
let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
velocities[nodeId].y += ay * timestep; // velocity
velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y;
node.y += velocities[nodeId].y * timestep; // position
velocity.y = this.calculateComponentVelocity(velocity.y, force.y, node.options.mass);
node.y += velocity.y * this.timestep;
}
else {
forces[nodeId].y = 0;
velocities[nodeId].y = 0;
force.y = 0;
velocity.y = 0;
}
let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
let totalVelocity = Math.sqrt(Math.pow(velocity.x,2) + Math.pow(velocity.y,2));
return totalVelocity;
}
/**
* calculate the forces for one physics iteration.
*/
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
}
/**
* When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
* because only the supportnodes for the smoothCurves have to settle.
* When initializing and stabilizing, we can freeze nodes with a predefined position.
* This greatly speeds up stabilization because only the supportnodes for the smoothCurves have to settle.
*
* @private
*/
@ -586,14 +600,16 @@ class PhysicsEngine {
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].x && nodes[id].y) {
this.freezeCache[id] = {x:nodes[id].options.fixed.x,y:nodes[id].options.fixed.y};
nodes[id].options.fixed.x = true;
nodes[id].options.fixed.y = true;
let fixed = nodes[id].options.fixed;
this.freezeCache[id] = {x:fixed.x, y:fixed.y};
fixed.x = true;
fixed.y = true;
}
}
}
}
/**
* Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
*
@ -619,8 +635,8 @@ class PhysicsEngine {
*/
stabilize(iterations = this.options.stabilization.iterations) {
if (typeof iterations !== 'number') {
console.log('The stabilize method needs a numeric amount of iterations. Switching to default: ', this.options.stabilization.iterations);
iterations = this.options.stabilization.iterations;
console.log('The stabilize method needs a numeric amount of iterations. Switching to default: ', iterations);
}
if (this.physicsBody.physicsNodeIndices.length === 0) {
@ -634,10 +650,7 @@ class PhysicsEngine {
// this sets the width of all nodes initially which could be required for the avoidOverlap
this.body.emitter.emit("_resizeNodes");
// stop the render loop
this.stopSimulation();
// set stabilze to false
this.stopSimulation(); // stop the render loop
this.stabilized = false;
// block redraw requests
@ -654,25 +667,37 @@ class PhysicsEngine {
}
/**
* If not already stabilizing, start it and emit a start event.
*
* @returns {boolean} true if stabilization started with this call
* @private
*/
_startStabilizing() {
if (this.startedStabilization === true) return false;
this.body.emitter.emit('startStabilizing');
this.startedStabilization = true;
return true;
}
/**
* One batch of stabilization
* @private
*/
_stabilizationBatch() {
var self = this;
var running = () => (self.stabilized === false && self.stabilizationIterations < self.targetIterations);
var running = () => (this.stabilized === false && this.stabilizationIterations < this.targetIterations);
var sendProgress = () => {
self.body.emitter.emit('stabilizationProgress', {
iterations: self.stabilizationIterations,
total: self.targetIterations
this.body.emitter.emit('stabilizationProgress', {
iterations: this.stabilizationIterations,
total: this.targetIterations
});
};
// this is here to ensure that there is at least one start event.
if (this.startedStabilization === false) {
this.body.emitter.emit('startStabilizing');
this.startedStabilization = true;
sendProgress();
if (this._startStabilizing()) {
sendProgress(); // Ensure that there is at least one start event.
}
var count = 0;
@ -720,15 +745,22 @@ class PhysicsEngine {
}
//--------------------------- DEBUGGING BELOW ---------------------------//
/**
* TODO: Is this function used at all? If not, remove it!
* Debug function that display arrows for the forces currently active in the network.
*
* Use this when debugging only.
*
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_drawForces(ctx) {
for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) {
let node = this.body.nodes[this.physicsBody.physicsNodeIndices[i]];
let force = this.physicsBody.forces[this.physicsBody.physicsNodeIndices[i]];
let index = this.physicsBody.physicsNodeIndices[i];
let node = this.body.nodes[index];
let force = this.physicsBody.forces[index];
let factor = 20;
let colorFactor = 0.03;
let forceSize = Math.sqrt(Math.pow(force.x,2) + Math.pow(force.x,2));
@ -738,20 +770,25 @@ class PhysicsEngine {
let color = util.HSVToHex((180 - Math.min(1,Math.max(0,colorFactor*forceSize))*180) / 360,1,1);
let point = {
x: node.x + factor*force.x,
y: node.y + factor*force.y
};
ctx.lineWidth = size;
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(node.x,node.y);
ctx.lineTo(node.x+factor*force.x, node.y+factor*force.y);
ctx.lineTo(point.x, point.y);
ctx.stroke();
let angle = Math.atan2(force.y, force.x);
ctx.fillStyle = color;
ctx.arrowEndpoint(node.x + factor*force.x + Math.cos(angle)*arrowSize, node.y + factor*force.y+Math.sin(angle)*arrowSize, angle, arrowSize);
EndPoints.draw(ctx, {type: 'arrow', point: point, angle: angle, length: arrowSize});
ctx.fill();
}
}
}
export default PhysicsEngine;

+ 51
- 37
lib/network/modules/components/physics/BarnesHutSolver.js View File

@ -15,7 +15,7 @@ class BarnesHutSolver {
this.randomSeed = 5;
// debug: show grid
//this.body.emitter.on("afterDrawing", (ctx) => {this._debug(ctx,'#ff0000')})
// this.body.emitter.on("afterDrawing", (ctx) => {this._debug(ctx,'#ff0000')})
}
/**
@ -25,7 +25,9 @@ class BarnesHutSolver {
setOptions(options) {
this.options = options;
this.thetaInversed = 1 / this.options.theta;
this.overlapAvoidanceFactor = 1 - Math.max(0, Math.min(1,this.options.avoidOverlap)); // if 1 then min distance = 0.5, if 0.5 then min distance = 0.5 + 0.5*node.shape.radius
// if 1 then min distance = 0.5, if 0.5 then min distance = 0.5 + 0.5*node.shape.radius
this.overlapAvoidanceFactor = 1 - Math.max(0, Math.min(1, this.options.avoidOverlap));
}
/**
@ -62,16 +64,26 @@ class BarnesHutSolver {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
// starting with root is irrelevant, it never passes the BarnesHutSolver condition
this._getForceContribution(barnesHutTree.root.children.NW, node);
this._getForceContribution(barnesHutTree.root.children.NE, node);
this._getForceContribution(barnesHutTree.root.children.SW, node);
this._getForceContribution(barnesHutTree.root.children.SE, node);
this._getForceContributions(barnesHutTree.root, node);
}
}
}
}
/**
* @param {Object} parentBranch
* @param {Node} node
* @private
*/
_getForceContributions(parentBranch, node) {
this._getForceContribution(parentBranch.children.NW, node);
this._getForceContribution(parentBranch.children.NE, node);
this._getForceContribution(parentBranch.children.SW, node);
this._getForceContribution(parentBranch.children.SE, node);
}
/**
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
* If a region contains a single node, we check if it is not itself, then we apply the force.
@ -99,10 +111,7 @@ class BarnesHutSolver {
else {
// Did not pass the condition, go into children if available
if (parentBranch.childrenCount === 4) {
this._getForceContribution(parentBranch.children.NW, node);
this._getForceContribution(parentBranch.children.NE, node);
this._getForceContribution(parentBranch.children.SW, node);
this._getForceContribution(parentBranch.children.SE, node);
this._getForceContributions(parentBranch, node);
}
else { // parentBranch must have only one node, if it was empty we wouldnt be here
if (parentBranch.children.data.id != node.id) { // if it is not self
@ -164,9 +173,10 @@ class BarnesHutSolver {
// get the range of the nodes
for (let i = 1; i < nodeCount; i++) {
let x = nodes[nodeIndices[i]].x;
let y = nodes[nodeIndices[i]].y;
if (nodes[nodeIndices[i]].options.mass > 0) {
let node = nodes[nodeIndices[i]];
let x = node.x;
let y = node.y;
if (node.options.mass > 0) {
if (x < minX) {
minX = x;
}
@ -238,14 +248,15 @@ class BarnesHutSolver {
* @private
*/
_updateBranchMass(parentBranch, node) {
let centerOfMass = parentBranch.centerOfMass;
let totalMass = parentBranch.mass + node.options.mass;
let totalMassInv = 1 / totalMass;
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
parentBranch.centerOfMass.x *= totalMassInv;
centerOfMass.x = centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
centerOfMass.x *= totalMassInv;
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
parentBranch.centerOfMass.y *= totalMassInv;
centerOfMass.y = centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
centerOfMass.y *= totalMassInv;
parentBranch.mass = totalMass;
let biggestSize = Math.max(Math.max(node.height, node.radius), node.width);
@ -268,22 +279,26 @@ class BarnesHutSolver {
this._updateBranchMass(parentBranch, node);
}
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
if (parentBranch.children.NW.range.maxY > node.y) { // in NW
this._placeInRegion(parentBranch, node, "NW");
let range = parentBranch.children.NW.range;
let region;
if (range.maxX > node.x) { // in NW or SW
if (range.maxY > node.y) {
region = "NW";
}
else { // in SW
this._placeInRegion(parentBranch, node, "SW");
else {
region = "SW";
}
}
else { // in NE or SE
if (parentBranch.children.NW.range.maxY > node.y) { // in NE
this._placeInRegion(parentBranch, node, "NE");
if (range.maxY > node.y) {
region = "NE";
}
else { // in SE
this._placeInRegion(parentBranch, node, "SE");
else {
region = "SE";
}
}
this._placeInRegion(parentBranch, node, region);
}
@ -296,27 +311,28 @@ class BarnesHutSolver {
* @private
*/
_placeInRegion(parentBranch, node, region) {
switch (parentBranch.children[region].childrenCount) {
let children = parentBranch.children[region];
switch (children.childrenCount) {
case 0: // place node here
parentBranch.children[region].children.data = node;
parentBranch.children[region].childrenCount = 1;
this._updateBranchMass(parentBranch.children[region], node);
children.children.data = node;
children.childrenCount = 1;
this._updateBranchMass(children, node);
break;
case 1: // convert into children
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
// we move one node a little bit and we do not put it in the tree.
if (parentBranch.children[region].children.data.x === node.x &&
parentBranch.children[region].children.data.y === node.y) {
if (children.children.data.x === node.x && children.children.data.y === node.y) {
node.x += this.seededRandom();
node.y += this.seededRandom();
}
else {
this._splitBranch(parentBranch.children[region]);
this._placeInTree(parentBranch.children[region], node);
this._splitBranch(children);
this._placeInTree(children, node);
}
break;
case 4: // place in branch
this._placeInTree(parentBranch.children[region], node);
this._placeInTree(children, node);
break;
}
}
@ -405,8 +421,6 @@ class BarnesHutSolver {
}
//--------------------------- DEBUGGING BELOW ---------------------------//

Loading…
Cancel
Save