UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

219 lines (183 loc) 6.78 kB
import {Graph} from "../../core/graph.js"; import type {CommunityResult, LouvainOptions, NodeId} from "../../types/index.js"; import {louvainOptimized} from "./louvain-optimized.js"; import { calculateModularity, getNeighborCommunities, getNodeDegree, getTotalEdgeWeight, } from "./modularity-utils.js"; /** * Louvain community detection algorithm * * Implements the Louvain method for community detection in graphs. * Uses modularity optimization to find community structure. * * References: * - Blondel, V. D., Guillaume, J. L., Lambiotte, R., & Lefebvre, E. (2008). * Fast unfolding of communities in large networks. * Journal of statistical mechanics: theory and experiment, 2008(10), P10008. * * @param graph - The input graph * @param options - Algorithm options * @returns Community detection result with communities, modularity, and iterations */ export function louvain( graph: Graph, options: LouvainOptions = {}, ): CommunityResult { const resolution = options.resolution ?? 1.0; const maxIterations = options.maxIterations ?? 100; const tolerance = options.tolerance ?? 1e-6; // Use optimized version by default for larger graphs // Only disable if explicitly requested or graph is very small const shouldUseOptimized = options.useOptimized !== false && graph.nodeCount > 50; if (shouldUseOptimized) { return louvainOptimized(graph, { resolution, maxIterations, tolerance, // Enable all optimizations by default pruneLeaves: true, importanceOrdering: true, thresholdCycling: true, pruningThreshold: 0.01, }); } // Initialize: each node in its own community const communities = initializeCommunities(graph); let modularity = calculateModularity(graph, communities, resolution); let iteration = 0; let improved = true; while (iteration < maxIterations && improved) { // Phase 1: Local optimization improved = louvainPhase1(graph, communities, resolution); if (improved) { const newModularity = calculateModularity(graph, communities, resolution); // Check for convergence if ((newModularity - modularity) < tolerance) { break; } modularity = newModularity; iteration++; } } return { communities: extractCommunities(communities), modularity: calculateModularity(graph, communities, resolution), iterations: iteration, }; } /** * Initialize communities: each node in its own community */ function initializeCommunities(graph: Graph): Map<NodeId, number> { const communities = new Map<NodeId, number>(); let communityId = 0; for (const node of graph.nodes()) { communities.set(node.id, communityId++); } return communities; } /** * Phase 1 of Louvain algorithm: Local optimization * For each node, try moving to neighboring communities and keep the move * that provides the best modularity gain. */ function louvainPhase1( graph: Graph, communities: Map<NodeId, number>, resolution: number, ): boolean { let globalImprovement = false; let localImprovement = true; while (localImprovement) { localImprovement = false; for (const node of graph.nodes()) { const nodeId = node.id; const currentCommunity = communities.get(nodeId); if (currentCommunity === undefined) { continue; } // Calculate current modularity contribution const currentModularity = nodeModularityContribution( graph, nodeId, currentCommunity, communities, resolution, ); let bestCommunity = currentCommunity; let bestModularity = currentModularity; // Try moving to neighboring communities const neighborCommunities = getNeighborCommunities(graph, nodeId, communities); for (const neighborCommunity of neighborCommunities) { if (neighborCommunity === currentCommunity) { continue; } const newModularity = nodeModularityContribution( graph, nodeId, neighborCommunity, communities, resolution, ); if (newModularity > bestModularity) { bestModularity = newModularity; bestCommunity = neighborCommunity; } } // Move node if improvement found if (bestCommunity !== currentCommunity) { communities.set(nodeId, bestCommunity); localImprovement = true; globalImprovement = true; } } } return globalImprovement; } /** * Calculate modularity contribution of a node to a specific community */ function nodeModularityContribution( graph: Graph, nodeId: NodeId, community: number, communities: Map<NodeId, number>, resolution: number, ): number { const totalEdgeWeight = getTotalEdgeWeight(graph); if (totalEdgeWeight === 0) { return 0; } let internalLinks = 0; let nodeDegree = 0; let communityDegree = 0; // Calculate node's connections to the community for (const neighbor of graph.neighbors(nodeId)) { const edge = graph.getEdge(nodeId, neighbor); const weight = edge?.weight ?? 1; nodeDegree += weight; if (communities.get(neighbor) === community) { internalLinks += weight; } } // Calculate total degree of the community (excluding the node if it's currently in it) for (const [otherNodeId, otherCommunity] of communities) { if (otherCommunity === community && otherNodeId !== nodeId) { communityDegree += getNodeDegree(graph, otherNodeId); } } // Modularity formula: Q = (1/2m) * Σ[A_ij - (k_i * k_j)/(2m)] * δ(c_i, c_j) const modularityIncrease = (internalLinks - ((resolution * nodeDegree * communityDegree) / (2 * totalEdgeWeight))) / totalEdgeWeight; return modularityIncrease; } /** * Extract final community structure */ function extractCommunities(communities: Map<NodeId, number>): NodeId[][] { const communityMap = new Map<number, NodeId[]>(); for (const [nodeId, community] of communities) { if (!communityMap.has(community)) { communityMap.set(community, []); } const communityNodes = communityMap.get(community); if (communityNodes) { communityNodes.push(nodeId); } } return Array.from(communityMap.values()); }