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;