UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

343 lines 10.5 kB
/** * Perform Markov Clustering on a graph */ export function markovClustering(graph, options = {}) { const { expansion = 2, inflation = 2, maxIterations = 100, tolerance = 1e-6, pruningThreshold = 1e-5, selfLoops = true, } = options; const nodes = Array.from(graph.nodes()); const nodeIds = nodes.map((node) => node.id); const n = nodeIds.length; if (n === 0) { return { communities: [], attractors: new Set(), iterations: 0, converged: true, }; } // Build initial transition matrix let matrix = buildTransitionMatrix(graph, nodeIds, selfLoops); let converged = false; let iteration = 0; for (iteration = 0; iteration < maxIterations; iteration++) { const oldMatrix = matrix.map((row) => [...row]); // Expansion step (matrix multiplication) matrix = matrixPower(matrix, expansion); // Inflation step (element-wise powering and column normalization) matrix = inflate(matrix, inflation); // Pruning step (remove small values) matrix = prune(matrix, pruningThreshold); // Check for convergence if (hasConverged(oldMatrix, matrix, tolerance)) { converged = true; break; } } // Extract clusters from final matrix const { communities, attractors } = extractClusters(matrix, nodeIds); return { communities, attractors, iterations: iteration + 1, converged, }; } /** * Build initial transition matrix from graph */ function buildTransitionMatrix(graph, nodeIds, selfLoops) { const n = nodeIds.length; const matrix = Array.from({ length: n }, () => Array(n).fill(0)); const nodeToIndex = new Map(); nodeIds.forEach((id, index) => nodeToIndex.set(id, index)); // Fill adjacency values for (let i = 0; i < n; i++) { const nodeId = nodeIds[i]; if (!nodeId) { continue; } const neighbors = graph.neighbors(nodeId); for (const neighbor of neighbors) { const j = nodeToIndex.get(neighbor); if (j !== undefined) { const edge = graph.getEdge(nodeId, neighbor); const weight = edge?.weight ?? 1; const row = matrix[i]; if (!row) { continue; } row[j] = weight; } } // Add self-loops if (selfLoops) { const row = matrix[i]; if (!row) { continue; } row[i] = 1; } } // Column-normalize the matrix for (let j = 0; j < n; j++) { let colSum = 0; for (let i = 0; i < n; i++) { const val = matrix[i]?.[j]; if (val !== undefined) { colSum += val; } } if (colSum > 0) { for (let i = 0; i < n; i++) { const row = matrix[i]; if (!row) { continue; } const val = row[j]; if (val !== undefined) { row[j] = val / colSum; } } } } return matrix; } /** * Raise matrix to a power (for expansion step) */ function matrixPower(matrix, power) { if (power === 1) { return matrix; } if (power === 2) { return matrixMultiply(matrix, matrix); } let result = matrix; for (let i = 1; i < power; i++) { result = matrixMultiply(result, matrix); } return result; } /** * Multiply two matrices */ function matrixMultiply(a, b) { const n = a.length; const m = b[0]?.length ?? 0; const p = b.length; const result = Array.from({ length: n }, () => Array(m).fill(0)); for (let i = 0; i < n; i++) { for (let j = 0; j < m; j++) { for (let k = 0; k < p; k++) { const aVal = a[i]?.[k] ?? 0; const bVal = b[k]?.[j] ?? 0; const resultRow = result[i]; if (!resultRow) { continue; } const prevVal = resultRow[j]; if (prevVal !== undefined) { resultRow[j] = prevVal + (aVal * bVal); } } } } return result; } /** * Inflation step: element-wise powering and column normalization */ function inflate(matrix, inflation) { const n = matrix.length; const result = Array.from({ length: n }, () => Array(n).fill(0)); // Element-wise powering for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { const val = matrix[i]?.[j]; if (val !== undefined) { const resultRow = result[i]; if (resultRow) { resultRow[j] = Math.pow(val, inflation); } } } } // Column normalization for (let j = 0; j < n; j++) { let colSum = 0; for (let i = 0; i < n; i++) { const val = result[i]?.[j]; if (val !== undefined) { colSum += val; } } if (colSum > 0) { for (let i = 0; i < n; i++) { const row = result[i]; if (!row) { continue; } const val = row[j]; if (val !== undefined) { row[j] = val / colSum; } } } } return result; } /** * Pruning step: remove small values */ function prune(matrix, threshold) { const n = matrix.length; const result = Array.from({ length: n }, () => Array(n).fill(0)); for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { const matrixRow = matrix[i]; if (!matrixRow) { continue; } const matrixVal = matrixRow[j]; if (matrixVal !== undefined && matrixVal >= threshold) { const resultRow = result[i]; if (resultRow) { resultRow[j] = matrixVal; } } } } // Re-normalize columns after pruning for (let j = 0; j < n; j++) { let colSum = 0; for (let i = 0; i < n; i++) { const resultRow = result[i]; if (!resultRow) { continue; } const val = resultRow[j]; if (val !== undefined) { colSum += val; } } if (colSum > 0) { for (let i = 0; i < n; i++) { const resultRow = result[i]; if (!resultRow) { continue; } const val = resultRow[j]; if (val !== undefined) { resultRow[j] = val / colSum; } } } } return result; } /** * Check if the algorithm has converged */ function hasConverged(oldMatrix, newMatrix, tolerance) { const n = oldMatrix.length; for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { const oldRow = oldMatrix[i]; const newRow = newMatrix[i]; if (!oldRow || !newRow) { continue; } const oldVal = oldRow[j]; const newVal = newRow[j]; if (oldVal !== undefined && newVal !== undefined && Math.abs(oldVal - newVal) > tolerance) { return false; } } } return true; } /** * Extract clusters from the final matrix */ function extractClusters(matrix, nodeIds) { const n = matrix.length; const attractors = new Set(); const communities = []; const nodeToCluster = new Map(); // Find attractors (columns with non-zero diagonal elements) for (let i = 0; i < n; i++) { const matrixRow = matrix[i]; if (!matrixRow) { continue; } const diagonalVal = matrixRow[i]; const nodeId = nodeIds[i]; if (diagonalVal !== undefined && diagonalVal > 0 && nodeId !== undefined) { attractors.add(nodeId); } } // Assign nodes to clusters based on columns let clusterIndex = 0; for (let j = 0; j < n; j++) { // Find nodes that belong to this cluster (column) const clusterNodes = []; for (let i = 0; i < n; i++) { const matrixRow = matrix[i]; if (!matrixRow) { continue; } const val = matrixRow[j]; if (val !== undefined && val > 0 && !nodeToCluster.has(i)) { clusterNodes.push(i); nodeToCluster.set(i, clusterIndex); } } if (clusterNodes.length > 0) { communities.push(clusterNodes.map((idx) => { const nodeId = nodeIds[idx]; return nodeId; }).filter((node) => node !== undefined)); clusterIndex++; } } // Handle isolated nodes for (let i = 0; i < n; i++) { if (!nodeToCluster.has(i)) { const nodeId = nodeIds[i]; if (nodeId !== undefined) { communities.push([nodeId]); } nodeToCluster.set(i, clusterIndex++); } } return { communities, attractors }; } /** * Calculate modularity of MCL clustering result */ export function calculateMCLModularity(graph, communities) { const m = graph.totalEdgeCount; if (m === 0) { return 0; } let modularity = 0; const communityMap = new Map(); // Build community map communities.forEach((community, index) => { community.forEach((nodeId) => { communityMap.set(nodeId, index); }); }); // Calculate modularity for (const edge of graph.edges()) { const sourceCommunity = communityMap.get(edge.source); const targetCommunity = communityMap.get(edge.target); if (sourceCommunity !== undefined && targetCommunity !== undefined && sourceCommunity === targetCommunity) { modularity += 1; } const sourceDegree = graph.degree(edge.source); const targetDegree = graph.degree(edge.target); modularity -= (sourceDegree * targetDegree) / (2 * m); } return modularity / (2 * m); } //# sourceMappingURL=mcl.js.map