@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
192 lines • 7.14 kB
JavaScript
import { bfsWithPathCounting } from "../traversal/bfs-variants.js";
/**
* Run single-source shortest path computation using BFS
*/
function brandesSingleSource(graph, source, optimized) {
const result = bfsWithPathCounting(graph, source, optimized !== undefined ? { optimized } : {});
return {
stack: result.stack,
predecessors: result.predecessors,
sigma: result.sigma,
distance: result.distances,
};
}
/**
* Accumulate betweenness contributions from a single source
*/
function accumulateBetweenness(result, source, centrality, options) {
const { stack, predecessors, sigma } = result;
const delta = new Map();
// Initialize delta
for (const node of stack) {
delta.set(node, 0);
}
// Accumulation - back-propagation of dependencies
// Process nodes in reverse BFS order
for (let i = stack.length - 1; i >= 0; i--) {
const w = stack[i];
if (!w) {
continue;
}
const wPreds = predecessors.get(w) ?? [];
const wSigma = sigma.get(w) ?? 0;
const wDelta = delta.get(w) ?? 0;
for (const v of wPreds) {
const vSigma = sigma.get(v) ?? 0;
const vDelta = delta.get(v) ?? 0;
if (vSigma > 0 && wSigma > 0) {
let contribution = (vSigma / wSigma) * (1 + wDelta);
// Apply endpoints option: when false, exclude endpoint contributions
if (!options.endpoints) {
// For standard betweenness, don't count paths that only involve endpoints
const isTargetEndpoint = wPreds.length === 0 && w !== source;
if (isTargetEndpoint) {
contribution = 0; // Exclude endpoint contributions
}
}
delta.set(v, vDelta + contribution);
}
}
if (w !== source) {
const currentCentrality = centrality[String(w)] ?? 0;
centrality[String(w)] = currentCentrality + wDelta;
}
}
}
/**
* Accumulate edge betweenness contributions from a single source
*/
function accumulateEdgeBetweenness(result, source, edgeCentrality, options) {
const { stack, predecessors, sigma } = result;
const delta = new Map();
// Initialize delta
for (const node of stack) {
delta.set(node, 0);
}
// Accumulation for edges
// Process nodes in reverse BFS order
for (let i = stack.length - 1; i >= 0; i--) {
const w = stack[i];
if (!w) {
continue;
}
const wPreds = predecessors.get(w) ?? [];
const wSigma = sigma.get(w) ?? 0;
const wDelta = delta.get(w) ?? 0;
for (const v of wPreds) {
const vSigma = sigma.get(v) ?? 0;
if (vSigma > 0 && wSigma > 0) {
let edgeContribution = (vSigma / wSigma) * (1 + wDelta);
// Apply endpoints option for edge betweenness
if (!options.endpoints) {
const isTargetEndpoint = wPreds.length === 0 && w !== source;
if (isTargetEndpoint) {
edgeContribution = 0; // Exclude endpoint contributions
}
}
// Update edge centrality
const edgeKey = `${String(v)}-${String(w)}`;
const currentEdgeCentrality = edgeCentrality.get(edgeKey) ?? 0;
edgeCentrality.set(edgeKey, currentEdgeCentrality + edgeContribution);
// Update node delta
const vDelta = delta.get(v) ?? 0;
delta.set(v, vDelta + edgeContribution);
}
}
}
}
/**
* Apply normalization to centrality values
*/
function normalizeCentrality(centrality, nodeCount, isDirected) {
const n = nodeCount;
const normalizationFactor = isDirected ?
(n - 1) * (n - 2) :
((n - 1) * (n - 2)) / 2;
if (normalizationFactor > 0) {
for (const key in centrality) {
const value = centrality[key];
if (value !== undefined) {
centrality[key] = value / normalizationFactor;
}
}
}
}
/**
* Calculate betweenness centrality for all nodes using Brandes' algorithm
*/
export function betweennessCentrality(graph, options = {}) {
const nodes = Array.from(graph.nodes()).map((node) => node.id);
const centrality = {};
// Initialize centrality scores
for (const nodeId of nodes) {
centrality[String(nodeId)] = 0;
}
// Brandes' algorithm - run from each source
for (const source of nodes) {
const result = brandesSingleSource(graph, source, options.optimized);
accumulateBetweenness(result, source, centrality, options);
}
// For undirected graphs, divide by 2 (each shortest path is counted twice)
if (!graph.isDirected) {
for (const nodeId of nodes) {
const key = String(nodeId);
const value = centrality[key];
if (value !== undefined) {
centrality[key] = value / 2;
}
}
}
// Normalization
if (options.normalized) {
normalizeCentrality(centrality, nodes.length, graph.isDirected);
}
return centrality;
}
/**
* Calculate betweenness centrality for a specific node
*/
export function nodeBetweennessCentrality(graph, targetNode, options = {}) {
if (!graph.hasNode(targetNode)) {
throw new Error(`Node ${String(targetNode)} not found in graph`);
}
const allCentralities = betweennessCentrality(graph, options);
return allCentralities[String(targetNode)] ?? 0;
}
/**
* Calculate edge betweenness centrality
*/
export function edgeBetweennessCentrality(graph, options = {}) {
const nodes = Array.from(graph.nodes()).map((node) => node.id);
const edgeCentrality = new Map();
// Initialize edge centrality scores
for (const edge of Array.from(graph.edges())) {
const edgeKey = `${String(edge.source)}-${String(edge.target)}`;
edgeCentrality.set(edgeKey, 0);
}
// Modified Brandes' algorithm for edge betweenness
for (const source of nodes) {
const result = brandesSingleSource(graph, source, options.optimized);
accumulateEdgeBetweenness(result, source, edgeCentrality, options);
}
// For undirected graphs, divide by 2 (each shortest path is counted twice)
if (!graph.isDirected) {
for (const [edgeKey, centrality] of Array.from(edgeCentrality)) {
edgeCentrality.set(edgeKey, centrality / 2);
}
}
// Normalization for edges
if (options.normalized) {
const n = nodes.length;
const normalizationFactor = graph.isDirected ?
(n - 1) * (n - 2) :
((n - 1) * (n - 2)) / 2;
if (normalizationFactor > 0) {
for (const [edgeKey, centrality] of Array.from(edgeCentrality)) {
edgeCentrality.set(edgeKey, centrality / normalizationFactor);
}
}
}
return edgeCentrality;
}
//# sourceMappingURL=betweenness.js.map