UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

294 lines 12.2 kB
import { PriorityQueue } from "../../data-structures/priority-queue.js"; export class DeltaPageRank { constructor(graph) { if (!graph.isDirected) { throw new Error("DeltaPageRank requires a directed graph"); } this.graph = graph; this.scores = new Map(); this.deltas = new Map(); this.activeNodes = new Set(); this.outDegrees = new Map(); this.outWeights = new Map(); this.danglingNodes = new Set(); this.nodeCount = graph.nodeCount; this.initialize(); } initialize() { const initialValue = 1.0 / this.nodeCount; // Initialize data structures for (const node of this.graph.nodes()) { const nodeId = node.id; this.scores.set(nodeId, 0); // Start with zero, will add initial value as delta this.deltas.set(nodeId, initialValue); this.activeNodes.add(nodeId); // Precompute out-degrees and weights let outDegree = 0; let totalOutWeight = 0; for (const neighbor of Array.from(this.graph.neighbors(nodeId))) { outDegree++; const edge = this.graph.getEdge(nodeId, neighbor); totalOutWeight += edge?.weight ?? 1; } this.outDegrees.set(nodeId, outDegree); this.outWeights.set(nodeId, totalOutWeight); if (outDegree === 0) { this.danglingNodes.add(nodeId); } } } compute(options = {}) { const { dampingFactor = 0.85, tolerance = 1e-6, maxIterations = 100, deltaThreshold = tolerance / 10, personalization, weight, } = options; if (dampingFactor < 0 || dampingFactor > 1) { throw new Error("Damping factor must be between 0 and 1"); } if (this.nodeCount === 0) { return new Map(); } // Normalize personalization vector if provided let personalVector = null; if (personalization) { personalVector = new Map(personalization); this.normalizeMap(personalVector); } const randomJump = (1 - dampingFactor) / this.nodeCount; let iteration = 0; while (this.activeNodes.size > 0 && iteration < maxIterations) { iteration++; // Create next iteration's deltas const nextDeltas = new Map(); // Initialize all nodes with zero delta for (const node of this.graph.nodes()) { nextDeltas.set(node.id, 0); } // Handle dangling nodes contribution let danglingSum = 0; for (const danglingNode of this.danglingNodes) { const currentScore = this.scores.get(danglingNode) ?? 0; const delta = this.deltas.get(danglingNode) ?? 0; danglingSum += currentScore + delta; } if (danglingSum > 0) { const danglingContribution = dampingFactor * danglingSum / this.nodeCount; for (const node of this.graph.nodes()) { const nodeId = node.id; const currentDelta = nextDeltas.get(nodeId) ?? 0; if (personalVector) { const personalContrib = danglingContribution * (personalVector.get(nodeId) ?? 0); nextDeltas.set(nodeId, currentDelta + personalContrib); } else { nextDeltas.set(nodeId, currentDelta + danglingContribution); } } } // Process only active nodes for (const nodeId of this.activeNodes) { const delta = this.deltas.get(nodeId) ?? 0; // Skip if delta is too small if (Math.abs(delta) < deltaThreshold) { continue; } // Apply delta to score const currentScore = this.scores.get(nodeId) ?? 0; this.scores.set(nodeId, currentScore + delta); const outWeight = this.outWeights.get(nodeId) ?? 0; if (outWeight > 0) { // Distribute delta to neighbors for (const neighbor of Array.from(this.graph.neighbors(nodeId))) { let edgeWeight = 1; if (weight) { const edge = this.graph.getEdge(nodeId, neighbor); edgeWeight = edge?.weight ?? 1; } const contribution = dampingFactor * delta * (edgeWeight / outWeight); const currentDelta = nextDeltas.get(neighbor) ?? 0; nextDeltas.set(neighbor, currentDelta + contribution); } } } // Add teleportation probability as delta for (const node of this.graph.nodes()) { const nodeId = node.id; const currentDelta = nextDeltas.get(nodeId) ?? 0; if (personalVector) { nextDeltas.set(nodeId, currentDelta + ((1 - dampingFactor) * (personalVector.get(nodeId) ?? 0))); } else { nextDeltas.set(nodeId, currentDelta + randomJump); } } // Find active nodes for next iteration const nextActive = new Set(); let maxDelta = 0; for (const [nodeId, delta] of nextDeltas) { maxDelta = Math.max(maxDelta, Math.abs(delta)); if (Math.abs(delta) >= deltaThreshold) { nextActive.add(nodeId); } } // Update for next iteration this.deltas = nextDeltas; this.activeNodes = nextActive; // Global convergence check if (maxDelta < tolerance) { break; } } // Normalize final scores this.normalizeMap(this.scores); return new Map(this.scores); } /** * Update PageRank scores after graph modification * This is where delta-based approach really shines */ update(modifiedNodes, options = {}) { // Mark modified nodes and their neighbors as active this.activeNodes.clear(); for (const nodeId of modifiedNodes) { this.activeNodes.add(nodeId); // Add incoming neighbors for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) { this.activeNodes.add(neighbor); } // Add outgoing neighbors for (const neighbor of Array.from(this.graph.neighbors(nodeId))) { this.activeNodes.add(neighbor); } } // Recompute only for active nodes return this.compute(options); } normalizeMap(map) { let sum = 0; for (const value of map.values()) { sum += value; } if (sum > 0) { for (const [key, value] of Array.from(map)) { map.set(key, value / sum); } } } } /** * Priority queue based implementation for even better performance * Processes nodes in order of their delta magnitude */ export class PriorityDeltaPageRank { constructor(graph) { if (!graph.isDirected) { throw new Error("PriorityDeltaPageRank requires a directed graph"); } this.graph = graph; this.scores = new Map(); this.nodeDeltas = new Map(); this.outWeights = new Map(); this.danglingNodes = new Set(); this.nodeCount = graph.nodeCount; // Priority queue ordered by delta magnitude (larger deltas have higher priority) this.priorityQueue = new PriorityQueue((a, b) => b - a); this.initialize(); } initialize() { const initialValue = 1.0 / this.nodeCount; // Initialize all nodes with initial delta for (const node of this.graph.nodes()) { const nodeId = node.id; this.scores.set(nodeId, 0); // Start with zero scores this.nodeDeltas.set(nodeId, initialValue); this.priorityQueue.enqueue(nodeId, initialValue); // Precompute out-weights let totalOutWeight = 0; let outDegree = 0; for (const neighbor of Array.from(this.graph.neighbors(nodeId))) { outDegree++; const edge = this.graph.getEdge(nodeId, neighbor); totalOutWeight += edge?.weight ?? 1; } this.outWeights.set(nodeId, totalOutWeight); if (outDegree === 0) { this.danglingNodes.add(nodeId); } } } computeWithPriority(options = {}) { const { dampingFactor = 0.85, tolerance = 1e-6, maxIterations = 100, deltaThreshold = tolerance / 10, weight, } = options; let iteration = 0; let processedCount = 0; while (!this.priorityQueue.isEmpty() && iteration < maxIterations) { const nodeId = this.priorityQueue.dequeue(); if (nodeId === undefined) { break; } const delta = this.nodeDeltas.get(nodeId) ?? 0; // Skip if delta is too small if (Math.abs(delta) < deltaThreshold) { continue; } // Apply delta to node's score const currentScore = this.scores.get(nodeId) ?? 0; this.scores.set(nodeId, currentScore + delta); this.nodeDeltas.set(nodeId, 0); // Distribute delta to neighbors const outWeight = this.outWeights.get(nodeId) ?? 0; if (outWeight > 0) { for (const neighbor of Array.from(this.graph.neighbors(nodeId))) { let edgeWeight = 1; if (weight) { const edge = this.graph.getEdge(nodeId, neighbor); edgeWeight = edge?.weight ?? 1; } const contribution = dampingFactor * delta * (edgeWeight / outWeight); const currentNeighborDelta = this.nodeDeltas.get(neighbor) ?? 0; const newDelta = currentNeighborDelta + contribution; this.nodeDeltas.set(neighbor, newDelta); // Re-enqueue neighbor with updated priority if (Math.abs(newDelta) >= deltaThreshold) { this.priorityQueue.enqueue(neighbor, Math.abs(newDelta)); } } } processedCount++; // Periodic convergence check if (processedCount % 1000 === 0) { let maxDelta = 0; for (const d of this.nodeDeltas.values()) { maxDelta = Math.max(maxDelta, Math.abs(d)); } if (maxDelta < tolerance) { break; } } iteration++; } // Apply any remaining deltas for (const [nodeId, delta] of this.nodeDeltas) { if (Math.abs(delta) >= deltaThreshold) { const currentScore = this.scores.get(nodeId) ?? 0; this.scores.set(nodeId, currentScore + delta); } } // Add teleportation probability const teleport = (1 - dampingFactor) / this.nodeCount; for (const [nodeId, score] of this.scores) { this.scores.set(nodeId, score + teleport); } // Normalize final scores this.normalizeMap(this.scores); return new Map(this.scores); } normalizeMap(map) { let sum = 0; for (const value of map.values()) { sum += value; } if (sum > 0) { for (const [key, value] of Array.from(map)) { map.set(key, value / sum); } } } } //# sourceMappingURL=delta-pagerank.js.map