Browse Source

Reduce the time-complexity of the network initial positioning. Very substantial performance gain for large graphs.

network_performance_pullrequest
Ludo Stellingwerff 7 years ago
parent
commit
5c76796354
3 changed files with 132 additions and 41 deletions
  1. +93
    -25
      lib/network/modules/KamadaKawai.js
  2. +21
    -7
      lib/network/modules/LayoutEngine.js
  3. +18
    -9
      lib/network/modules/components/algorithms/FloydWarshall.js

+ 93
- 25
lib/network/modules/KamadaKawai.js View File

@ -49,11 +49,14 @@ class KamadaKawai {
// get the K Matrix // get the K Matrix
this._createK_matrix(D_matrix); this._createK_matrix(D_matrix);
// initial E Matrix
this._createE_matrix();
// calculate positions // calculate positions
let threshold = 0.01; let threshold = 0.01;
let innerThreshold = 1; let innerThreshold = 1;
let iterations = 0; let iterations = 0;
let maxIterations = Math.max(1000,Math.min(10*this.body.nodeIndices.length,6000));
let maxIterations = Math.max(1000, Math.min(10 * this.body.nodeIndices.length, 6000));
let maxInnerIterations = 5; let maxInnerIterations = 5;
let maxEnergy = 1e9; let maxEnergy = 1e9;
@ -64,10 +67,10 @@ class KamadaKawai {
[highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(ignoreClusters); [highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(ignoreClusters);
delta_m = maxEnergy; delta_m = maxEnergy;
subIterations = 0; subIterations = 0;
while(delta_m > innerThreshold && subIterations < maxInnerIterations) {
while (delta_m > innerThreshold && subIterations < maxInnerIterations) {
subIterations += 1; subIterations += 1;
this._moveNode(highE_nodeId, dE_dx, dE_dy); this._moveNode(highE_nodeId, dE_dx, dE_dy);
[delta_m,dE_dx,dE_dy] = this._getEnergy(highE_nodeId);
[delta_m, dE_dx, dE_dy] = this._getEnergy(highE_nodeId);
} }
} }
} }
@ -87,7 +90,7 @@ class KamadaKawai {
for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) { for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) {
let m = nodesArray[nodeIdx]; let m = nodesArray[nodeIdx];
// by not evaluating nodes with predefined positions we should only move nodes that have no positions. // by not evaluating nodes with predefined positions we should only move nodes that have no positions.
if ((nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true) || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) {
if ((nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true) || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) {
let [delta_m,dE_dx,dE_dy] = this._getEnergy(m); let [delta_m,dE_dx,dE_dy] = this._getEnergy(m);
if (maxEnergy < delta_m) { if (maxEnergy < delta_m) {
maxEnergy = delta_m; maxEnergy = delta_m;
@ -108,24 +111,7 @@ class KamadaKawai {
* @private * @private
*/ */
_getEnergy(m) { _getEnergy(m) {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
let x_m = nodes[m].x;
let y_m = nodes[m].y;
let dE_dx = 0;
let dE_dy = 0;
for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
let i = nodesArray[iIdx];
if (i !== m) {
let x_i = nodes[i].x;
let y_i = nodes[i].y;
let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
dE_dx += this.K_matrix[m][i] * ((x_m - x_i) - this.L_matrix[m][i] * (x_m - x_i) * denominator);
dE_dy += this.K_matrix[m][i] * ((y_m - y_i) - this.L_matrix[m][i] * (y_m - y_i) * denominator);
}
}
let [dE_dx,dE_dy] = this.E_sums[m];
let delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2)); let delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2));
return [delta_m, dE_dx, dE_dy]; return [delta_m, dE_dx, dE_dy];
} }
@ -147,15 +133,20 @@ class KamadaKawai {
let x_m = nodes[m].x; let x_m = nodes[m].x;
let y_m = nodes[m].y; let y_m = nodes[m].y;
let km = this.K_matrix[m];
let lm = this.L_matrix[m];
for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) { for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
let i = nodesArray[iIdx]; let i = nodesArray[iIdx];
if (i !== m) { if (i !== m) {
let x_i = nodes[i].x; let x_i = nodes[i].x;
let y_i = nodes[i].y; let y_i = nodes[i].y;
let kmat = km[i];
let lmat = lm[i];
let denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5); let denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5);
d2E_dx2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(y_m - y_i, 2) * denominator);
d2E_dxdy += this.K_matrix[m][i] * (this.L_matrix[m][i] * (x_m - x_i) * (y_m - y_i) * denominator);
d2E_dy2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(x_m - x_i, 2) * denominator);
d2E_dx2 += kmat * (1 - lmat * Math.pow(y_m - y_i, 2) * denominator);
d2E_dxdy += kmat * (lmat * (x_m - x_i) * (y_m - y_i) * denominator);
d2E_dy2 += kmat * (1 - lmat * Math.pow(x_m - x_i, 2) * denominator);
} }
} }
// make the variable names easier to make the solving of the linear system easier to read // make the variable names easier to make the solving of the linear system easier to read
@ -168,6 +159,9 @@ class KamadaKawai {
// move the node // move the node
nodes[m].x += dx; nodes[m].x += dx;
nodes[m].y += dy; nodes[m].y += dy;
// Recalculate E_matrix (should be incremental)
this._updateE_matrix(m);
} }
@ -208,8 +202,82 @@ class KamadaKawai {
} }
} }
/**
* Create matrix with all energies between nodes
* @private
*/
_createE_matrix() {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
this.E_matrix = {};
this.E_sums = {};
for (let mIdx = 0; mIdx < nodesArray.length; mIdx++) {
this.E_matrix[nodesArray[mIdx]] = [];
}
for (let mIdx = 0; mIdx < nodesArray.length; mIdx++) {
let m = nodesArray[mIdx];
let x_m = nodes[m].x;
let y_m = nodes[m].y;
let dE_dx = 0;
let dE_dy = 0;
for (let iIdx = mIdx; iIdx < nodesArray.length; iIdx++) {
let i = nodesArray[iIdx];
if (i !== m) {
let x_i = nodes[i].x;
let y_i = nodes[i].y;
let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
this.E_matrix[m][iIdx] = [
this.K_matrix[m][i] * ((x_m - x_i) - this.L_matrix[m][i] * (x_m - x_i) * denominator),
this.K_matrix[m][i] * ((y_m - y_i) - this.L_matrix[m][i] * (y_m - y_i) * denominator)
];
this.E_matrix[i][mIdx] = this.E_matrix[m][iIdx];
dE_dx += this.E_matrix[m][iIdx][0];
dE_dy += this.E_matrix[m][iIdx][1];
}
}
//Store sum
this.E_sums[m] = [dE_dx, dE_dy];
}
}
//Update method, just doing single column (rows are auto-updated) (update all sums)
_updateE_matrix(m) {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
let colm = this.E_matrix[m];
let kcolm = this.K_matrix[m];
let lcolm = this.L_matrix[m];
let x_m = nodes[m].x;
let y_m = nodes[m].y;
let dE_dx = 0;
let dE_dy = 0;
for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
let i = nodesArray[iIdx];
if (i !== m) {
//Keep old energy value for sum modification below
let cell = colm[iIdx];
let oldDx = cell[0];
let oldDy = cell[1];
//Calc new energy:
let x_i = nodes[i].x;
let y_i = nodes[i].y;
let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
let dx = kcolm[i] * ((x_m - x_i) - lcolm[i] * (x_m - x_i) * denominator);
let dy = kcolm[i] * ((y_m - y_i) - lcolm[i] * (y_m - y_i) * denominator);
colm[iIdx] = [dx, dy];
dE_dx += dx;
dE_dy += dy;
//add new energy to sum of each column
let sum = this.E_sums[i];
sum[0] += (dx-oldDx);
sum[1] += (dy-oldDy);
}
}
//Store sum at -1 index
this.E_sums[m] = [dE_dx, dE_dy];
}
} }
export default KamadaKawai; export default KamadaKawai;

