UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

297 lines (247 loc) 8.43 kB
import type {Graph} from "../../core/graph.js"; import type {NodeId} from "../../types/index.js"; /** * Closeness centrality implementation * * Measures how close a node is to all other nodes in the graph. * Uses BFS to compute shortest path distances efficiently. */ /** * Closeness centrality options */ export interface ClosenessCentralityOptions { /** * Whether to normalize the centrality values (default: false) */ normalized?: boolean; /** * Use harmonic mean instead of reciprocal of sum (default: false) * Better for disconnected graphs */ harmonic?: boolean; /** * Consider only nodes within this distance (default: undefined = all nodes) */ cutoff?: number; } /** * Calculate closeness centrality for all nodes */ export function closenessCentrality( graph: Graph, options: ClosenessCentralityOptions = {}, ): Record<string, number> { const nodes = Array.from(graph.nodes()).map((node) => node.id); const centrality: Record<string, number> = {}; for (const sourceNode of nodes) { centrality[String(sourceNode)] = nodeClosenessCentrality(graph, sourceNode, options); } return centrality; } /** * Calculate closeness centrality for a specific node */ export function nodeClosenessCentrality( graph: Graph, node: NodeId, options: ClosenessCentralityOptions = {}, ): number { if (!graph.hasNode(node)) { throw new Error(`Node ${String(node)} not found in graph`); } const distances = singleSourceShortestPathLengths(graph, node, options.cutoff); if (distances.size <= 1) { return 0; // No other nodes reachable } let centrality = 0; if (options.harmonic) { // Harmonic centrality: sum of reciprocals of distances for (const [targetNode, distance] of Array.from(distances)) { if (targetNode !== node && distance > 0) { centrality += 1 / distance; } } } else { // Standard closeness: reciprocal of sum of distances let totalDistance = 0; let reachableNodes = 0; for (const [targetNode, distance] of Array.from(distances)) { if (targetNode !== node) { totalDistance += distance; reachableNodes++; } } if (totalDistance > 0) { centrality = 1 / totalDistance; // Wasserman and Faust normalization for disconnected graphs if (options.normalized) { const n = Array.from(graph.nodes()).length; centrality = centrality * reachableNodes / (n - 1); } } } // Normalization for harmonic centrality if (options.harmonic && options.normalized) { const n = Array.from(graph.nodes()).length; if (n > 1) { centrality = centrality / (n - 1); } } return centrality; } /** * Single-source shortest path lengths using BFS (for unweighted graphs) */ function singleSourceShortestPathLengths( graph: Graph, source: NodeId, cutoff?: number, ): Map<NodeId, number> { const distances = new Map<NodeId, number>(); const visited = new Set<NodeId>(); const queue: {node: NodeId, distance: number}[] = []; // Initialize queue.push({node: source, distance: 0}); visited.add(source); distances.set(source, 0); while (queue.length > 0) { const current = queue.shift(); if (!current) { break; } // Skip if beyond cutoff if (cutoff !== undefined && current.distance >= cutoff) { continue; } // Explore neighbors for (const neighbor of Array.from(graph.neighbors(current.node))) { if (!visited.has(neighbor)) { const newDistance = current.distance + 1; // Skip if beyond cutoff if (cutoff !== undefined && newDistance > cutoff) { continue; } visited.add(neighbor); distances.set(neighbor, newDistance); queue.push({node: neighbor, distance: newDistance}); } } } return distances; } /** * Calculate weighted closeness centrality using Dijkstra's algorithm */ export function weightedClosenessCentrality( graph: Graph, options: ClosenessCentralityOptions = {}, ): Record<string, number> { const nodes = Array.from(graph.nodes()).map((node) => node.id); const centrality: Record<string, number> = {}; for (const sourceNode of nodes) { centrality[String(sourceNode)] = nodeWeightedClosenessCentrality(graph, sourceNode, options); } return centrality; } /** * Calculate weighted closeness centrality for a specific node using Dijkstra */ export function nodeWeightedClosenessCentrality( graph: Graph, node: NodeId, options: ClosenessCentralityOptions = {}, ): number { if (!graph.hasNode(node)) { throw new Error(`Node ${String(node)} not found in graph`); } const distances = dijkstraDistances(graph, node, options.cutoff); if (distances.size <= 1) { return 0; // No other nodes reachable } let centrality = 0; if (options.harmonic) { // Harmonic centrality: sum of reciprocals of distances for (const [targetNode, distance] of Array.from(distances)) { if (targetNode !== node && distance > 0 && distance < Infinity) { centrality += 1 / distance; } } } else { // Standard closeness: reciprocal of sum of distances let totalDistance = 0; let reachableNodes = 0; for (const [targetNode, distance] of Array.from(distances)) { if (targetNode !== node && distance < Infinity) { totalDistance += distance; reachableNodes++; } } if (totalDistance > 0) { centrality = 1 / totalDistance; // Wasserman and Faust normalization for disconnected graphs if (options.normalized) { const n = Array.from(graph.nodes()).length; centrality = centrality * reachableNodes / (n - 1); } } } // Normalization for harmonic centrality if (options.harmonic && options.normalized) { const n = Array.from(graph.nodes()).length; if (n > 1) { centrality = centrality / (n - 1); } } return centrality; } /** * Single-source shortest path distances using Dijkstra's algorithm */ function dijkstraDistances( graph: Graph, source: NodeId, cutoff?: number, ): Map<NodeId, number> { const distances = new Map<NodeId, number>(); const visited = new Set<NodeId>(); const pq: {node: NodeId, distance: number}[] = []; // Initialize distances for (const node of Array.from(graph.nodes())) { distances.set(node.id, node.id === source ? 0 : Infinity); } pq.push({node: source, distance: 0}); while (pq.length > 0) { // Simple priority queue (could be optimized with a proper heap) pq.sort((a, b) => a.distance - b.distance); const current = pq.shift(); if (!current || visited.has(current.node)) { continue; } visited.add(current.node); // Skip if beyond cutoff if (cutoff !== undefined && current.distance > cutoff) { continue; } // Explore neighbors for (const neighbor of Array.from(graph.neighbors(current.node))) { if (visited.has(neighbor)) { continue; } const edge = graph.getEdge(current.node, neighbor); if (!edge) { continue; } const edgeWeight = edge.weight ?? 1; const tentativeDistance = current.distance + edgeWeight; const currentDistance = distances.get(neighbor) ?? Infinity; if (tentativeDistance < currentDistance) { distances.set(neighbor, tentativeDistance); // Skip if beyond cutoff if (cutoff === undefined || tentativeDistance <= cutoff) { pq.push({node: neighbor, distance: tentativeDistance}); } } } } return distances; }