@graphty/algorithms 2.0.3 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +2 -2
  2. package/dist/algorithms.js +85 -47
  3. package/dist/algorithms.js.map +1 -1
  4. package/dist/algorithms.standalone.js +86 -48
  5. package/dist/algorithms.standalone.js.map +1 -1
  6. package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts +9 -3
  7. package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts.map +1 -1
  8. package/dist/src/algorithms/centrality/delta-pagerank-simple.js +29 -31
  9. package/dist/src/algorithms/centrality/delta-pagerank-simple.js.map +1 -1
  10. package/dist/src/algorithms/centrality/eigenvector.d.ts +6 -3
  11. package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -1
  12. package/dist/src/algorithms/centrality/eigenvector.js +14 -5
  13. package/dist/src/algorithms/centrality/eigenvector.js.map +1 -1
  14. package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
  15. package/dist/src/algorithms/centrality/pagerank.js +7 -3
  16. package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
  17. package/dist/src/core/graph.d.ts.map +1 -1
  18. package/dist/src/core/graph.js +7 -3
  19. package/dist/src/core/graph.js.map +1 -1
  20. package/dist/src/errors.d.ts +22 -0
  21. package/dist/src/errors.d.ts.map +1 -1
  22. package/dist/src/errors.js +24 -0
  23. package/dist/src/errors.js.map +1 -1
  24. package/dist/src/index.d.ts +1 -1
  25. package/dist/src/index.d.ts.map +1 -1
  26. package/dist/src/index.js +1 -1
  27. package/dist/src/index.js.map +1 -1
  28. package/dist/src/indexed/dijkstra.d.ts +2 -0
  29. package/dist/src/indexed/dijkstra.d.ts.map +1 -1
  30. package/dist/src/indexed/dijkstra.js +32 -13
  31. package/dist/src/indexed/dijkstra.js.map +1 -1
  32. package/dist/src/link-prediction/adamic-adar.js +1 -1
  33. package/dist/src/link-prediction/adamic-adar.js.map +1 -1
  34. package/dist/src/link-prediction/common-neighbors.js +1 -1
  35. package/dist/src/link-prediction/common-neighbors.js.map +1 -1
  36. package/dist/src/types/index.d.ts +4 -0
  37. package/dist/src/types/index.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/algorithms/centrality/delta-pagerank-simple.ts +44 -33
  40. package/src/algorithms/centrality/eigenvector.ts +14 -5
  41. package/src/algorithms/centrality/pagerank.ts +9 -3
  42. package/src/core/graph.ts +8 -3
  43. package/src/errors.ts +29 -0
  44. package/src/index.ts +1 -1
  45. package/src/indexed/dijkstra.ts +33 -13
  46. package/src/link-prediction/adamic-adar.ts +1 -1
  47. package/src/link-prediction/common-neighbors.ts +1 -1
  48. package/src/types/index.ts +4 -0
package/README.md CHANGED
@@ -110,8 +110,8 @@ class Graph {
110
110
  ```typescript
111
111
  interface GraphConfig {
112
112
  directed: boolean; // Default: false
113
- allowSelfLoops: boolean; // Default: false
114
- allowParallelEdges: boolean; // Default: false
113
+ allowSelfLoops: boolean; // Default: true
114
+ allowParallelEdges: boolean; // Default: false. When true, re-adding a pair replaces its edge (one edge per pair)
115
115
  }
116
116
  ```
117
117
 
@@ -85,7 +85,8 @@ class Graph {
85
85
  if (!this.config.allowSelfLoops && source === target) {
86
86
  throw new Error("Self-loops are not allowed in this graph");
87
87
  }
88
- if (!this.config.allowParallelEdges && this.hasEdge(source, target)) {
88
+ const replacing = this.hasEdge(source, target);
89
+ if (!this.config.allowParallelEdges && replacing) {
89
90
  throw new Error("Parallel edges are not allowed in this graph");
90
91
  }
91
92
  const edge = { source, target, weight, data };
@@ -107,7 +108,9 @@ class Graph {
107
108
  }
108
109
  }
109
110
  }
110
- this.edgeCount++;
111
+ if (!replacing) {
112
+ this.edgeCount++;
113
+ }
111
114
  this.mutations++;
112
115
  }
