| "use strict"; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.buildGraph = buildGraph; |
| exports.minCut = minCut; |
| exports.spectralClustering = spectralClustering; |
| exports.louvainCommunities = louvainCommunities; |
| exports.calculateModularity = calculateModularity; |
| exports.findBridges = findBridges; |
| exports.findArticulationPoints = findArticulationPoints; |
| |
| |
| |
| function buildGraph(nodes, edges) { |
| const adjacency = new Map(); |
| for (const node of nodes) { |
| adjacency.set(node, new Map()); |
| } |
| for (const { from, to, weight = 1 } of edges) { |
| if (!adjacency.has(from)) |
| adjacency.set(from, new Map()); |
| if (!adjacency.has(to)) |
| adjacency.set(to, new Map()); |
| |
| adjacency.get(from).set(to, weight); |
| adjacency.get(to).set(from, weight); |
| } |
| return { nodes, edges, adjacency }; |
| } |
| |
| |
| |
| |
| |
| |
| function minCut(graph) { |
| const n = graph.nodes.length; |
| if (n < 2) { |
| return { groups: [graph.nodes], cutWeight: 0, modularity: 0 }; |
| } |
| |
| const adj = new Map(); |
| for (const [node, neighbors] of graph.adjacency) { |
| adj.set(node, new Map(neighbors)); |
| } |
| let minCutWeight = Infinity; |
| let bestPartition = []; |
| const merged = new Map(); |
| for (const node of graph.nodes) { |
| merged.set(node, [node]); |
| } |
| let remaining = [...graph.nodes]; |
| |
| while (remaining.length > 1) { |
| |
| const inA = new Set([remaining[0]]); |
| const weights = new Map(); |
| for (const node of remaining) { |
| if (!inA.has(node)) { |
| weights.set(node, adj.get(remaining[0])?.get(node) || 0); |
| } |
| } |
| let lastAdded = remaining[0]; |
| let beforeLast = remaining[0]; |
| while (inA.size < remaining.length) { |
| |
| let maxWeight = -Infinity; |
| let maxNode = ''; |
| for (const [node, weight] of weights) { |
| if (!inA.has(node) && weight > maxWeight) { |
| maxWeight = weight; |
| maxNode = node; |
| } |
| } |
| if (!maxNode) |
| break; |
| beforeLast = lastAdded; |
| lastAdded = maxNode; |
| inA.add(maxNode); |
| |
| for (const [neighbor, w] of adj.get(maxNode) || []) { |
| if (!inA.has(neighbor)) { |
| weights.set(neighbor, (weights.get(neighbor) || 0) + w); |
| } |
| } |
| } |
| |
| const cutWeight = weights.get(lastAdded) || 0; |
| if (cutWeight < minCutWeight) { |
| minCutWeight = cutWeight; |
| const lastGroup = merged.get(lastAdded) || [lastAdded]; |
| const otherNodes = remaining.filter(n => n !== lastAdded).flatMap(n => merged.get(n) || [n]); |
| bestPartition = [lastGroup, otherNodes]; |
| } |
| |
| if (remaining.length > 1) { |
| |
| const mergedNodes = [...(merged.get(beforeLast) || []), ...(merged.get(lastAdded) || [])]; |
| merged.set(beforeLast, mergedNodes); |
| |
| for (const [neighbor, w] of adj.get(lastAdded) || []) { |
| if (neighbor !== beforeLast) { |
| const current = adj.get(beforeLast)?.get(neighbor) || 0; |
| adj.get(beforeLast)?.set(neighbor, current + w); |
| adj.get(neighbor)?.set(beforeLast, current + w); |
| } |
| } |
| |
| remaining = remaining.filter(n => n !== lastAdded); |
| adj.delete(lastAdded); |
| for (const [, neighbors] of adj) { |
| neighbors.delete(lastAdded); |
| } |
| } |
| } |
| const modularity = calculateModularity(graph, bestPartition); |
| return { |
| groups: bestPartition.filter(g => g.length > 0), |
| cutWeight: minCutWeight, |
| modularity, |
| }; |
| } |
| |
| |
| |
| |
| |
| |
| function spectralClustering(graph, k = 2) { |
| const n = graph.nodes.length; |
| const nodeIndex = new Map(graph.nodes.map((node, i) => [node, i])); |
| const clusters = new Map(); |
| if (n === 0) { |
| return { clusters, eigenvalues: [], coordinates: new Map() }; |
| } |
| |
| const degree = new Float64Array(n); |
| const laplacian = Array(n).fill(null).map(() => Array(n).fill(0)); |
| for (const [node, neighbors] of graph.adjacency) { |
| const i = nodeIndex.get(node); |
| let d = 0; |
| for (const [neighbor, weight] of neighbors) { |
| const j = nodeIndex.get(neighbor); |
| laplacian[i][j] = -weight; |
| d += weight; |
| } |
| degree[i] = d; |
| laplacian[i][i] = d; |
| } |
| |
| for (let i = 0; i < n; i++) { |
| for (let j = 0; j < n; j++) { |
| if (degree[i] > 0 && degree[j] > 0) { |
| laplacian[i][j] /= Math.sqrt(degree[i] * degree[j]); |
| } |
| } |
| } |
| |
| const eigenvectors = []; |
| const eigenvalues = []; |
| for (let ev = 0; ev < Math.min(k, n); ev++) { |
| let vector = new Float64Array(n); |
| for (let i = 0; i < n; i++) { |
| vector[i] = Math.random(); |
| } |
| normalize(vector); |
| |
| for (const prev of eigenvectors) { |
| const dot = dotProduct(vector, new Float64Array(prev)); |
| for (let i = 0; i < n; i++) { |
| vector[i] -= dot * prev[i]; |
| } |
| } |
| normalize(vector); |
| |
| for (let iter = 0; iter < 100; iter++) { |
| const newVector = new Float64Array(n); |
| for (let i = 0; i < n; i++) { |
| for (let j = 0; j < n; j++) { |
| newVector[i] += laplacian[i][j] * vector[j]; |
| } |
| } |
| |
| for (const prev of eigenvectors) { |
| const dot = dotProduct(newVector, new Float64Array(prev)); |
| for (let i = 0; i < n; i++) { |
| newVector[i] -= dot * prev[i]; |
| } |
| } |
| normalize(newVector); |
| vector = newVector; |
| } |
| |
| let eigenvalue = 0; |
| for (let i = 0; i < n; i++) { |
| let sum = 0; |
| for (let j = 0; j < n; j++) { |
| sum += laplacian[i][j] * vector[j]; |
| } |
| eigenvalue += vector[i] * sum; |
| } |
| eigenvectors.push(Array.from(vector)); |
| eigenvalues.push(eigenvalue); |
| } |
| |
| const coordinates = new Map(); |
| for (let i = 0; i < n; i++) { |
| coordinates.set(graph.nodes[i], eigenvectors.map(ev => ev[i])); |
| } |
| |
| const clusterAssignment = kMeans(graph.nodes.map(node => coordinates.get(node)), k); |
| for (let i = 0; i < n; i++) { |
| clusters.set(graph.nodes[i], clusterAssignment[i]); |
| } |
| return { clusters, eigenvalues, coordinates }; |
| } |
| |
| |
| |
| |
| |
| |
| function louvainCommunities(graph) { |
| const communities = new Map(); |
| let communityId = 0; |
| |
| for (const node of graph.nodes) { |
| communities.set(node, communityId++); |
| } |
| |
| let m = 0; |
| for (const { weight = 1 } of graph.edges) { |
| m += weight; |
| } |
| m /= 2; |
| if (m === 0) |
| return communities; |
| |
| const nodeWeight = new Map(); |
| for (const node of graph.nodes) { |
| let w = 0; |
| for (const [, weight] of graph.adjacency.get(node) || []) { |
| w += weight; |
| } |
| nodeWeight.set(node, w); |
| } |
| |
| const communityWeight = new Map(); |
| for (const node of graph.nodes) { |
| const c = communities.get(node); |
| communityWeight.set(c, (communityWeight.get(c) || 0) + (nodeWeight.get(node) || 0)); |
| } |
| |
| let improved = true; |
| while (improved) { |
| improved = false; |
| for (const node of graph.nodes) { |
| const currentCommunity = communities.get(node); |
| const ki = nodeWeight.get(node) || 0; |
| |
| let bestCommunity = currentCommunity; |
| let bestGain = 0; |
| const neighborCommunities = new Set(); |
| for (const [neighbor] of graph.adjacency.get(node) || []) { |
| neighborCommunities.add(communities.get(neighbor)); |
| } |
| for (const targetCommunity of neighborCommunities) { |
| if (targetCommunity === currentCommunity) |
| continue; |
| |
| let ki_in = 0; |
| for (const [neighbor, weight] of graph.adjacency.get(node) || []) { |
| if (communities.get(neighbor) === targetCommunity) { |
| ki_in += weight; |
| } |
| } |
| const sumTot = communityWeight.get(targetCommunity) || 0; |
| const gain = ki_in / m - (ki * sumTot) / (2 * m * m); |
| if (gain > bestGain) { |
| bestGain = gain; |
| bestCommunity = targetCommunity; |
| } |
| } |
| |
| if (bestCommunity !== currentCommunity) { |
| communities.set(node, bestCommunity); |
| |
| communityWeight.set(currentCommunity, (communityWeight.get(currentCommunity) || 0) - ki); |
| communityWeight.set(bestCommunity, (communityWeight.get(bestCommunity) || 0) + ki); |
| improved = true; |
| } |
| } |
| } |
| |
| const renumber = new Map(); |
| let newId = 0; |
| for (const [node, c] of communities) { |
| if (!renumber.has(c)) { |
| renumber.set(c, newId++); |
| } |
| communities.set(node, renumber.get(c)); |
| } |
| return communities; |
| } |
| |
| |
| |
| function calculateModularity(graph, partition) { |
| let m = 0; |
| for (const { weight = 1 } of graph.edges) { |
| m += weight; |
| } |
| m /= 2; |
| if (m === 0) |
| return 0; |
| let modularity = 0; |
| for (const group of partition) { |
| const groupSet = new Set(group); |
| |
| let inGroup = 0; |
| let degreeSum = 0; |
| for (const node of group) { |
| for (const [neighbor, weight] of graph.adjacency.get(node) || []) { |
| if (groupSet.has(neighbor)) { |
| inGroup += weight; |
| } |
| degreeSum += weight; |
| } |
| } |
| inGroup /= 2; |
| modularity += inGroup / m - Math.pow(degreeSum / (2 * m), 2); |
| } |
| return modularity; |
| } |
| |
| |
| |
| function findBridges(graph) { |
| const bridges = []; |
| const visited = new Set(); |
| const discovery = new Map(); |
| const low = new Map(); |
| const parent = new Map(); |
| let time = 0; |
| function dfs(node) { |
| visited.add(node); |
| discovery.set(node, time); |
| low.set(node, time); |
| time++; |
| for (const [neighbor] of graph.adjacency.get(node) || []) { |
| if (!visited.has(neighbor)) { |
| parent.set(neighbor, node); |
| dfs(neighbor); |
| low.set(node, Math.min(low.get(node), low.get(neighbor))); |
| if (low.get(neighbor) > discovery.get(node)) { |
| bridges.push({ from: node, to: neighbor }); |
| } |
| } |
| else if (neighbor !== parent.get(node)) { |
| low.set(node, Math.min(low.get(node), discovery.get(neighbor))); |
| } |
| } |
| } |
| for (const node of graph.nodes) { |
| if (!visited.has(node)) { |
| parent.set(node, null); |
| dfs(node); |
| } |
| } |
| return bridges; |
| } |
| |
| |
| |
| function findArticulationPoints(graph) { |
| const points = []; |
| const visited = new Set(); |
| const discovery = new Map(); |
| const low = new Map(); |
| const parent = new Map(); |
| let time = 0; |
| function dfs(node) { |
| visited.add(node); |
| discovery.set(node, time); |
| low.set(node, time); |
| time++; |
| let children = 0; |
| for (const [neighbor] of graph.adjacency.get(node) || []) { |
| if (!visited.has(neighbor)) { |
| children++; |
| parent.set(neighbor, node); |
| dfs(neighbor); |
| low.set(node, Math.min(low.get(node), low.get(neighbor))); |
| |
| if ((parent.get(node) === null && children > 1) || |
| (parent.get(node) !== null && low.get(neighbor) >= discovery.get(node))) { |
| if (!points.includes(node)) { |
| points.push(node); |
| } |
| } |
| } |
| else if (neighbor !== parent.get(node)) { |
| low.set(node, Math.min(low.get(node), discovery.get(neighbor))); |
| } |
| } |
| } |
| for (const node of graph.nodes) { |
| if (!visited.has(node)) { |
| parent.set(node, null); |
| dfs(node); |
| } |
| } |
| return points; |
| } |
| |
| function normalize(v) { |
| let sum = 0; |
| for (let i = 0; i < v.length; i++) { |
| sum += v[i] * v[i]; |
| } |
| const norm = Math.sqrt(sum); |
| if (norm > 0) { |
| for (let i = 0; i < v.length; i++) { |
| v[i] /= norm; |
| } |
| } |
| } |
| function dotProduct(a, b) { |
| let sum = 0; |
| for (let i = 0; i < a.length; i++) { |
| sum += a[i] * b[i]; |
| } |
| return sum; |
| } |
| function kMeans(points, k, maxIter = 100) { |
| const n = points.length; |
| if (n === 0 || k === 0) |
| return []; |
| const dim = points[0].length; |
| |
| const centroids = []; |
| const used = new Set(); |
| while (centroids.length < Math.min(k, n)) { |
| const idx = Math.floor(Math.random() * n); |
| if (!used.has(idx)) { |
| used.add(idx); |
| centroids.push([...points[idx]]); |
| } |
| } |
| const assignment = new Array(n).fill(0); |
| for (let iter = 0; iter < maxIter; iter++) { |
| |
| let changed = false; |
| for (let i = 0; i < n; i++) { |
| let minDist = Infinity; |
| let minC = 0; |
| for (let c = 0; c < centroids.length; c++) { |
| let dist = 0; |
| for (let d = 0; d < dim; d++) { |
| dist += Math.pow(points[i][d] - centroids[c][d], 2); |
| } |
| if (dist < minDist) { |
| minDist = dist; |
| minC = c; |
| } |
| } |
| if (assignment[i] !== minC) { |
| assignment[i] = minC; |
| changed = true; |
| } |
| } |
| if (!changed) |
| break; |
| |
| const counts = new Array(k).fill(0); |
| for (let c = 0; c < centroids.length; c++) { |
| for (let d = 0; d < dim; d++) { |
| centroids[c][d] = 0; |
| } |
| } |
| for (let i = 0; i < n; i++) { |
| const c = assignment[i]; |
| counts[c]++; |
| for (let d = 0; d < dim; d++) { |
| centroids[c][d] += points[i][d]; |
| } |
| } |
| for (let c = 0; c < centroids.length; c++) { |
| if (counts[c] > 0) { |
| for (let d = 0; d < dim; d++) { |
| centroids[c][d] /= counts[c]; |
| } |
| } |
| } |
| } |
| return assignment; |
| } |
| exports.default = { |
| buildGraph, |
| minCut, |
| spectralClustering, |
| louvainCommunities, |
| calculateModularity, |
| findBridges, |
| findArticulationPoints, |
| }; |
|
|