cytoscape
Version:
Graph theory (a.k.a. network) library for analysis and visualisation
233 lines (188 loc) • 6.01 kB
JavaScript
// Implemented by Zoe Xi @zoexi for GSOC 2016
// https://github.com/cytoscape/cytoscape.js-markov-cluster
// Implemented from Stijn van Dongen's (author of MCL algorithm) documentation: http://micans.org/mcl/
// and lecture notes: https://www.cs.ucsb.edu/~xyan/classes/CS595D-2009winter/MCL_Presentation2.pdf
import * as util from '../../util/index.mjs';
/* eslint-disable no-unused-vars */
let defaults = util.defaults({
expandFactor: 2, // affects time of computation and cluster granularity to some extent: M * M
inflateFactor: 2, // affects cluster granularity (the greater the value, the more clusters): M(i,j) / E(j)
multFactor: 1, // optional self loops for each node. Use a neutral value to improve cluster computations.
maxIterations: 20, // maximum number of iterations of the MCL algorithm in a single run
attributes: [ // attributes/features used to group nodes, ie. similarity values between nodes
function(edge) {
return 1;
}
]
});
/* eslint-enable */
let setOptions = ( options ) => defaults( options );
/* eslint-disable no-unused-vars, no-console */
if( process.env.NODE_ENV !== 'production' ){
var printMatrix = function( M ) { // used for debugging purposes only
let n = Math.sqrt(M.length);
for ( let i = 0; i < n; i++ ) {
let row = '';
for ( let j = 0; j < n; j++ ) {
row += Number(M[i*n+j]).toFixed(3) + ' ';
}
console.log(row);
}
console.log('');
};
}
/* eslint-enable */
let getSimilarity = function( edge, attributes ) {
let total = 0;
for ( let i = 0; i < attributes.length; i++ ) {
total += attributes[i]( edge );
}
return total;
};
let addLoops = function( M, n, val ) {
for (let i = 0; i < n; i++) {
M[i * n + i] = val;
}
};
let normalize = function( M, n ) {
let sum;
for ( let col = 0; col < n; col++ ) {
sum = 0;
for ( let row = 0; row < n; row++ ) {
sum += M[row * n + col];
}
for ( let row = 0; row < n; row++ ) {
M[row * n + col] = M[row * n + col] / sum;
}
}
};
// TODO: blocked matrix multiplication?
let mmult = function( A, B, n ) {
let C = new Array( n * n );
for ( let i = 0; i < n; i++ ) {
for ( let j = 0; j < n; j++ ) {
C[i * n + j] = 0;
}
for ( let k = 0; k < n; k++ ) {
for ( let j = 0; j < n; j++ ) {
C[i * n + j] += A[i * n + k] * B[k * n + j];
}
}
}
return C;
};
let expand = function( M, n, expandFactor /** power **/ ) {
let _M = M.slice(0);
for ( let p = 1; p < expandFactor; p++ ) {
M = mmult( M, _M, n );
}
return M;
};
let inflate = function( M, n, inflateFactor /** r **/ ) {
let _M = new Array( n * n );
// M(i,j) ^ inflatePower
for ( let i = 0; i < n * n; i++ ) {
_M[i] = Math.pow( M[i], inflateFactor );
}
normalize( _M, n );
return _M;
};
let hasConverged = function( M, _M, n2, roundFactor ) {
// Check that both matrices have the same elements (i,j)
for ( let i = 0; i < n2; i++ ) {
let v1 = Math.round( M[i] * Math.pow(10, roundFactor) ) / Math.pow(10, roundFactor); // truncate to 'roundFactor' decimal places
let v2 = Math.round( _M[i] * Math.pow(10, roundFactor) ) / Math.pow(10, roundFactor);
if ( v1 !== v2 ) {
return false;
}
}
return true;
};
let assign = function( M, n, nodes, cy ) {
let clusters = [];
for ( let i = 0; i < n; i++ ) {
let cluster = [];
for ( let j = 0; j < n; j++ ) {
// Row-wise attractors and elements that they attract belong in same cluster
if ( Math.round( M[i * n + j] * 1000 ) / 1000 > 0 ) {
cluster.push( nodes[j] );
}
}
if ( cluster.length !== 0 ) {
clusters.push( cy.collection(cluster) );
}
}
return clusters;
};
let isDuplicate = function( c1, c2 ) {
for (let i = 0; i < c1.length; i++) {
if (!c2[i] || c1[i].id() !== c2[i].id()) {
return false;
}
}
return true;
};
let removeDuplicates = function( clusters ) {
for (let i = 0; i < clusters.length; i++) {
for (let j = 0; j < clusters.length; j++) {
if (i != j && isDuplicate(clusters[i], clusters[j])) {
clusters.splice(j, 1);
}
}
}
return clusters;
};
let markovClustering = function( options ) {
let nodes = this.nodes();
let edges = this.edges();
let cy = this.cy();
// Set parameters of algorithm:
let opts = setOptions( options );
// Map each node to its position in node array
let id2position = {};
for( let i = 0; i < nodes.length; i++ ){
id2position[ nodes[i].id() ] = i;
}
// Generate stochastic matrix M from input graph G (should be symmetric/undirected)
let n = nodes.length, n2 = n * n;
let M = new Array( n2 ), _M;
for ( let i = 0; i < n2; i++ ) {
M[i] = 0;
}
for ( let e = 0; e < edges.length; e++ ) {
let edge = edges[e];
let i = id2position[ edge.source().id() ];
let j = id2position[ edge.target().id() ];
let sim = getSimilarity( edge, opts.attributes );
M[i * n + j] += sim; // G should be symmetric and undirected
M[j * n + i] += sim;
}
// Begin Markov cluster algorithm
// Step 1: Add self loops to each node, ie. add multFactor to matrix diagonal
addLoops( M, n, opts.multFactor );
// Step 2: M = normalize( M );
normalize( M, n );
let isStillMoving = true;
let iterations = 0;
while ( isStillMoving && iterations < opts.maxIterations ) {
isStillMoving = false;
// Step 3:
_M = expand( M, n, opts.expandFactor );
// Step 4:
M = inflate( _M, n, opts.inflateFactor );
// Step 5: check to see if ~steady state has been reached
if ( ! hasConverged( M, _M, n2, 4 ) ) {
isStillMoving = true;
}
iterations++;
}
// Build clusters from matrix
let clusters = assign( M, n, nodes, cy );
// Remove duplicate clusters due to symmetry of graph and M matrix
clusters = removeDuplicates( clusters );
return clusters;
};
export default {
markovClustering,
mcl: markovClustering
};