UNPKG

arela

Version:

AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.

231 lines 8.38 kB
/** * Louvain Community Detection Algorithm * Maximizes modularity to find optimal communities in weighted graphs */ /** * Run the Louvain algorithm to detect communities in a graph */ export function louvainClustering(graph) { // Initialize: each node is its own community let communities = initializeCommunities(graph); let currentGraph = graph; let improved = true; let iteration = 0; const maxIterations = 100; while (improved && iteration < maxIterations) { improved = false; iteration++; // Phase 1: Optimize modularity locally const modularityBefore = calculateModularity(communities, currentGraph); for (const node of currentGraph.nodes) { const currentCommunity = findCommunityById(node.id, communities); if (!currentCommunity) continue; const neighbors = getNeighborCommunities(node.id, currentGraph, communities); // Try moving node to each neighbor community for (const neighbor of neighbors) { if (neighbor.id === currentCommunity.id) continue; const deltaQ = modularityGain(node.id, currentCommunity, neighbor, currentGraph, communities); if (deltaQ > 1e-6) { moveNode(node.id, currentCommunity, neighbor); improved = true; } } } const modularityAfter = calculateModularity(communities, currentGraph); // Phase 2: Aggregate communities into super-nodes if (improved && modularityAfter > modularityBefore) { currentGraph = aggregateGraph(currentGraph, communities); communities = initializeCommunities(currentGraph); } else { improved = false; } } return communities; } /** * Initialize communities with each node as its own community */ function initializeCommunities(graph) { return graph.nodes.map((node, index) => ({ nodes: [node.id], id: `${index}`, })); } /** * Find community containing a specific node */ function findCommunityById(nodeId, communities) { return communities.find((c) => c.nodes.includes(nodeId)); } /** * Get all unique communities connected to a node */ function getNeighborCommunities(nodeId, graph, communities) { const neighborCommunities = new Set(); // Find all neighbors of this node const neighbors = new Set(); for (const edge of graph.edges) { if (edge.from === nodeId) { neighbors.add(edge.to); } if (edge.to === nodeId) { neighbors.add(edge.from); } } // Get unique communities of neighbors for (const neighborId of neighbors) { const community = findCommunityById(neighborId, communities); if (community) { neighborCommunities.add(community.id); } } // Add current community const currentCommunity = findCommunityById(nodeId, communities); if (currentCommunity) { neighborCommunities.add(currentCommunity.id); } // Return community objects return Array.from(neighborCommunities) .map((id) => communities.find((c) => c.id === id)) .filter((c) => c !== undefined); } /** * Calculate modularity gain if node moves from one community to another */ function modularityGain(nodeId, fromCommunity, toCommunity, graph, communities) { const m = graph.edges.length; // Total edges if (m === 0) return 0; // Count internal edges within each community const fromInternalBefore = countInternalEdges(fromCommunity, graph); const toInternalBefore = countInternalEdges(toCommunity, graph); // Simulate node move const tempFromNodes = fromCommunity.nodes.filter((n) => n !== nodeId); const tempToNodes = [...toCommunity.nodes, nodeId]; const fromInternalAfter = tempFromNodes.length > 0 ? countInternalEdgesForNodes(tempFromNodes, graph) : 0; const toInternalAfter = countInternalEdgesForNodes(tempToNodes, graph); // Degree of nodes in each community const nodeDegree = graph.nodes.find((n) => n.id === nodeId)?.degree || 0; const fromDegree = tempFromNodes.reduce((sum, id) => sum + (graph.nodes.find((n) => n.id === id)?.degree || 0), 0); const toDegree = tempToNodes.reduce((sum, id) => sum + (graph.nodes.find((n) => n.id === id)?.degree || 0), 0); // Calculate change in modularity const before = (fromInternalBefore / m - Math.pow(fromDegree / (2 * m), 2)) + (toInternalBefore / m - Math.pow(toDegree / (2 * m), 2)); const after = (tempFromNodes.length > 0 ? (fromInternalAfter / m - Math.pow(fromDegree / (2 * m), 2)) : 0) + (toInternalAfter / m - Math.pow((toDegree + nodeDegree) / (2 * m), 2)); return after - before; } /** * Move node from one community to another */ function moveNode(nodeId, fromCommunity, toCommunity) { fromCommunity.nodes = fromCommunity.nodes.filter((n) => n !== nodeId); toCommunity.nodes.push(nodeId); } /** * Count edges within a community */ function countInternalEdges(community, graph) { const nodeSet = new Set(community.nodes); let count = 0; for (const edge of graph.edges) { if (nodeSet.has(edge.from) && nodeSet.has(edge.to)) { count += edge.weight; } } return count; } /** * Count edges within a set of nodes */ function countInternalEdgesForNodes(nodeIds, graph) { const nodeSet = new Set(nodeIds); let count = 0; for (const edge of graph.edges) { if (nodeSet.has(edge.from) && nodeSet.has(edge.to)) { count += edge.weight; } } return count; } /** * Count edges going out of a community */ function countExternalEdges(community, graph) { const nodeSet = new Set(community.nodes); let count = 0; for (const edge of graph.edges) { const fromIn = nodeSet.has(edge.from); const toIn = nodeSet.has(edge.to); if ((fromIn && !toIn) || (!fromIn && toIn)) { count += edge.weight; } } return count; } /** * Calculate total modularity of the graph */ export function calculateModularity(communities, graph) { const m = graph.edges.length; if (m === 0) return 0; let Q = 0; for (const community of communities) { if (community.nodes.length === 0) continue; const eIn = countInternalEdges(community, graph); const eTot = community.nodes.reduce((sum, id) => sum + (graph.nodes.find((n) => n.id === id)?.degree || 0), 0); Q += eIn / m - Math.pow(eTot / (2 * m), 2); } return Q; } /** * Aggregate communities into super-nodes for next iteration */ function aggregateGraph(graph, communities) { // Map from old node IDs to community IDs const nodeToCommId = new Map(); for (const community of communities) { for (const nodeId of community.nodes) { nodeToCommId.set(nodeId, community.id); } } // Create new nodes for each community const newNodes = communities.map((comm, idx) => { const degree = comm.nodes.reduce((sum, id) => sum + (graph.nodes.find((n) => n.id === id)?.degree || 0), 0); return { id: idx, path: `community_${comm.id}`, type: "community", degree, }; }); // Create new edges between communities const edgeMap = new Map(); const communityIndexMap = new Map(); for (let i = 0; i < communities.length; i++) { communityIndexMap.set(communities[i].id, i); } for (const edge of graph.edges) { const fromComm = nodeToCommId.get(edge.from); const toComm = nodeToCommId.get(edge.to); if (fromComm && toComm && fromComm !== toComm) { const key = `${Math.min(communityIndexMap.get(fromComm), communityIndexMap.get(toComm))}_${Math.max(communityIndexMap.get(fromComm), communityIndexMap.get(toComm))}`; edgeMap.set(key, (edgeMap.get(key) || 0) + edge.weight); } } const newEdges = []; for (const [key, weight] of edgeMap) { const [from, to] = key.split("_").map(Number); newEdges.push({ from, to, weight }); } return { nodes: newNodes, edges: newEdges, }; } //# sourceMappingURL=louvain.js.map