+ 21
- 7
lib/network/modules/LayoutEngine.js View File

@ -161,9 +161,9 @@ class LayoutEngine {
positionInitially(nodesArray) { positionInitially(nodesArray) {
if (this.options.hierarchical.enabled !== true) { if (this.options.hierarchical.enabled !== true) {
this.randomSeed = this.initialRandomSeed; this.randomSeed = this.initialRandomSeed;
let radius = nodesArray.length + 50;
for (let i = 0; i < nodesArray.length; i++) { for (let i = 0; i < nodesArray.length; i++) {
let node = nodesArray[i]; let node = nodesArray[i];
let radius = 10 * 0.1 * nodesArray.length + 10;
let angle = 2 * Math.PI * this.seededRandom(); let angle = 2 * Math.PI * this.seededRandom();
if (node.x === undefined) { if (node.x === undefined) {
node.x = radius * Math.cos(angle); node.x = radius * Math.cos(angle);
@ -196,34 +196,48 @@ class LayoutEngine {
if (positionDefined < 0.5 * this.body.nodeIndices.length) { if (positionDefined < 0.5 * this.body.nodeIndices.length) {
let MAX_LEVELS = 10; let MAX_LEVELS = 10;
let level = 0; let level = 0;
let clusterThreshold = 100;
let clusterThreshold = 150;
//Performance enhancement, cluster edges need only be simpel straight lines
let clusterOptions = {
clusterEdgeProperties:{
smooth: {
enabled: false
}
}
};
// if there are a lot of nodes, we cluster before we run the algorithm. // if there are a lot of nodes, we cluster before we run the algorithm.
if (this.body.nodeIndices.length > clusterThreshold) { if (this.body.nodeIndices.length > clusterThreshold) {
let startLength = this.body.nodeIndices.length; let startLength = this.body.nodeIndices.length;
while (this.body.nodeIndices.length > clusterThreshold) {
while (this.body.nodeIndices.length > clusterThreshold && level <= MAX_LEVELS) {
//console.time("clustering") //console.time("clustering")
level += 1; level += 1;
let before = this.body.nodeIndices.length; let before = this.body.nodeIndices.length;
// if there are many nodes we do a hubsize cluster // if there are many nodes we do a hubsize cluster
if (level % 3 === 0) { if (level % 3 === 0) {
this.body.modules.clustering.clusterBridges();
console.log("bridges");
this.body.modules.clustering.clusterBridges(clusterOptions);
} }
else { else {
this.body.modules.clustering.clusterOutliers();
console.log("outliers");
this.body.modules.clustering.clusterOutliers(clusterOptions);
} }
let after = this.body.nodeIndices.length; let after = this.body.nodeIndices.length;
if ((before == after && level % 3 !== 0) || level > MAX_LEVELS) {
if (before == after && level % 3 !== 0) {
this._declusterAll(); this._declusterAll();
this.body.emitter.emit("_layoutFailed"); this.body.emitter.emit("_layoutFailed");
console.info("This network could not be positioned by this version of the improved layout algorithm. Please disable improvedLayout for better performance."); console.info("This network could not be positioned by this version of the improved layout algorithm. Please disable improvedLayout for better performance.");
return; return;
} }
//console.timeEnd("clustering") //console.timeEnd("clustering")
//console.log(level,after)
//console.log(before,level,after);
} }
// increase the size of the edges // increase the size of the edges
this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150, 2 * startLength)}) this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150, 2 * startLength)})
} }
if (level > MAX_LEVELS){
console.info("The clustering didn't succeed within the amount of interations allowed, progressing with partial result.");
}
// position the system for these nodes and edges // position the system for these nodes and edges
this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true); this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true);

+ 18
- 9
lib/network/modules/components/algorithms/FloydWarshall.js View File

@ -4,7 +4,8 @@
class FloydWarshall { class FloydWarshall {
constructor(){}
constructor() {
}
getDistances(body, nodesArray, edgesArray) { getDistances(body, nodesArray, edgesArray) {
let D_matrix = {}; let D_matrix = {};
@ -12,11 +13,11 @@ class FloydWarshall {
// prepare matrix with large numbers // prepare matrix with large numbers
for (let i = 0; i < nodesArray.length; i++) { for (let i = 0; i < nodesArray.length; i++) {
D_matrix[nodesArray[i]] = {};
D_matrix[nodesArray[i]] = {};
let node = nodesArray[i];
let cell = {};
D_matrix[node] = cell;
for (let j = 0; j < nodesArray.length; j++) { for (let j = 0; j < nodesArray.length; j++) {
D_matrix[nodesArray[i]][nodesArray[j]] = (i == j ? 0 : 1e9);
D_matrix[nodesArray[i]][nodesArray[j]] = (i == j ? 0 : 1e9);
cell[nodesArray[j]] = (i == j ? 0 : 1e9);
} }
} }
@ -34,10 +35,18 @@ class FloydWarshall {
// Adapted FloydWarshall based on unidirectionality to greatly reduce complexity. // Adapted FloydWarshall based on unidirectionality to greatly reduce complexity.
for (let k = 0; k < nodeCount; k++) { for (let k = 0; k < nodeCount; k++) {
for (let i = 0; i < nodeCount-1; i++) {
for (let j = i+1; j < nodeCount; j++) {
D_matrix[nodesArray[i]][nodesArray[j]] = Math.min(D_matrix[nodesArray[i]][nodesArray[j]],D_matrix[nodesArray[i]][nodesArray[k]] + D_matrix[nodesArray[k]][nodesArray[j]])
D_matrix[nodesArray[j]][nodesArray[i]] = D_matrix[nodesArray[i]][nodesArray[j]];
let knode = nodesArray[k];
let kcolm = D_matrix[knode];
for (let i = 0; i < nodeCount - 1; i++) {
let inode = nodesArray[i];
let icolm = D_matrix[inode];
for (let j = i + 1; j < nodeCount; j++) {
let jnode = nodesArray[j];
let jcolm = D_matrix[jnode];
let val = Math.min(icolm[jnode], icolm[knode] + kcolm[jnode]);
icolm[jnode] = val;
jcolm[inode] = val;
} }
} }
} }

Loading…
Cancel
Save