ruvector-fixed / dist /core /graph-algorithms.js
Archie
Fix dimension/dimensions bug and positional insert/search args
40d7073
"use strict";
/**
* Graph Algorithms - MinCut, Spectral Clustering, Community Detection
*
* Provides graph partitioning and clustering algorithms for:
* - Code module detection
* - Dependency clustering
* - Architecture analysis
* - Refactoring suggestions
*/
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;
/**
* Build adjacency representation from edges
*/
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());
// Undirected graph - add both directions
adjacency.get(from).set(to, weight);
adjacency.get(to).set(from, weight);
}
return { nodes, edges, adjacency };
}
/**
* Minimum Cut (Stoer-Wagner algorithm)
*
* Finds the minimum weight cut that partitions the graph into two parts.
* Useful for finding loosely coupled module boundaries.
*/
function minCut(graph) {
const n = graph.nodes.length;
if (n < 2) {
return { groups: [graph.nodes], cutWeight: 0, modularity: 0 };
}
// Copy adjacency for modification
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(); // Track merged nodes
for (const node of graph.nodes) {
merged.set(node, [node]);
}
let remaining = [...graph.nodes];
// Stoer-Wagner phases
while (remaining.length > 1) {
// Maximum adjacency search
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) {
// Find node with maximum weight to A
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);
// Update weights
for (const [neighbor, w] of adj.get(maxNode) || []) {
if (!inA.has(neighbor)) {
weights.set(neighbor, (weights.get(neighbor) || 0) + w);
}
}
}
// Cut of the phase
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];
}
// Merge last two nodes
if (remaining.length > 1) {
// Merge lastAdded into beforeLast
const mergedNodes = [...(merged.get(beforeLast) || []), ...(merged.get(lastAdded) || [])];
merged.set(beforeLast, mergedNodes);
// Update adjacency
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);
}
}
// Remove lastAdded
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,
};
}
/**
* Spectral Clustering (using power iteration)
*
* Uses graph Laplacian eigenvectors for clustering.
* Good for finding natural clusters in code dependencies.
*/
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() };
}
// Build Laplacian matrix (D - A)
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;
}
// Normalized Laplacian: D^(-1/2) L D^(-1/2)
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]);
}
}
}
// Power iteration to find eigenvectors
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);
// Deflation: orthogonalize against previous eigenvectors
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);
// Power iteration
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];
}
}
// Deflation
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;
}
// Compute eigenvalue
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);
}
// K-means clustering on eigenvector coordinates
const coordinates = new Map();
for (let i = 0; i < n; i++) {
coordinates.set(graph.nodes[i], eigenvectors.map(ev => ev[i]));
}
// Simple k-means
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 };
}
/**
* Louvain Community Detection
*
* Greedy modularity optimization for finding communities.
* Good for detecting natural module boundaries.
*/
function louvainCommunities(graph) {
const communities = new Map();
let communityId = 0;
// Initialize: each node in its own community
for (const node of graph.nodes) {
communities.set(node, communityId++);
}
// Total edge weight
let m = 0;
for (const { weight = 1 } of graph.edges) {
m += weight;
}
m /= 2; // Undirected
if (m === 0)
return communities;
// Node weights (sum of edge weights)
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);
}
// Community weights
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));
}
// Iterate until no improvement
let improved = true;
while (improved) {
improved = false;
for (const node of graph.nodes) {
const currentCommunity = communities.get(node);
const ki = nodeWeight.get(node) || 0;
// Calculate modularity gain for moving to neighbor communities
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;
// Calculate edge weight to target community
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;
}
}
// Move node if beneficial
if (bestCommunity !== currentCommunity) {
communities.set(node, bestCommunity);
// Update community weights
communityWeight.set(currentCommunity, (communityWeight.get(currentCommunity) || 0) - ki);
communityWeight.set(bestCommunity, (communityWeight.get(bestCommunity) || 0) + ki);
improved = true;
}
}
}
// Renumber communities to be contiguous
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;
}
/**
* Calculate modularity of a partition
*/
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);
// Edges within 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; // Count each edge once
modularity += inGroup / m - Math.pow(degreeSum / (2 * m), 2);
}
return modularity;
}
/**
* Find bridges (edges whose removal disconnects components)
*/
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;
}
/**
* Find articulation points (nodes whose removal disconnects components)
*/
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)));
// Root with 2+ children or non-root with low[v] >= disc[u]
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;
}
// Helper functions
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;
// Random initialization
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++) {
// Assign points to nearest centroid
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;
// Update centroids
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,
};