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.

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