import BarnesHutSolver from './components/physics/BarnesHutSolver';
|
|
import Repulsion from './components/physics/RepulsionSolver';
|
|
import HierarchicalRepulsion from './components/physics/HierarchicalRepulsionSolver';
|
|
import SpringSolver from './components/physics/SpringSolver';
|
|
import HierarchicalSpringSolver from './components/physics/HierarchicalSpringSolver';
|
|
import CentralGravitySolver from './components/physics/CentralGravitySolver';
|
|
import ForceAtlas2BasedRepulsionSolver from './components/physics/FA2BasedRepulsionSolver';
|
|
import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver';
|
|
|
|
class PhysicsBase {
|
|
constructor() {
|
|
this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
|
|
this.options = {};
|
|
|
|
this.referenceState = {};
|
|
this.previousStates = {};
|
|
|
|
this.startedStabilization = false;
|
|
this.stabilized = false;
|
|
this.stabilizationIterations = 0;
|
|
this.timestep = 0.5;
|
|
|
|
// parameters for the adaptive timestep
|
|
this.adaptiveTimestep = false;
|
|
this.adaptiveTimestepEnabled = false;
|
|
this.adaptiveCounter = 0;
|
|
this.adaptiveInterval = 3;
|
|
}
|
|
|
|
/**
|
|
* configure the engine.
|
|
*/
|
|
initPhysicsSolvers() {
|
|
var options;
|
|
if (this.options.solver === 'forceAtlas2Based') {
|
|
options = this.options.forceAtlas2Based;
|
|
this.nodesSolver = new ForceAtlas2BasedRepulsionSolver(this.body, this.physicsBody, options);
|
|
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
|
|
this.gravitySolver = new ForceAtlas2BasedCentralGravitySolver(this.body, this.physicsBody, options);
|
|
}
|
|
else if (this.options.solver === 'repulsion') {
|
|
options = this.options.repulsion;
|
|
this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
|
|
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
|
|
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
|
|
}
|
|
else if (this.options.solver === 'hierarchicalRepulsion') {
|
|
options = this.options.hierarchicalRepulsion;
|
|
this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
|
|
this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
|
|
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
|
|
}
|
|
else { // barnesHut
|
|
options = this.options.barnesHut;
|
|
this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options);
|
|
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
|
|
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
|
|
}
|
|
|
|
this.modelOptions = options;
|
|
}
|
|
|
|
/**
|
|
* A single simulation step (or 'tick') in the physics simulation
|
|
*
|
|
* @private
|
|
*/
|
|
physicsTick() {
|
|
// this is here to ensure that there is no start event when the network is already stable.
|
|
if (this.startedStabilization === false) {
|
|
this.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();
|
|
}
|
|
|
|
// increment the counter
|
|
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.calculateForces();
|
|
this.moveNodes();
|
|
}
|
|
|
|
// determine if the network has stabilzied
|
|
if (this.stabilized === true) {
|
|
this.revert();
|
|
}
|
|
|
|
this.stabilizationIterations++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Revert the simulation one step. This is done so after stabilization, every new start of the simulation will also say stabilized.
|
|
*/
|
|
revert() {
|
|
var nodeIds = Object.keys(this.previousStates);
|
|
var nodes = this.body.nodes;
|
|
var velocities = this.physicsBody.velocities;
|
|
this.referenceState = {};
|
|
|
|
for (let i = 0; i < nodeIds.length; i++) {
|
|
let nodeId = nodeIds[i];
|
|
if (nodes[nodeId] !== undefined) {
|
|
if (this.isWorker || nodes[nodeId].options.physics === true) {
|
|
this.referenceState[nodeId] = {
|
|
positions: {x:nodes[nodeId].x, y:nodes[nodeId].y}
|
|
};
|
|
velocities[nodeId].x = this.previousStates[nodeId].vx;
|
|
velocities[nodeId].y = this.previousStates[nodeId].vy;
|
|
nodes[nodeId].x = this.previousStates[nodeId].x;
|
|
nodes[nodeId].y = this.previousStates[nodeId].y;
|
|
}
|
|
}
|
|
else {
|
|
delete this.previousStates[nodeId];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This compares the reference state to the current state
|
|
*/
|
|
_evaluateStepQuality() {
|
|
let dx, dy, dpos;
|
|
let nodes = this.body.nodes;
|
|
let reference = this.referenceState;
|
|
let posThreshold = 0.3;
|
|
|
|
for (let nodeId in this.referenceState) {
|
|
if (this.referenceState.hasOwnProperty(nodeId) && nodes[nodeId] !== undefined) {
|
|
dx = nodes[nodeId].x - reference[nodeId].positions.x;
|
|
dy = nodes[nodeId].y - reference[nodeId].positions.y;
|
|
|
|
dpos = Math.sqrt(Math.pow(dx,2) + Math.pow(dy,2))
|
|
|
|
if (dpos > posThreshold) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* move the nodes one timestap and check if they are stabilized
|
|
* @returns {boolean}
|
|
*/
|
|
moveNodes() {
|
|
var nodeIndices = this.physicsBody.physicsNodeIndices;
|
|
var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9;
|
|
var maxNodeVelocity = 0;
|
|
var averageNodeVelocity = 0;
|
|
|
|
// the velocity threshold (energy in the system) for the adaptivity toggle
|
|
var velocityAdaptiveThreshold = 5;
|
|
|
|
for (let i = 0; i < nodeIndices.length; i++) {
|
|
let nodeId = nodeIndices[i];
|
|
let nodeVelocity = this._performStep(nodeId, maxVelocity);
|
|
// stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
|
|
maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity);
|
|
averageNodeVelocity += nodeVelocity;
|
|
}
|
|
|
|
// evaluating the stabilized and adaptiveTimestepEnabled conditions
|
|
this.adaptiveTimestepEnabled = (averageNodeVelocity/nodeIndices.length) < velocityAdaptiveThreshold;
|
|
this.stabilized = maxNodeVelocity < this.options.minVelocity;
|
|
}
|
|
|
|
// TODO consider moving _performStep in here
|
|
// right now Physics nodes don't have setX setY functions
|
|
// - maybe switch logic of setX and set x?
|
|
// - add functions to physics nodes - seems not desirable
|
|
|
|
/**
|
|
* calculate the forces for one physics iteration.
|
|
*/
|
|
calculateForces() {
|
|
this.gravitySolver.solve();
|
|
this.nodesSolver.solve();
|
|
this.edgesSolver.solve();
|
|
}
|
|
|
|
/**
|
|
* One batch of stabilization
|
|
* @private
|
|
*/
|
|
_stabilizationBatch() {
|
|
// this is here to ensure that there is at least one start event.
|
|
if (this.startedStabilization === false) {
|
|
this.emit('startStabilizing');
|
|
this.startedStabilization = true;
|
|
}
|
|
|
|
var count = 0;
|
|
while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) {
|
|
this.physicsTick();
|
|
count++;
|
|
}
|
|
|
|
if (this.stabilized === false && this.stabilizationIterations < this.targetIterations) {
|
|
this.emit('stabilizationProgress', {iterations: this.stabilizationIterations, total: this.targetIterations});
|
|
setTimeout(this._stabilizationBatch.bind(this),0);
|
|
}
|
|
else {
|
|
this._finalizeStabilization();
|
|
}
|
|
}
|
|
}
|
|
|
|
export default PhysicsBase;
|