/** * Graphology Leiden Algorithm * ============================ * * JavaScript implementation of the Leiden community detection * algorithm for graphology. * * Vendored from: https://github.com/graphology/graphology/tree/master/src/communities-leiden * License: MIT * * [Reference] * Traag, V. A., et al. "From Louvain to Leiden: Guaranteeing Well-Connected * Communities". Scientific Reports, vol. 9, no 1, 2019, p. 5233. * https://arxiv.org/abs/1810.08473 */ var resolveDefaults = require('graphology-utils/defaults'); var isGraph = require('graphology-utils/is-graph'); var inferType = require('graphology-utils/infer-type'); var SparseMap = require('mnemonist/sparse-map'); var SparseQueueSet = require('mnemonist/sparse-queue-set'); var createRandomIndex = require('pandemonium/random-index').createRandomIndex; var utils = require('./utils.cjs'); var indices = require('graphology-indices/louvain'); var addWeightToCommunity = utils.addWeightToCommunity; var UndirectedLouvainIndex = indices.UndirectedLouvainIndex; var UndirectedLeidenAddenda = utils.UndirectedLeidenAddenda; var DEFAULTS = { attributes: { community: 'community', weight: 'weight' }, randomness: 0.01, randomWalk: true, resolution: 1, rng: Math.random, weighted: false }; var EPSILON = 1e-10; function tieBreaker( bestCommunity, currentCommunity, targetCommunity, delta, bestDelta ) { if (Math.abs(delta - bestDelta) < EPSILON) { if (bestCommunity === currentCommunity) { return false; } else { return targetCommunity > bestCommunity; } } else if (delta > bestDelta) { return true; } return false; } function undirectedLeiden(detailed, graph, options) { var index = new UndirectedLouvainIndex(graph, { attributes: { weight: options.attributes.weight }, keepDendrogram: detailed, resolution: options.resolution, weighted: options.weighted }); var addenda = new UndirectedLeidenAddenda(index, { randomness: options.randomness, rng: options.rng }); var randomIndex = createRandomIndex(options.rng); // Communities var currentCommunity, targetCommunity; var communities = new SparseMap(Float64Array, index.C); // Traversal var queue = new SparseQueueSet(index.C), start, end, weight, ci, ri, s, i, j, l; // Metrics var degree, targetCommunityDegree; // Moves var bestCommunity, bestDelta, deltaIsBetter, delta; // Details var deltaComputations = 0, nodesVisited = 0, moves = [], currentMoves; while (true) { l = index.C; currentMoves = 0; // Traversal of the graph ri = options.randomWalk ? randomIndex(l) : 0; for (s = 0; s < l; s++, ri++) { i = ri % l; queue.enqueue(i); } while (queue.size !== 0) { i = queue.dequeue(); nodesVisited++; degree = 0; communities.clear(); currentCommunity = index.belongings[i]; start = index.starts[i]; end = index.starts[i + 1]; // Traversing neighbors for (; start < end; start++) { j = index.neighborhood[start]; weight = index.weights[start]; targetCommunity = index.belongings[j]; // Incrementing metrics degree += weight; addWeightToCommunity(communities, targetCommunity, weight); } // Finding best community to move to bestDelta = index.fastDeltaWithOwnCommunity( i, degree, communities.get(currentCommunity) || 0, currentCommunity ); bestCommunity = currentCommunity; for (ci = 0; ci < communities.size; ci++) { targetCommunity = communities.dense[ci]; if (targetCommunity === currentCommunity) continue; targetCommunityDegree = communities.vals[ci]; deltaComputations++; delta = index.fastDelta( i, degree, targetCommunityDegree, targetCommunity ); deltaIsBetter = tieBreaker( bestCommunity, currentCommunity, targetCommunity, delta, bestDelta ); if (deltaIsBetter) { bestDelta = delta; bestCommunity = targetCommunity; } } if (bestDelta < 0) { bestCommunity = index.isolate(i, degree); if (bestCommunity === currentCommunity) continue; } else { if (bestCommunity === currentCommunity) { continue; } else { index.move(i, degree, bestCommunity); } } currentMoves++; // Adding neighbors from other communities to the queue start = index.starts[i]; end = index.starts[i + 1]; for (; start < end; start++) { j = index.neighborhood[start]; targetCommunity = index.belongings[j]; if (targetCommunity !== bestCommunity) queue.enqueue(j); } } moves.push(currentMoves); if (currentMoves === 0) { index.zoomOut(); break; } if (!addenda.onlySingletons()) { // We continue working on the induced graph addenda.zoomOut(); continue; } break; } var results = { index: index, deltaComputations: deltaComputations, nodesVisited: nodesVisited, moves: moves }; return results; } /** * Function returning the communities mapping of the graph. * * @param {boolean} assign - Assign communities to nodes attributes? * @param {boolean} detailed - Whether to return detailed information. * @param {Graph} graph - Target graph. * @param {object} options - Options: * @param {object} attributes - Attribute names: * @param {string} community - Community node attribute name. * @param {string} weight - Weight edge attribute name. * @param {number} randomness - Randomness parameter. * @param {boolean} randomWalk - Whether to traverse the graph in random order. * @param {number} resolution - Resolution parameter. * @param {function} rng - RNG function to use. * @param {boolean} weighted - Whether to compute the weighted version. * @return {object} */ function leiden(assign, detailed, graph, options) { if (!isGraph(graph)) throw new Error( 'graphology-communities-leiden: the given graph is not a valid graphology instance.' ); var type = inferType(graph); if (type === 'mixed') throw new Error( 'graphology-communities-leiden: cannot run the algorithm on a true mixed graph.' ); if (type === 'directed') throw new Error( 'graphology-communities-leiden: not yet implemented for directed graphs.' ); // Attributes name options = resolveDefaults(options, DEFAULTS); // Empty graph case var c = 0; if (graph.size === 0) { if (assign) { graph.forEachNode(function (node) { graph.setNodeAttribute(node, options.attributes.communities, c++); }); return; } var communities = {}; graph.forEachNode(function (node) { communities[node] = c++; }); if (!detailed) return communities; return { communities: communities, count: graph.order, deltaComputations: 0, dendrogram: null, level: 0, modularity: NaN, moves: null, nodesVisited: 0, resolution: options.resolution }; } var fn = undirectedLeiden; var results = fn(detailed, graph, options); var index = results.index; // Standard output if (!detailed) { if (assign) { index.assign(options.attributes.community); return; } return index.collect(); } // Detailed output var output = { count: index.C, deltaComputations: results.deltaComputations, dendrogram: index.dendrogram, level: index.level, modularity: index.modularity(), moves: results.moves, nodesVisited: results.nodesVisited, resolution: options.resolution }; if (assign) { index.assign(options.attributes.community); return output; } output.communities = index.collect(); return output; } /** * Exporting. */ var fn = leiden.bind(null, false, false); fn.assign = leiden.bind(null, true, false); fn.detailed = leiden.bind(null, false, true); fn.defaults = DEFAULTS; module.exports = fn;