113
116
  /**
@@ -352,6 +355,23 @@ class ConvergenceError extends Error {
352
355
  this.name = "ConvergenceError";
353
356
  }
354
357
  }
358
+ class PathWalkError extends Error {
359
+ /**
360
+ * Builds the error and its message.
361
+ * @param source - The search's source node index
362
+ * @param target - The node index the walk started from
363
+ * @param reason - `"gap"` or `"cycle"`, as above
364
+ */
365
+ constructor(source, target, reason) {
366
+ super(
367
+ `cannot walk the shortest path from node ${String(source)} to node ${String(target)}: the predecessor array has a ${reason}`
368
+ );
369
+ this.source = source;
370
+ this.target = target;
371
+ this.reason = reason;
372
+ this.name = "PathWalkError";
373
+ }
374
+ }
355
375
  var lib = {};
356
376
  var SparseTypedFastBitSet = {};
357
377
  var utils = {};
@@ -5253,7 +5273,7 @@ class PriorityDeltaPageRank {
5253
5273
  }
5254
5274
  }
5255
5275
  function eigenvectorCentrality(graph, options = {}) {
5256
- const { maxIterations = 100, tolerance = 1e-6, normalized = true, startVector } = options;
5276
+ const { maxIterations = 100, tolerance = 1e-6, normalized = true, startVector, mode = "in" } = options;
5257
5277
  const nodeIds = Array.from(graph.nodes(), (node) => node.id);
5258
5278
  const keys = nodeIds.map((id) => id.toString());
5259
5279
  const n = keys.length;
@@ -5262,7 +5282,13 @@ function eigenvectorCentrality(graph, options = {}) {
5262
5282
  return centrality;
5263
5283
  }
5264
5284
  const index2 = new Map(keys.map((key, i) => [key, i]));
5265
- const adjacency = nodeIds.map((id) => Array.from(graph.neighbors(id), (m) => index2.get(m.toString()) ?? 0));
5285
+ const feeders = (id) => {
5286
+ if (!graph.isDirected || mode === "out") {
5287
+ return graph.neighbors(id);
5288
+ }
5289
+ return mode === "in" ? graph.inNeighbors(id) : /* @__PURE__ */ new Set([...graph.inNeighbors(id), ...graph.neighbors(id)]);
5290
+ };
5291
+ const adjacency = nodeIds.map((id) => Array.from(feeders(id), (m) => index2.get(m.toString()) ?? 0));
5266
5292
  if (isNilpotent(adjacency)) {
5267
5293
  for (const key of keys) {
5268
5294
  centrality[key] = 0;
@@ -5550,6 +5576,8 @@ class SimpleDeltaPageRank {
5550
5576
  */
5551
5577
  constructor(graph) {
5552
5578
  this.previousScores = null;
5579
+ this.iterations = 0;
5580
+ this.converged = false;
5553
5581
  if (!graph.isDirected) {
5554
5582
  throw new Error("PageRank requires a directed graph");
5555
5583
  }
@@ -5562,20 +5590,32 @@ class SimpleDeltaPageRank {
5562
5590
  * @param options.dampingFactor - Probability of following a link (default: 0.85)
5563
5591
  * @param options.tolerance - Convergence tolerance threshold (default: 1e-6)
5564
5592
  * @param options.maxIterations - Maximum number of iterations (default: 100)
5593
+ * @param options.initialRanks - Starting scores (missing nodes start at 1/n; normalized to sum 1)
5565
5594
  * @param options.personalization - Personalization vector for Personalized PageRank
5566
5595
  * @param options.weight - Edge attribute name for weighted PageRank
5567
- * @returns Map of node IDs to their PageRank scores
5596
+ * @returns Map of node IDs to their PageRank scores; `iterations` and `converged` describe the run
5568
5597
  */
5569
5598
  compute(options = {}) {
5570
- const { dampingFactor = 0.85, tolerance = 1e-6, maxIterations = 100, personalization, weight } = options;
5599
+ const {
5600
+ dampingFactor = 0.85,
5601
+ tolerance = 1e-6,
5602
+ maxIterations = 100,
5603
+ initialRanks,
5604
+ personalization,
5605
+ weight
5606
+ } = options;
5607
+ this.iterations = 0;
5608
+ this.converged = false;
5571
5609
  if (this.nodeCount === 0) {
5610
+ this.converged = true;
5572
5611
  return /* @__PURE__ */ new Map();
5573
5612
  }
5574
5613
  const scores = /* @__PURE__ */ new Map();
5575
5614
  const newScores = /* @__PURE__ */ new Map();
5576
5615
  for (const node of this.graph.nodes()) {
5577
- scores.set(node.id, 1 / this.nodeCount);
5616
+ scores.set(node.id, initialRanks?.get(node.id) ?? 1 / this.nodeCount);
5578
5617
  }
5618
+ this.normalizeMap(scores);
5579
5619
  const outWeights = /* @__PURE__ */ new Map();
5580
5620
  const danglingNodes = /* @__PURE__ */ new Set();
5581
5621
  for (const node of this.graph.nodes()) {
@@ -5583,8 +5623,7 @@ class SimpleDeltaPageRank {
5583
5623
  let outDegree = 0;
5584
5624
  for (const neighbor of Array.from(this.graph.neighbors(node.id))) {
5585
5625
  outDegree++;
5586
- const edge = this.graph.getEdge(node.id, neighbor);
5587
- totalWeight += edge?.weight ?? 1;
5626
+ totalWeight += weight ? this.graph.getEdge(node.id, neighbor)?.weight ?? 1 : 1;
5588
5627
  }
5589
5628
  outWeights.set(node.id, totalWeight);
5590
5629
  if (outDegree === 0) {
@@ -5596,7 +5635,6 @@ class SimpleDeltaPageRank {
5596
5635
  personalVector = new Map(personalization);
5597
5636
  this.normalizeMap(personalVector);
5598
5637
  }
5599
- let activeNodes = new Set(scores.keys());
5600
5638
  let iteration = 0;
5601
5639
  for (iteration = 0; iteration < maxIterations; iteration++) {
5602
5640
  for (const nodeId of scores.keys()) {
@@ -5622,7 +5660,7 @@ class SimpleDeltaPageRank {
5622
5660
  }
5623
5661
  }
5624
5662
  }
5625
- for (const nodeId of activeNodes) {
5663
+ for (const nodeId of scores.keys()) {
5626
5664
  const currentRank = scores.get(nodeId) ?? 0;
5627
5665
  const nodeOutWeight = outWeights.get(nodeId) ?? 0;
5628
5666
  if (nodeOutWeight > 0) {
@@ -5638,33 +5676,21 @@ class SimpleDeltaPageRank {
5638
5676
  }
5639
5677
  }
5640
5678
  }
5641
- const nextActive = /* @__PURE__ */ new Set();
5642
5679
  let maxDiff = 0;
5643
5680
  for (const nodeId of scores.keys()) {
5644
- const oldRank = scores.get(nodeId) ?? 0;
5645
- const newRank = newScores.get(nodeId) ?? 0;
5646
- const diff = Math.abs(newRank - oldRank);
5647
- maxDiff = Math.max(maxDiff, diff);
5648
- if (diff > tolerance / 10) {
5649
- nextActive.add(nodeId);
5650
- for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
5651
- nextActive.add(neighbor);
5652
- }
5653
- for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) {
5654
- nextActive.add(neighbor);
5655
- }
5656
- }
5681
+ maxDiff = Math.max(maxDiff, Math.abs((newScores.get(nodeId) ?? 0) - (scores.get(nodeId) ?? 0)));
5657
5682
  }
5658
5683
  scores.clear();
5659
5684
  for (const [k, v] of newScores) {
5660
5685
  scores.set(k, v);
5661
5686
  }
5662
5687
  newScores.clear();
5663
- activeNodes = nextActive;
5664
5688
  if (maxDiff < tolerance) {
5689
+ this.converged = true;
5665
5690
  break;
5666
5691
  }
5667
5692
  }
5693
+ this.iterations = this.converged ? iteration + 1 : iteration;
5668
5694
  this.previousScores = new Map(scores);
5669
5695
  return scores;
5670
5696
  }
@@ -5685,6 +5711,8 @@ class SimpleDeltaPageRank {
5685
5711
  return this.compute(options);
5686
5712
  }
5687
5713
  const { dampingFactor = 0.85, tolerance = 1e-6, maxIterations = 100, personalization, weight } = options;
5714
+ this.iterations = 0;
5715
+ this.converged = false;
5688
5716
  const scores = new Map(this.previousScores);
5689
5717
  const newScores = /* @__PURE__ */ new Map();
5690
5718
  for (const node of this.graph.nodes()) {
@@ -5709,8 +5737,7 @@ class SimpleDeltaPageRank {
5709
5737
  let outDegree = 0;
5710
5738
  for (const neighbor of Array.from(this.graph.neighbors(node.id))) {
5711
5739
  outDegree++;
5712
- const edge = this.graph.getEdge(node.id, neighbor);
5713
- totalWeight += edge?.weight ?? 1;
5740
+ totalWeight += weight ? this.graph.getEdge(node.id, neighbor)?.weight ?? 1 : 1;
5714
5741
  }
5715
5742
  outWeights.set(node.id, totalWeight);
5716
5743
  if (outDegree === 0) {
@@ -5787,9 +5814,11 @@ class SimpleDeltaPageRank {
5787
5814
  newScores.clear();
5788
5815
  activeNodes = nextActive;
5789
5816
  if (maxDiff < tolerance) {
5817
+ this.converged = true;
5790
5818
  break;
5791
5819
  }
5792
5820
  }
5821
+ this.iterations = this.converged ? iteration + 1 : iteration;
5793
5822
  this.previousScores = new Map(scores);
5794
5823
  return scores;
5795
5824
  }
@@ -5832,6 +5861,9 @@ function pageRank$1(graph, options = {}) {
5832
5861
  tolerance,
5833
5862
  maxIterations
5834
5863
  };
5864
+ if (initialRanks) {
5865
+ deltaOptions.initialRanks = initialRanks;
5866
+ }
5835
5867
  if (personalization) {
5836
5868
  deltaOptions.personalization = personalization;
5837
5869
  }
@@ -5846,9 +5878,8 @@ function pageRank$1(graph, options = {}) {
5846
5878
  }
5847
5879
  return {
5848
5880
  ranks: result2,
5849
- iterations: maxIterations,
5850
- // For now, assume we used all iterations
5851
- converged: true
5881
+ iterations: deltaPageRank.iterations,
5882
+ converged: deltaPageRank.converged
5852
5883
  };
5853
5884
  }
5854
5885
  let ranks = /* @__PURE__ */ new Map();
@@ -5950,7 +5981,8 @@ function pageRank$1(graph, options = {}) {
5950
5981
  }
5951
5982
  return {
5952
5983
  ranks: result,
5953
- iterations: iteration + 1,
5984
+ // The loop breaks before incrementing on convergence, and leaves at maxIterations otherwise.
5985
+ iterations: converged ? iteration + 1 : iteration,
5954
5986
  converged
5955
5987
  };
5956
5988
  }
@@ -10122,7 +10154,7 @@ function adamicAdarPrediction(graph, options = {}) {
10122
10154
  for (let j = i + 1; j < nodes.length; j++) {
10123
10155
  const source = nodes[i];
10124
10156
  const target = nodes[j];
10125
- if (!source || !target) {
10157
+ if (source === void 0 || target === void 0) {
10126
10158
  continue;
10127
10159
  }
10128
10160
  if (!includeExisting && graph.hasEdge(source, target)) {
@@ -10300,7 +10332,7 @@ function commonNeighborsPrediction(graph, options = {}) {
10300
10332
  for (let j = i + 1; j < nodes.length; j++) {
10301
10333
  const source = nodes[i];
10302
10334
  const target = nodes[j];
10303
- if (!source || !target) {
10335
+ if (source === void 0 || target === void 0) {
10304
10336
  continue;
10305
10337
  }
10306
10338
  if (!includeExisting && graph.hasEdge(source, target)) {
@@ -11604,6 +11636,22 @@ class IndexedMinHeap {
11604
11636
  this.slot[node] = at;
11605
11637
  }
11606
11638
  }
11639
+ function walkBack(g, predArc, source, target) {
11640
+ const arcs = [];
11641
+ let node = target;
11642
+ while (node !== source) {
11643
+ if (arcs.length >= g.nodeCount - 1) {
11644
+ throw new PathWalkError(source, target, "cycle");
11645
+ }
11646
+ const arc = predArc[node];
11647
+ if (!(arc < g.arcCount)) {
11648
+ throw new PathWalkError(source, target, "gap");
11649
+ }
11650
+ arcs.push(arc);
11651
+ node = arcSourceIn(g.rowPtr, arc);
11652
+ }
11653
+ return arcs;
11654
+ }
11607
11655
  function walkPredArcs(g, predArc, source, target) {
11608
11656
  if (target === source) {
11609
11657
  return Uint32Array.of(source);
@@ -11611,12 +11659,7 @@ function walkPredArcs(g, predArc, source, target) {
11611
11659
  if (predArc[target] === INVALID_INDEX) {
11612
11660
  return new Uint32Array(0);
11613
11661
  }
11614
- const reversed = [target];
11615
- let node = target;
11616
- while (node !== source) {
11617
- node = arcSourceIn(g.rowPtr, predArc[node]);
11618
- reversed.push(node);
11619
- }
11662
+ const reversed = [target, ...walkBack(g, predArc, source, target).map((arc) => arcSourceIn(g.rowPtr, arc))];
11620
11663
  const out = new Uint32Array(reversed.length);
11621
11664
  for (let i = 0; i < reversed.length; i++) {
11622
11665
  out[i] = reversed[reversed.length - 1 - i];
@@ -11627,13 +11670,7 @@ function walkPredEdges(g, predArc, source, target) {
11627
11670
  if (target === source || predArc[target] === INVALID_INDEX) {
11628
11671
  return new Uint32Array(0);
11629
11672
  }
11630
- const reversed = [];
11631
- let node = target;
11632
- while (node !== source) {
11633
- const arc = predArc[node];
11634
- reversed.push(g.arcToEdge[arc]);
11635
- node = arcSourceIn(g.rowPtr, arc);
11636
- }
11673
+ const reversed = walkBack(g, predArc, source, target).map((arc) => g.arcToEdge[arc]);
11637
11674
  const out = new Uint32Array(reversed.length);
11638
11675
  for (let i = 0; i < reversed.length; i++) {
11639
11676
  out[i] = reversed[reversed.length - 1 - i];
@@ -11811,6 +11848,7 @@ export {
11811
11848
  DirectionOptimizedBFS,
11812
11849
  Graph,
11813
11850
  GraphBitSet,
11851
+ PathWalkError,
11814
11852
  PriorityDeltaPageRank,
11815
11853
  PriorityQueue,
11816
11854
  UnionFind,