@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
140 lines (117 loc) • 4.29 kB
text/typescript
import type {Graph} from "../../core/graph.js";
import type {CentralityOptions, CentralityResult} from "../../types/index.js";
/**
* Katz centrality implementation
*
* A generalization of eigenvector centrality that gives each node a base amount
* of influence regardless of its position in the network. Uses an attenuation
* factor (alpha) to control how much a node's centrality depends on its neighbors.
*
* Katz centrality = alpha * sum(neighbors' centrality) + beta
*
* Time complexity: O(V + E) per iteration
* Space complexity: O(V)
*/
export interface KatzCentralityOptions extends CentralityOptions {
alpha?: number; // Attenuation factor (default: 0.1)
beta?: number; // Base centrality weight (default: 1.0)
maxIterations?: number; // Maximum iterations (default: 100)
tolerance?: number; // Convergence tolerance (default: 1e-6)
}
/**
* Calculate Katz centrality for all nodes in the graph
* Uses iterative method to compute centrality scores
*/
export function katzCentrality(
graph: Graph,
options: KatzCentralityOptions = {},
): CentralityResult {
const {
alpha = 0.1,
beta = 1.0,
maxIterations = 100,
tolerance = 1e-6,
normalized = true,
} = options;
const centrality: CentralityResult = {};
const nodes = Array.from(graph.nodes());
const nodeIds = nodes.map((node) => node.id);
if (nodeIds.length === 0) {
return centrality;
}
// Check if alpha is valid (should be less than 1/lambda_max)
// For simplicity, we'll trust the user's choice or use conservative default
// Initialize centrality scores
let currentScores = new Map<string, number>();
let previousScores = new Map<string, number>();
// Start with beta for all nodes
for (const nodeId of nodeIds) {
currentScores.set(nodeId.toString(), beta);
}
// Iterative computation
for (let iteration = 0; iteration < maxIterations; iteration++) {
previousScores = new Map(currentScores);
currentScores = new Map();
// Update each node's centrality
for (const nodeId of nodeIds) {
let sum = 0;
// For directed graphs, use in-neighbors
// For undirected graphs, use all neighbors
const neighbors = graph.isDirected ?
Array.from(graph.inNeighbors(nodeId)) :
graph.neighbors(nodeId);
for (const neighbor of neighbors) {
const neighborKey = neighbor.toString();
sum += previousScores.get(neighborKey) ?? 0;
}
currentScores.set(nodeId.toString(), (alpha * sum) + beta);
}
// Check for convergence
let maxDiff = 0;
for (const [nodeId, value] of Array.from(currentScores)) {
const prevValue = previousScores.get(nodeId) ?? 0;
const diff = Math.abs(value - prevValue);
maxDiff = Math.max(maxDiff, diff);
}
if (maxDiff < tolerance) {
break;
}
}
// Prepare results
for (const [nodeId, value] of Array.from(currentScores)) {
centrality[nodeId] = value;
}
// Normalize if requested
if (normalized) {
let maxValue = 0;
let minValue = Number.POSITIVE_INFINITY;
for (const value of Object.values(centrality)) {
maxValue = Math.max(maxValue, value);
minValue = Math.min(minValue, value);
}
const range = maxValue - minValue;
if (range > 0) {
for (const nodeId of Object.keys(centrality)) {
const centralityValue = centrality[nodeId];
if (centralityValue !== undefined) {
centrality[nodeId] = (centralityValue - minValue) / range;
}
}
}
}
return centrality;
}
/**
* Calculate Katz centrality for a specific node
*/
export function nodeKatzCentrality(
graph: Graph,
nodeId: string | number,
options: KatzCentralityOptions = {},
): number {
if (!graph.hasNode(nodeId)) {
throw new Error(`Node ${String(nodeId)} not found in graph`);
}
const centrality = katzCentrality(graph, options);
return centrality[nodeId.toString()] ?? 0;
}