@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
294 lines • 12.2 kB
JavaScript
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