vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

260 lines
9.6 KiB

  1. import BarnesHutSolver from './components/physics/BarnesHutSolver';
  2. import Repulsion from './components/physics/RepulsionSolver';
  3. import HierarchicalRepulsion from './components/physics/HierarchicalRepulsionSolver';
  4. import SpringSolver from './components/physics/SpringSolver';
  5. import HierarchicalSpringSolver from './components/physics/HierarchicalSpringSolver';
  6. import CentralGravitySolver from './components/physics/CentralGravitySolver';
  7. import ForceAtlas2BasedRepulsionSolver from './components/physics/FA2BasedRepulsionSolver';
  8. import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver';
  9. class PhysicsBase {
  10. constructor() {
  11. this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
  12. this.options = {};
  13. this.referenceState = {};
  14. this.previousStates = {};
  15. this.startedStabilization = false;
  16. this.stabilized = false;
  17. this.stabilizationIterations = 0;
  18. this.timestep = 0.5;
  19. // parameters for the adaptive timestep
  20. this.adaptiveTimestep = false;
  21. this.adaptiveTimestepEnabled = false;
  22. this.adaptiveCounter = 0;
  23. this.adaptiveInterval = 3;
  24. }
  25. /**
  26. * configure the engine.
  27. */
  28. initPhysicsSolvers() {
  29. var options;
  30. if (this.options.solver === 'forceAtlas2Based') {
  31. options = this.options.forceAtlas2Based;
  32. this.nodesSolver = new ForceAtlas2BasedRepulsionSolver(this.body, this.physicsBody, options);
  33. this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
  34. this.gravitySolver = new ForceAtlas2BasedCentralGravitySolver(this.body, this.physicsBody, options);
  35. }
  36. else if (this.options.solver === 'repulsion') {
  37. options = this.options.repulsion;
  38. this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
  39. this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
  40. this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
  41. }
  42. else if (this.options.solver === 'hierarchicalRepulsion') {
  43. options = this.options.hierarchicalRepulsion;
  44. this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
  45. this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
  46. this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
  47. }
  48. else { // barnesHut
  49. options = this.options.barnesHut;
  50. this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options);
  51. this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
  52. this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
  53. }
  54. this.modelOptions = options;
  55. }
  56. /**
  57. * A single simulation step (or 'tick') in the physics simulation
  58. *
  59. * @private
  60. */
  61. physicsTick() {
  62. // this is here to ensure that there is no start event when the network is already stable.
  63. if (this.startedStabilization === false) {
  64. this.emit('startStabilizing');
  65. this.startedStabilization = true;
  66. }
  67. if (this.stabilized === false) {
  68. // adaptivity means the timestep adapts to the situation, only applicable for stabilization
  69. if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) {
  70. // this is the factor for increasing the timestep on success.
  71. let factor = 1.2;
  72. // we assume the adaptive interval is
  73. if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations.
  74. // first the big step and revert. Revert saves the reference state.
  75. this.timestep = 2 * this.timestep;
  76. this.calculateForces();
  77. this.moveNodes();
  78. this.revert();
  79. // now the normal step. Since this is the last step, it is the more stable one and we will take this.
  80. this.timestep = 0.5 * this.timestep;
  81. // since it's half the step, we do it twice.
  82. this.calculateForces();
  83. this.moveNodes();
  84. this.calculateForces();
  85. this.moveNodes();
  86. // we compare the two steps. if it is acceptable we double the step.
  87. if (this._evaluateStepQuality() === true) {
  88. this.timestep = factor * this.timestep;
  89. }
  90. else {
  91. // if not, we decrease the step to a minimum of the options timestep.
  92. // if the decreased timestep is smaller than the options step, we do not reset the counter
  93. // we assume that the options timestep is stable enough.
  94. if (this.timestep/factor < this.options.timestep) {
  95. this.timestep = this.options.timestep;
  96. }
  97. else {
  98. // if the timestep was larger than 2 times the option one we check the adaptivity again to ensure
  99. // that large instabilities do not form.
  100. this.adaptiveCounter = -1; // check again next iteration
  101. this.timestep = Math.max(this.options.timestep, this.timestep/factor);
  102. }
  103. }
  104. }
  105. else {
  106. // normal step, keeping timestep constant
  107. this.calculateForces();
  108. this.moveNodes();
  109. }
  110. // increment the counter
  111. this.adaptiveCounter += 1;
  112. }
  113. else {
  114. // case for the static timestep, we reset it to the one in options and take a normal step.
  115. this.timestep = this.options.timestep;
  116. this.calculateForces();
  117. this.moveNodes();
  118. }
  119. // determine if the network has stabilzied
  120. if (this.stabilized === true) {
  121. this.revert();
  122. }
  123. this.stabilizationIterations++;
  124. }
  125. }
  126. /**
  127. * Revert the simulation one step. This is done so after stabilization, every new start of the simulation will also say stabilized.
  128. */
  129. revert() {
  130. var nodeIds = Object.keys(this.previousStates);
  131. var nodes = this.body.nodes;
  132. var velocities = this.physicsBody.velocities;
  133. this.referenceState = {};
  134. for (let i = 0; i < nodeIds.length; i++) {
  135. let nodeId = nodeIds[i];
  136. if (nodes[nodeId] !== undefined) {
  137. if (this.isWorker || nodes[nodeId].options.physics === true) {
  138. this.referenceState[nodeId] = {
  139. positions: {x:nodes[nodeId].x, y:nodes[nodeId].y}
  140. };
  141. velocities[nodeId].x = this.previousStates[nodeId].vx;
  142. velocities[nodeId].y = this.previousStates[nodeId].vy;
  143. nodes[nodeId].x = this.previousStates[nodeId].x;
  144. nodes[nodeId].y = this.previousStates[nodeId].y;
  145. }
  146. }
  147. else {
  148. delete this.previousStates[nodeId];
  149. }
  150. }
  151. }
  152. /**
  153. * This compares the reference state to the current state
  154. */
  155. _evaluateStepQuality() {
  156. let dx, dy, dpos;
  157. let nodes = this.body.nodes;
  158. let reference = this.referenceState;
  159. let posThreshold = 0.3;
  160. for (let nodeId in this.referenceState) {
  161. if (this.referenceState.hasOwnProperty(nodeId) && nodes[nodeId] !== undefined) {
  162. dx = nodes[nodeId].x - reference[nodeId].positions.x;
  163. dy = nodes[nodeId].y - reference[nodeId].positions.y;
  164. dpos = Math.sqrt(Math.pow(dx,2) + Math.pow(dy,2))
  165. if (dpos > posThreshold) {
  166. return false;
  167. }
  168. }
  169. }
  170. return true;
  171. }
  172. /**
  173. * move the nodes one timestap and check if they are stabilized
  174. * @returns {boolean}
  175. */
  176. moveNodes() {
  177. var nodeIndices = this.physicsBody.physicsNodeIndices;
  178. var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9;
  179. var maxNodeVelocity = 0;
  180. var averageNodeVelocity = 0;
  181. // the velocity threshold (energy in the system) for the adaptivity toggle
  182. var velocityAdaptiveThreshold = 5;
  183. for (let i = 0; i < nodeIndices.length; i++) {
  184. let nodeId = nodeIndices[i];
  185. let nodeVelocity = this._performStep(nodeId, maxVelocity);
  186. // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
  187. maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity);
  188. averageNodeVelocity += nodeVelocity;
  189. }
  190. // evaluating the stabilized and adaptiveTimestepEnabled conditions
  191. this.adaptiveTimestepEnabled = (averageNodeVelocity/nodeIndices.length) < velocityAdaptiveThreshold;
  192. this.stabilized = maxNodeVelocity < this.options.minVelocity;
  193. }
  194. // TODO consider moving _performStep in here
  195. // right now Physics nodes don't have setX setY functions
  196. // - maybe switch logic of setX and set x?
  197. // - add functions to physics nodes - seems not desirable
  198. /**
  199. * calculate the forces for one physics iteration.
  200. */
  201. calculateForces() {
  202. this.gravitySolver.solve();
  203. this.nodesSolver.solve();
  204. this.edgesSolver.solve();
  205. }
  206. /**
  207. * One batch of stabilization
  208. * @private
  209. */
  210. _stabilizationBatch() {
  211. // this is here to ensure that there is at least one start event.
  212. if (this.startedStabilization === false) {
  213. this.emit('startStabilizing');
  214. this.startedStabilization = true;
  215. }
  216. var count = 0;
  217. while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) {
  218. this.physicsTick();
  219. count++;
  220. }
  221. if (this.stabilized === false && this.stabilizationIterations < this.targetIterations) {
  222. this.emit('stabilizationProgress', {iterations: this.stabilizationIterations, total: this.targetIterations});
  223. setTimeout(this._stabilizationBatch.bind(this),0);
  224. }
  225. else {
  226. this._finalizeStabilization();
  227. }
  228. }
  229. }
  230. export default PhysicsBase;